diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5896c16 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Config.cs b/Config.cs index 8fe7f15..b81ee73 100644 --- a/Config.cs +++ b/Config.cs @@ -1,69 +1,82 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem; + using System.ComponentModel; -using SSMenuSystem.Features; + #if EXILED using Exiled.API.Interfaces; #endif +using Features; -namespace SSMenuSystem -{ - /// - public class Config +/// +// ReSharper disable UnusedAutoPropertyAccessor.Global +public class Config #if EXILED - : IConfig + : IConfig #endif - { - /// - public bool IsEnabled { get; set; } = true; +{ + /// + /// Gets or sets a value indicating whether the plugin should be enabled. + /// + public bool IsEnabled { get; set; } = true; - /// - public bool Debug { get; set; } + /// + /// Gets or sets a value indicating whether the plugin should display debugging logs. + /// + public bool Debug { get; set; } - /// - /// Gets or sets a value indicating whether pins is allowed or not (pin is a thing that has been displayed on all menus). - /// - [Description("Whether pins is allowed or not (pin is a thing that has been displayed on all menus).")] - public bool AllowPinnedContent { get; set; } = true; + /// + /// Gets or sets a value indicating whether pins is allowed or not (pin is a thing that has been displayed on all menus). + /// + [Description("Whether pins is allowed or not (pin is a thing that has been displayed on all menus).")] + public bool AllowPinnedContent { get; set; } = true; - /// - /// Gets or sets a value indicating whether clients (= non-moderators) whould see errors or not. - /// - [Description("Whether clients (= non-moderators) whould see errors or not.")] - public bool ShowErrorToClient { get; set; } = true; + /// + /// Gets or sets a value indicating whether clients (= non-moderators) should see errors or not. + /// + [Description("Whether clients (= non-moderators) should see errors or not.")] + public bool ShowErrorToClient { get; set; } = true; - /// - /// Gets or sets a value indicating whether clients (= non-moderators) whould see total errors (= some plugins-content name) or not. HIGLY UNRECOMMENDED TO SET TRUE. - /// - [Description("Whether clients (= non-moderators) whould see total errors (= some plugins-content name) or not. HIGLY UNRECOMMENDED TO SET TRUE.")] - public bool ShowFullErrorToClient { get; set; } = false; + /// + /// Gets or sets a value indicating whether clients (= non-moderators) should see total errors (= some plugins-content name) or not. It is advised to leave this as false. + /// + [Description("Whether clients (= non-moderators) should see total errors (= some plugins-content name) or not. It is advised to leave this as false.")] + public bool ShowFullErrorToClient { get; set; } = false; - /// - /// Gets or sets a value indicating whether moderators (= has RA access) whould see total errors (= some plugins-content name). - /// - [Description("Whether moderators (= has RA access) whould see total errors (= some plugins-content name).")] - public bool ShowFullErrorToModerators { get; set; } = true; + /// + /// Gets or sets a value indicating whether moderators (= has RA access) should see total errors (= some plugins-content name). + /// + [Description("Whether moderators (= has RA access) should see total errors (= some plugins-content name).")] + public bool ShowFullErrorToModerators { get; set; } = true; - /// - /// If there is only one menu registered and this set to false, this menu would be automatiquely displayed. - /// - [Description("If there is only one menu registered and this set to false, this menu would be automatiquely displayed.")] - public bool ForceMainMenuEvenIfOnlyOne { get; set; } + /// + /// Gets or sets a value indicating whether singularly registered menus should be automatically shown. + /// + [Description("Indicates whether singularly registered menus should be automatically shown.")] + public bool ForceMainMenuEvenIfOnlyOne { get; set; } - /// - /// Gets or sets a value indicating whether examples is enabled. Warning: if set to true, some content of examples would be Game breaking (speed ability, scan ability, etc...) - /// - [Description("Whether examples is enabled. Warning: if set to true, some content of examples would be Game breaking (speed ability, scan ability, etc...).")] - public bool EnableExamples { get; set; } = true; + /// + /// Gets or sets a value indicating whether examples is enabled. Warning: if set to true, some content of examples would be Game breaking (speed ability, scan ability, etc...) + /// + [Description("Whether examples is enabled. Warning: if set to true, some content of examples would be Game breaking (speed ability, scan ability, etc...).")] + public bool EnableExamples { get; set; } = true; - /// - /// The comptability system config. - /// - public ComptabilityConfig ComptabilitySystem { get; set; } = new(); + /// + /// Gets or sets the compatibility system config. + /// + public CompatibilityConfig CompatibilitySystem { get; set; } = new (); #if !EXILED - /// - /// Plugin translations. - /// - public Translation Translation { get; set; } = new(); + /// + /// Gets or sets the plugin's translations. + /// + [Description("Translations for the plugin.")] + public Translation Translation { get; set; } = new (); #endif - } } \ No newline at end of file diff --git a/Configs/GroupHeader.cs b/Configs/GroupHeader.cs index 963cd48..6d332e0 100644 --- a/Configs/GroupHeader.cs +++ b/Configs/GroupHeader.cs @@ -1,38 +1,44 @@ -namespace SSMenuSystem.Configs +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Configs; + +/// +/// Button labels config. +/// +public class GroupHeader { /// - /// Button labels config. + /// Initializes a new instance of the class. /// - public class GroupHeader + /// the label text. + /// the button text. + public GroupHeader(string label, string? hint) { - /// - /// The label of button (displayed at the left). - /// - public string Label { get; set; } + this.Label = label; + this.Hint = hint ?? "MISSING_HINT"; + } - /// - /// The Button content (displayed on the button). - /// - public string Hint { get; set; } + /// + /// Initializes a new instance of the class. + /// + public GroupHeader() + { + this.Label = "MISSING_LABEL"; + this.Hint = "MISSING_HINT"; + } - /// - /// Initialize a new instance of - /// - /// the label text. - /// the button text. - public GroupHeader(string label, string hint) - { - Label = label; - Hint = hint; - } + /// + /// Gets or sets the label of button (displayed at the left). + /// + public string Label { get; set; } - /// - /// Default constructor of . - /// - public GroupHeader() - { - Label = "MISSING_LABEL"; - Hint = "MISSING_HINT"; - } - } + /// + /// Gets or sets the Button content (displayed on the button). + /// + public string Hint { get; set; } } \ No newline at end of file diff --git a/Configs/LabelButton.cs b/Configs/LabelButton.cs index 1d16e90..ebbccb1 100644 --- a/Configs/LabelButton.cs +++ b/Configs/LabelButton.cs @@ -1,38 +1,44 @@ -namespace SSMenuSystem.Configs +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Configs; + +/// +/// Button labels config. +/// +public class LabelButton { /// - /// Button labels config. + /// Initializes a new instance of the class. /// - public class LabelButton + /// the label text. + /// the button text. + public LabelButton(string label, string buttonText) { - /// - /// The label of button (displayed at the left). - /// - public string Label { get; set; } + this.Label = label; + this.ButtonText = buttonText; + } - /// - /// The Button content (displayed on the button). - /// - public string ButtonText { get; set; } + /// + /// Initializes a new instance of the class. + /// + public LabelButton() + { + this.Label = "MISSING_LABEL"; + this.ButtonText = "MISSING_VALUE"; + } - /// - /// Initialize a new instance of - /// - /// the label text. - /// the button text. - public LabelButton(string label, string buttonText) - { - Label = label; - ButtonText = buttonText; - } + /// + /// Gets or sets the label of button (displayed at the left). + /// + public string Label { get; set; } - /// - /// Default constructor of . - /// - public LabelButton() - { - Label = "MISSING_LABEL"; - ButtonText = "MISSING_VALUE"; - } - } + /// + /// Gets or sets the Button content (displayed on the button). + /// + public string ButtonText { get; set; } } \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 826e4af..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - $(MSBuildThisFileDirectory)\SSMenuSystem.ruleset - - \ No newline at end of file diff --git a/EventHandler.cs b/EventHandler.cs index 6a82991..d08e992 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -1,159 +1,202 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem; + using System; using System.Collections.Generic; using System.Linq; + +using Features; +using Features.Wrappers; using LabApi.Events.Arguments.PlayerEvents; using LabApi.Events.CustomHandlers; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Wrappers; -using UserSettings.ServerSpecific; using MEC; -using Exiled.Events.EventArgs.Player; -using Log = SSMenuSystem.Features.Log; +using UserSettings.ServerSpecific; -namespace SSMenuSystem +using Log = Features.Log; + +/// +/// The event handler. +/// +// ReSharper disable once ClassNeverInstantiated.Global +internal class EventHandler : CustomEventsHandler { - // ReSharper disable once ClassNeverInstantiated.Global - internal class EventHandler : CustomEventsHandler + /// + /// Triggered when receiving a ServerSpecificSetting input from a player. + /// + /// The player's hub. + /// The ServerSpecificSetting. + /// Thrown if the ServerSpecificSetting was not registered via this plugin. + /// Thrown if the ServerSpecificSetting has an invalid id. + public static void OnReceivingInput(ReferenceHub hub, ServerSpecificSettingBase ss) { - public override void OnPlayerJoined(PlayerJoinedEventArgs ev) => Timing.RunCoroutine(Parameters.SyncAll(ev.Player.ReferenceHub)); - public override void OnPlayerLeft(PlayerLeftEventArgs ev) => Menu.DeletePlayer(ev.Player.ReferenceHub); - public override void OnPlayerGroupChanged(PlayerGroupChangedEventArgs ev) => - SyncChangedGroup(ev.Player.ReferenceHub); - - private static void SyncChangedGroup(ReferenceHub hub) + try { - Timing.CallDelayed(0.1f, () => + if (Parameters.SyncCache.TryGetValue(hub, out List value)) { - if (Parameters.SyncCache.ContainsKey(hub)) - return; + value.Add(ss); + Log.Debug("received value that been flagged as \"SyncCached\". Redirected values to Cache."); + return; + } - Menu menu = Menu.GetCurrentPlayerMenu(hub); - menu?.Reload(hub); - if (menu == null) - Menu.LoadForPlayer(hub, null); - }); - } + if (ss.OriginalDefinition != null) + { + ss.Label = ss.OriginalDefinition.Label; + ss.HintDescription = ss.OriginalDefinition.HintDescription; + ss.SettingId = ss.OriginalDefinition.SettingId; + } + else + { + // is a pin or header. + ss.SettingId -= Menu.GetCurrentPlayerMenu(hub)?.Hash ?? 0; + } - public static void OnReceivingInput(ReferenceHub hub, ServerSpecificSettingBase ss) - { - try + // return to menu + if (ss.SettingId == -999) { - if (Parameters.SyncCache.TryGetValue(hub, out List value)) - { - value.Add(ss); - Log.Debug("received value that been flagged as \"SyncCached\". Redirected values to Cache."); - return; - } + Menu.LoadForPlayer(hub, null); + return; + } - if (ss.OriginalDefinition != null) - { - ss.Label = ss.OriginalDefinition.Label; - ss.HintDescription = ss.OriginalDefinition.HintDescription; - ss.SettingId = ss.OriginalDefinition.SettingId; - } - else // is a pin or header - ss.SettingId -= Menu.GetCurrentPlayerMenu(hub)?.Hash ?? 0; + // check permissions + Menu? menu = Menu.GetCurrentPlayerMenu(hub); + if (!menu?.CheckAccess(hub) ?? false) + { + Log.Warn($"{hub.nicknameSync.MyNick} tried to interact with menu {menu?.Name} which is disabled for him."); + Menu.LoadForPlayer(hub, null); + return; + } - // return to menu - if (ss.SettingId == -999) + // global/local keybinds + if (ss.SettingId > Keybind.Increment && ss is SSKeybindSetting setting) + { + Keybind? loadedKeybind = Menu.TryGetKeybinding(hub, ss, menu); + if (loadedKeybind != null) { - Menu.LoadForPlayer(hub, null); + loadedKeybind.Action?.Invoke(hub, setting.SyncIsPressed); return; } + } - // check permissions - Menu menu = Menu.GetCurrentPlayerMenu(hub); - if (!menu?.CheckAccess(hub) ?? false) + // load main menu + if (ss.SettingId == 0 && menu != null) + { + // return to upper menu (or main menu) + Menu? m = Menu.GetMenu(menu.MenuRelated!); + Menu.LoadForPlayer(hub, m); + } + + // Load method when input is used on specific menu. + else if (menu != null) + { + if (ss.SettingId < 0) { - Log.Warn($"{hub.nicknameSync.MyNick} tried to interact with menu {menu.Name} which is disabled for him."); - Menu.LoadForPlayer(hub, null); - return; + Menu.LoadForPlayer(hub, menu.TryGetSubMenu(ss.SettingId)); } - - // global/local keybinds - if (ss.SettingId > Keybind.Increment && ss is SSKeybindSetting setting) + else { - Keybind loadedKeybind = Menu.TryGetKeybinding(hub, ss, menu); - if (loadedKeybind != null) + if (menu.InternalSettingsSync[hub].Any(x => x.SettingId == ss.SettingId)) { - loadedKeybind.Action?.Invoke(hub, setting.SyncIsPressed); - return; + menu.InternalSettingsSync[hub][menu.InternalSettingsSync[hub].FindIndex(x => x.SettingId == ss.SettingId)] = ss; } - } - - // load main menu - if (ss.SettingId == 0 && menu != null) - { - // return to upper menu (or main menu) - Menu m = Menu.GetMenu(menu.MenuRelated); - Menu.LoadForPlayer(hub, m); - } - // load method when input is used on specific menu. - else if (menu != null) - { - if (ss.SettingId < 0) - Menu.LoadForPlayer(hub, menu.TryGetSubMenu(ss.SettingId)); else { - if (menu.InternalSettingsSync[hub].Any(x => x.SettingId == ss.SettingId)) - menu.InternalSettingsSync[hub][menu.InternalSettingsSync[hub].FindIndex(x => x.SettingId == ss.SettingId)] = ss; - else - menu.InternalSettingsSync[hub].Add(ss); - - ServerSpecificSettingBase s = menu.SentSettings.TryGetValue(hub, out ServerSpecificSettingBase[] customSettings) ? customSettings.FirstOrDefault(b => b.SettingId == ss.SettingId) : null; - if (s == null) - throw new Exception("Failed to find the sent setting."); - - switch (s) - { - case Button wBtn: - wBtn.Action?.Invoke(hub, (SSButton)ss); - break; - case Dropdown wDropdown: - wDropdown.Action?.Invoke(hub, wDropdown.Options[((SSDropdownSetting)ss).SyncSelectionIndexRaw], ((SSDropdownSetting)ss).SyncSelectionIndexRaw, (SSDropdownSetting)ss); - break; - case Plaintext wPlaintext: - wPlaintext.OnChanged?.Invoke(hub, ((SSPlaintextSetting)ss).SyncInputText, (SSPlaintextSetting)ss); - break; - case Slider wSlider: - wSlider.Action?.Invoke(hub, ((SSSliderSetting)ss).SyncFloatValue, (SSSliderSetting)ss); - break; - case YesNoButton wYesNo: - wYesNo.Action?.Invoke(hub, ((SSTwoButtonsSetting)ss).SyncIsA, (SSTwoButtonsSetting)ss); - break; - } - - if (ss.SettingId > menu.Hash) - ss.SettingId -= menu.Hash; - menu.OnInput(hub, ss); + menu.InternalSettingsSync[hub].Add(ss); } - } - // load selected menu. - else - { - if (!Menu.Menus.Any(x => x.Id == ss.SettingId)) - throw new KeyNotFoundException($"invalid loaded id ({ss.SettingId}). please report this bug to developers."); - Menu m = Menu.Menus.FirstOrDefault(x => x.Id == ss.SettingId); - Menu.LoadForPlayer(hub, m); + + ServerSpecificSettingBase? s = menu.SentSettings.TryGetValue(hub, out ServerSpecificSettingBase[] customSettings) ? customSettings.FirstOrDefault(b => b.SettingId == ss.SettingId) : null; + if (s is null) + { + throw new Exception("Failed to find the sent setting."); + } + + switch (s) + { + case Button wBtn: + wBtn.Action?.Invoke(hub, (SSButton)ss); + break; + case Dropdown wDropdown: + wDropdown.Action?.Invoke(hub, wDropdown.Options[((SSDropdownSetting)ss).SyncSelectionIndexRaw], ((SSDropdownSetting)ss).SyncSelectionIndexRaw, (SSDropdownSetting)ss); + break; + case Plaintext wPlaintext: + wPlaintext.OnChanged?.Invoke(hub, ((SSPlaintextSetting)ss).SyncInputText, (SSPlaintextSetting)ss); + break; + case Slider wSlider: + wSlider.Action?.Invoke(hub, ((SSSliderSetting)ss).SyncFloatValue, (SSSliderSetting)ss); + break; + case YesNoButton wYesNo: + wYesNo.Action?.Invoke(hub, ((SSTwoButtonsSetting)ss).SyncIsA, (SSTwoButtonsSetting)ss); + break; + } + + if (ss.SettingId > menu.Hash) + { + ss.SettingId -= menu.Hash; + } + + menu.OnInput(hub, ss); } } - catch (Exception e) + + // load selected menu. + else { - Log.Error($"there is a error while receiving input {ss.SettingId} ({ss.Label}): {e.Message}\nActivate Debugger to show full details."); -#if DEBUG - Log.Error(e.ToString()); -#else - Log.Debug(e.ToString()); -#endif - if (Plugin.Instance.Config.ShowErrorToClient) + if (Menu.Menus.All(x => x.Id != ss.SettingId)) { - Features.Utils.SendToPlayer(hub, null, new ServerSpecificSettingBase[] - { - new SSTextArea(-5, $"{Plugin.Instance.Translation.ServerError}\n{((hub.serverRoles.RemoteAdmin || Plugin.Instance.Config.ShowFullErrorToClient) && Plugin.Instance.Config.ShowFullErrorToModerators ? e.ToString() : Plugin.Instance.Translation.NoPermission)}", SSTextArea.FoldoutMode.CollapsedByDefault, Plugin.Instance.Translation.ServerError), - new SSButton(-999, Plugin.Instance.Translation.ReloadButton.Label, Plugin.Instance.Translation.ReloadButton.ButtonText) - }); + throw new KeyNotFoundException($"invalid loaded id ({ss.SettingId}). please report this bug to developers."); } + + Menu? m = Menu.Menus.FirstOrDefault(x => x.Id == ss.SettingId); + Menu.LoadForPlayer(hub, m); + } + } + catch (Exception e) + { + Log.Error($"there is a error while receiving input {ss.SettingId} ({ss.Label}): {e.Message}\nActivate Debugger to show full details."); + #if DEBUG + Log.Error(e.ToString()); + #else + Log.Debug(e.ToString()); + #endif + if (Plugin.Instance!.Config.ShowErrorToClient) + { + Utils.SendToPlayer( + hub, + null, + [new SSTextArea(-5, $"{Plugin.Instance.Translation.ServerError}\n{((hub.serverRoles.RemoteAdmin || Plugin.Instance.Config.ShowFullErrorToClient) && Plugin.Instance.Config.ShowFullErrorToModerators ? e.ToString() : Plugin.Instance.Translation.NoPermission)}", SSTextArea.FoldoutMode.CollapsedByDefault, Plugin.Instance.Translation.ServerError), new SSButton(-999, Plugin.Instance.Translation.ReloadButton.Label, Plugin.Instance.Translation.ReloadButton.ButtonText),]); } } } + + /// + public override void OnPlayerJoined(PlayerJoinedEventArgs ev) => Timing.RunCoroutine(Parameters.SyncAll(ev.Player.ReferenceHub)); + + /// + public override void OnPlayerLeft(PlayerLeftEventArgs ev) => Menu.DeletePlayer(ev.Player.ReferenceHub); + + /// + public override void OnPlayerGroupChanged(PlayerGroupChangedEventArgs ev) => SyncChangedGroup(ev.Player.ReferenceHub); + + private static void SyncChangedGroup(ReferenceHub hub) + { + Timing.CallDelayed(0.1f, () => + { + if (Parameters.SyncCache.ContainsKey(hub)) + { + return; + } + + Menu? menu = Menu.GetCurrentPlayerMenu(hub); + menu?.Reload(hub); + if (menu == null) + { + Menu.LoadForPlayer(hub, null); + } + }); + } } \ No newline at end of file diff --git a/Examples/AbilitiesExample.cs b/Examples/AbilitiesExample.cs index b9af122..75f009f 100644 --- a/Examples/AbilitiesExample.cs +++ b/Examples/AbilitiesExample.cs @@ -1,133 +1,184 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Examples; + using System; using System.Collections.Generic; + using CustomPlayerEffects; using InventorySystem; using InventorySystem.Items; using PlayerRoles; using PlayerRoles.FirstPersonControl; using PlayerStatsSystem; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Wrappers; +using Features; +using Features.Wrappers; using UnityEngine; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Examples +/// +/// An example menu for demonstrating various abilities. +/// +internal class AbilitiesExample : Menu { - internal class AbilitiesExample : Menu + private const float HealAllyHp = 50f; + private const float HealAllyRange = 3.5f; + private const byte BoostIntensity = 60; + private const float BoostHealthDrain = 5f; + private static readonly HashSet ActiveSpeedBoosts = new (); + private List settings = null!; + + /// + public override string Name { get; set; } = "Abilities Extension"; + + /// + public override int Id { get; set; } = -8; + + /// + public override Type? MenuRelated { get; set; } = typeof(MainExample); + + /// + public override ServerSpecificSettingBase[] Settings => this.GetSettings(); + + /// + protected override void OnRegistered() { - private const float HealAllyHp = 50f; - private const float HealAllyRange = 3.5f; - private const byte BoostIntensity = 60; - private const float BoostHealthDrain = 5f; - private List _settings; - private static readonly HashSet ActiveSpeedBoosts = new(); + ReferenceHub.OnPlayerRemoved += OnDisconnect; + PlayerRoleManager.OnRoleChanged += this.OnRoleChanged; + StaticUnityMethods.OnUpdate += this.OnUpdate; + } - public override ServerSpecificSettingBase[] Settings => GetSettings(); + private static void OnDisconnect(ReferenceHub hub) + { + ActiveSpeedBoosts.Remove(hub); + } - private void TryHealAlly(ReferenceHub sender) + private void TryHealAlly(ReferenceHub sender) + { + ItemIdentifier curItem = sender.inventory.CurItem; + if (curItem.TypeId != ItemType.Medkit) { - ItemIdentifier curItem = sender.inventory.CurItem; - if (curItem.TypeId != ItemType.Medkit) - return; - Vector3 position = sender.PlayerCameraReference.position; - Vector3 forward = sender.PlayerCameraReference.forward; - - while (Physics.Raycast(position, forward, out RaycastHit hitInfo, HealAllyRange) && hitInfo.collider.TryGetComponent(out HitboxIdentity component) && !HitboxIdentity.IsEnemy(component.TargetHub, sender)) - { - if (component.TargetHub == sender) - { - position += forward * 0.08f; - } - else - { - component.TargetHub.playerStats.GetModule().ServerHeal(HealAllyHp); - sender.inventory.ServerRemoveItem(curItem.SerialNumber, null); - break; - } - } + return; } - private void SetSpeedBoost(ReferenceHub hub, bool state) + Vector3 position = sender.PlayerCameraReference.position; + Vector3 forward = sender.PlayerCameraReference.forward; + + while (Physics.Raycast(position, forward, out RaycastHit hitInfo, HealAllyRange) && hitInfo.collider.TryGetComponent(out HitboxIdentity component) && !HitboxIdentity.IsEnemy(component.TargetHub, sender)) { - MovementBoost effect = hub.playerEffectsController.GetEffect(); - if (state && hub.IsHuman()) + if (component.TargetHub == sender) { - effect.ServerSetState(BoostIntensity); - ActiveSpeedBoosts.Add(hub); + position += forward * 0.08f; } else { - effect.ServerDisable(); - ActiveSpeedBoosts.Remove(hub); + component.TargetHub.playerStats.GetModule().ServerHeal(HealAllyHp); + sender.inventory.ServerRemoveItem(curItem.SerialNumber, null); + break; } } - private ServerSpecificSettingBase[] GetSettings() + } + + private void SetSpeedBoost(ReferenceHub hub, bool state) + { + MovementBoost effect = hub.playerEffectsController.GetEffect(); + if (state && hub.IsHuman()) + { + effect.ServerSetState(BoostIntensity); + ActiveSpeedBoosts.Add(hub); + } + else { - _settings = new List - { - new SSGroupHeader("Abilities"), - new Keybind(ExampleId.HealAlly, "Heal Ally", (hub, isPressed) => + effect.ServerDisable(); + ActiveSpeedBoosts.Remove(hub); + } + } + + private ServerSpecificSettingBase[] GetSettings() + { + this.settings = + [ + new SSGroupHeader("Abilities"), new Keybind( + ExampleId.HealAlly, + "Heal Ally", + (hub, isPressed) => + { + if (isPressed) { - if (isPressed) - TryHealAlly(hub); - }, KeyCode.H, hint: - $"Press this key while holding a medkit to instantly heal a stationary ally for {HealAllyHp} HP.", isGlobal:false), - new Keybind(ExampleId.SpeedBoostKey, "Speed Boost (Human-only)", (hub, isPressed) => + this.TryHealAlly(hub); + } + }, + suggestedKey: KeyCode.H, + hint: $"Press this key while holding a medkit to instantly heal a stationary ally for {HealAllyHp} HP.", + isGlobal: false), + + new Keybind( + ExampleId.SpeedBoostKey, + "Speed Boost (Human-only)", + (hub, isPressed) => { - if (hub.GetParameter(ExampleId.SpeedBoostToggle).SyncIsB) + if (hub.GetParameter(ExampleId.SpeedBoostToggle) !.SyncIsB) { if (!isPressed) + { return; - SetSpeedBoost(hub, !ActiveSpeedBoosts.Contains(hub)); + } + + this.SetSpeedBoost(hub, !ActiveSpeedBoosts.Contains(hub)); } else - SetSpeedBoost(hub, isPressed); - }, KeyCode.Y, hint: "Increase your speed by draining your health.", isGlobal: false), - new SSTwoButtonsSetting(ExampleId.SpeedBoostToggle, "Speed Boost - Activation Mode", "Hold", "Toggle") - }; + { + this.SetSpeedBoost(hub, isPressed); + } + }, + suggestedKey: KeyCode.Y, + hint: "Increase your speed by draining your health.", + isGlobal: false), - return _settings.ToArray(); - } + new SSTwoButtonsSetting( + ExampleId.SpeedBoostToggle, + "Speed Boost - Activation Mode", + "Hold", + "Toggle"), - private static void OnDisconnect(ReferenceHub hub) - { - ActiveSpeedBoosts.Remove(hub); - } + ]; - private void OnRoleChanged( - ReferenceHub userHub, - PlayerRoleBase prevRole, - PlayerRoleBase newRole) - { - this.SetSpeedBoost(userHub, false); - } + return this.settings.ToArray(); + } + + private void OnRoleChanged( + ReferenceHub userHub, + PlayerRoleBase prevRole, + PlayerRoleBase newRole) + { + this.SetSpeedBoost(userHub, false); + } - protected override void OnRegistered() + private void OnUpdate() + { + if (!StaticUnityMethods.IsPlaying) { - ReferenceHub.OnPlayerRemoved += OnDisconnect; - PlayerRoleManager.OnRoleChanged += OnRoleChanged; - StaticUnityMethods.OnUpdate += OnUpdate; + return; } - private void OnUpdate() + foreach (ReferenceHub activeSpeedBoost in ActiveSpeedBoosts) { - if (!StaticUnityMethods.IsPlaying) - return; - foreach (ReferenceHub activeSpeedBoost in ActiveSpeedBoosts) + if (!Mathf.Approximately(activeSpeedBoost.GetVelocity().SqrMagnitudeIgnoreY(), 0.0f)) { - if (!Mathf.Approximately(activeSpeedBoost.GetVelocity().SqrMagnitudeIgnoreY(), 0.0f)) - activeSpeedBoost.playerStats.DealDamage(new UniversalDamageHandler(Time.deltaTime * BoostHealthDrain, DeathTranslations.Scp207)); + activeSpeedBoost.playerStats.DealDamage(new UniversalDamageHandler(Time.deltaTime * BoostHealthDrain, DeathTranslations.Scp207)); } } + } - public override string Name { get; set; } = "Abilities Extension"; - public override int Id { get; set; } = -8; - private static class ExampleId - { - public static readonly int SpeedBoostKey = 5; - public static readonly int SpeedBoostToggle = 7; - public static readonly int HealAlly = 9; - } - public override Type MenuRelated { get; set; } = typeof(MainExample); + private static class ExampleId + { + public static readonly int SpeedBoostKey = 5; + public static readonly int SpeedBoostToggle = 7; + public static readonly int HealAlly = 9; } } \ No newline at end of file diff --git a/Examples/DemoExample.cs b/Examples/DemoExample.cs index d96fb3f..85efaa5 100644 --- a/Examples/DemoExample.cs +++ b/Examples/DemoExample.cs @@ -1,54 +1,58 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Examples; + using System; -using SSMenuSystem.Features; +using Features; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Examples +/// +/// A demo example menu. +/// +internal class DemoExample : Menu { - internal class DemoExample : Menu - { - private string[] _options = new string[4] - { - "Option 1", - "Option 2", - "Option 3", - "Option 4" - }; + private string[] options = + [ + "Option 1", + "Option 2", + "Option 3", + "Option 4", + ]; + + /// + public override ServerSpecificSettingBase[] Settings => + [ + new SSGroupHeader("GroupHeader"), + new SSTwoButtonsSetting(1, "TwoButtonsSetting", "Option A", "Option B"), + new SSTextArea(2, "TextArea"), + new SSTextArea(3, "Multiline collapsable TextArea.\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", SSTextArea.FoldoutMode.ExtendedByDefault), + new SSSliderSetting(4, "SliderSetting", 0.0f, 1f), + new SSPlaintextSetting(5, "Plaintext"), + new SSKeybindSetting(6, "KeybindSetting"), + new SSDropdownSetting(7, "DropdownSetting", this.options), + new SSDropdownSetting(8, "Scrollable DropdownSetting", this.options, entryType: SSDropdownSetting.DropdownEntryType.Scrollable), + new SSButton(9, "Button", "Press me!"), + new SSGroupHeader("Hints", hint: "Group headers are used to separate settings into subcategories."), + new SSTwoButtonsSetting(10, "Another TwoButtonsSetting", "Option A", "Option B", hint: "Two Buttons are used to store Boolean values."), + new SSSliderSetting(11, "Another SliderSetting", 0.0f, 1f, hint: "Sliders store a numeric value within a defined range."), + new SSPlaintextSetting(12, "Another Plaintext", hint: "Plaintext fields store any provided text."), + new SSKeybindSetting(13, "Another KeybindSetting", hint: "Allows checking if the player is currently holding the action key."), + new SSDropdownSetting(14, "Another DropdownSetting", this.options, hint: "Stores an integer value between 0 and the length of options minus 1."), + new SSDropdownSetting(15, "Another Scrollable DropdownSetting", this.options, entryType: SSDropdownSetting.DropdownEntryType.Scrollable, hint: "Alternative to dropdown. API is the same as in regular dropdown, but the client-side entry behaves differently."), + new SSButton(16, "Another Button", "Press me! (again)", hint: "Triggers an event whenever it is pressed."), + ]; + + /// + public override string Name { get; set; } = "Demo Example"; - public override ServerSpecificSettingBase[] Settings => new ServerSpecificSettingBase[] - { - new SSGroupHeader("GroupHeader"), - new SSTwoButtonsSetting(1, "TwoButtonsSetting", "Option A", "Option B"), - new SSTextArea(2, "TextArea"), - new SSTextArea(3, - "Multiline collapsable TextArea.\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - SSTextArea.FoldoutMode.ExtendedByDefault), - new SSSliderSetting(4, "SliderSetting", 0.0f, 1f), - new SSPlaintextSetting(5, "Plaintext"), - new SSKeybindSetting(6, "KeybindSetting"), - new SSDropdownSetting(7, "DropdownSetting", _options), - new SSDropdownSetting(8, "Scrollable DropdownSetting", _options, - entryType: SSDropdownSetting.DropdownEntryType.Scrollable), - new SSButton(9, "Button", "Press me!"), - new SSGroupHeader("Hints", hint: "Group headers are used to separate settings into subcategories."), - new SSTwoButtonsSetting(10, "Another TwoButtonsSetting", "Option A", "Option B", - hint: "Two Buttons are used to store Boolean values."), - new SSSliderSetting(11, "Another SliderSetting", 0.0f, 1f, - hint: "Sliders store a numeric value within a defined range."), - new SSPlaintextSetting(12, "Another Plaintext", hint: "Plaintext fields store any provided text."), - new SSKeybindSetting(13, "Another KeybindSetting", - hint: "Allows checking if the player is currently holding the action key."), - new SSDropdownSetting(14, "Another DropdownSetting", _options, - hint: "Stores an integer value between 0 and the length of options minus 1."), - new SSDropdownSetting(15, "Another Scrollable DropdownSetting", _options, - entryType: SSDropdownSetting.DropdownEntryType.Scrollable, - hint: - "Alternative to dropdown. API is the same as in regular dropdown, but the client-side entry behaves differently."), - new SSButton(16, "Another Button", "Press me! (again)", - hint: "Triggers an event whenever it is pressed.") - }; + /// + public override int Id { get; set; } = -6; - public override string Name { get; set; } = "Demo Example"; - public override int Id { get; set; } = -6; - public override Type MenuRelated { get; set; } = typeof(MainExample); - } + /// + public override Type? MenuRelated { get; set; } = typeof(MainExample); } \ No newline at end of file diff --git a/Examples/LightSpawnerExample.cs b/Examples/LightSpawnerExample.cs index 7a9cdcc..5aacb50 100644 --- a/Examples/LightSpawnerExample.cs +++ b/Examples/LightSpawnerExample.cs @@ -1,212 +1,252 @@ -using System; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +#pragma warning disable SA1011 // Square bracket nullability symbol spacing. +namespace SSMenuSystem.Examples; + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; + using AdminToys; using GameCore; using Mirror; +using Features; +using Features.Wrappers; using UnityEngine; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Wrappers; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Examples +/// +/// A light spawner demo. +/// +internal class LightSpawnerExample : Menu { - internal class LightSpawnerExample : Menu - { - private List _settings; - private readonly List _addedSettings = new(); - private List _presets; - private LightShadows[] _shadowsType; - private LightType[] _lightType; - private LightShape[] _lightShape; + private readonly List addedSettings = new (); + private readonly List spawnedToys = new (); + private List? settings; + private List? presets; + private LightShadows[]? shadowsType; + private LightType[]? lightType; + private LightShape[]? lightShape; + private SSTextArea? selectedColorTextArea; + + /// + public override ServerSpecificSettingBase[] Settings => this.GetSettings(); + + /// + public override string Name { get; set; } = "Light Spawner"; + + /// + public override int Id { get; set; } = -5; - private SSTextArea _selectedColorTextArea; - private bool AnySpawned => !_spawnedToys.IsEmpty(); - private readonly List _spawnedToys = new(); + /// + public override Type? MenuRelated { get; set; } = typeof(MainExample); - public override ServerSpecificSettingBase[] Settings => GetSettings(); + private bool AnySpawned => !this.spawnedToys.IsEmpty(); - private ServerSpecificSettingBase[] GetSettings() + /// + public override bool CheckAccess(ReferenceHub hub) => PermissionsHandler.IsPermitted(hub.serverRoles.Permissions, PlayerPermissions.FacilityManagement); + + /// + public override void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) + { + if (setting.SettingId > ExampleId.DestroySpecific) { - _presets ??= new List - { - new("White", Color.white), - new("Black", Color.black), - new("Gray", Color.gray), - new("Red", Color.red), - new("Green", Color.green), - new("Blue", Color.blue), - new("Yellow", Color.yellow), - new("Cyan", Color.cyan), - new("Magenta", Color.magenta), - }; - - _shadowsType ??= EnumUtils.Values; - _lightType ??= EnumUtils.Values; - _lightShape ??= EnumUtils.Values; - _selectedColorTextArea ??= new SSTextArea(ExampleId.SelectedColor, "Selected Color: None"); - - _settings = new List - { - new Slider(ExampleId.Intensity, "Intensity", 0, 100, (hub, _, _) => ReloadColorInfoForUser(hub), 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Slider(ExampleId.Range, "Range", 0, 100, null, 10, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Dropdown(ExampleId.Color, "Color (preset)", _presets.Select(x => x.Name).ToArray(), (hub, _, _, _) => ReloadColorInfoForUser(hub)), - new Plaintext(ExampleId.CustomColor, "Custom Color (R G B)", (hub, _, _) => ReloadColorInfoForUser(hub), characterLimit:11, hint: "Leave empty to use a preset."), - _selectedColorTextArea, - new Dropdown(ExampleId.ShadowType, "Shadows Type", _shadowsType.Select(x => x.ToString()).ToArray()), - new Slider(ExampleId.ShadowStrength, "Shadow Strength", 0, 100), - new Dropdown(ExampleId.LightType, "Light Type", _lightType.Select(x => x.ToString()).ToArray()), - new Dropdown(ExampleId.LightShape, "Light Shape", _lightShape.Select(x => x.ToString()).ToArray()), - new Slider(ExampleId.SpotAngle, "Spot Angle", 0, 100, null, 30, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Slider(ExampleId.InnerSpotAngle, "Inner Spot Angle", 0, 100, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Button(ExampleId.ConfirmSpawning, "Confirm Spawning", "Spawn", (hub, _) => Spawn(hub)) - }; - _settings.AddRange(_addedSettings); - return _settings.ToArray(); + this.Destroy(setting.SettingId); } - private void ReloadColorInfoForUser(ReferenceHub hub) => _selectedColorTextArea.SendTextUpdate(GetColorInfoForUser(hub), receiveFilter:(h) => h == hub); - - private void Spawn(ReferenceHub sender) + if (setting.SettingId == ExampleId.DestroyAll) { - LightSourceToy lightSourceToy = null; - foreach (GameObject gameObject in NetworkClient.prefabs.Values) - { - if (gameObject.TryGetComponent(out LightSourceToy component)) - { - lightSourceToy = UnityEngine.Object.Instantiate(component); - lightSourceToy.OnSpawned(sender, new ArraySegment(Array.Empty())); - break; - } - } + this.DestroyAll(); + } - if (lightSourceToy == null) - return; - lightSourceToy.NetworkLightIntensity = sender.GetParameter(ExampleId.Intensity).SyncFloatValue; - lightSourceToy.NetworkLightRange = sender.GetParameter(ExampleId.Range).SyncFloatValue; - Color color = GetColorInfo(sender); - lightSourceToy.NetworkLightColor = color; - lightSourceToy.NetworkShadowType = (LightShadows)sender.GetParameter(ExampleId.ShadowType).SyncSelectionIndexRaw; - lightSourceToy.NetworkShadowStrength = sender.GetParameter(ExampleId.ShadowStrength).SyncFloatValue; - lightSourceToy.NetworkLightType = (LightType)sender.GetParameter(ExampleId.LightType).SyncSelectionIndexRaw; - lightSourceToy.NetworkLightShape = (LightShape)sender.GetParameter(ExampleId.LightShape).SyncSelectionIndexRaw; - lightSourceToy.NetworkSpotAngle = sender.GetParameter(ExampleId.SpotAngle).SyncFloatValue; - lightSourceToy.NetworkInnerSpotAngle = sender.GetParameter(ExampleId.InnerSpotAngle).SyncFloatValue; - - if (!AnySpawned) - { - _addedSettings.Add(new SSGroupHeader("Spawned Lights")); - _addedSettings.Add(new Button(ExampleId.DestroyAll, "All Lights", "Destroy All (HOLD)", null, 2)); - } + base.OnInput(hub, setting); + } - string hint = - $"{lightSourceToy.LightType} Color: {color} SpawnPosition: {lightSourceToy.transform.position}" + "\n" + ("Spawned by " + sender.LoggedNameFromRefHub() + " at round time " + RoundStart.RoundLength.ToString("hh\\:mm\\:ss\\.fff", CultureInfo.InvariantCulture)); - _addedSettings.Add(new Button(ExampleId.DestroySpecific + (int)lightSourceToy.netId, $"Primitive NetID#{lightSourceToy.netId}", "Destroy (HOLD)", null, 0.4f, hint)); - _spawnedToys.Add(lightSourceToy); - ReloadAll(); - } + /// + /// Gets a color info for a user. + /// + /// The player's hub. + /// The color string for a user. + public string GetColorInfoForUser(ReferenceHub hub) + { + return "Selected color: ███████████"; + } + + private ServerSpecificSettingBase[] GetSettings() + { + this.presets ??= + [ + new ("White", Color.white), + new ("Black", Color.black), + new ("Gray", Color.gray), + new ("Red", Color.red), + new ("Green", Color.green), + new ("Blue", Color.blue), + new ("Yellow", Color.yellow), + new ("Cyan", Color.cyan), + new ("Magenta", Color.magenta), + ]; + + this.shadowsType ??= EnumUtils.Values; + this.lightType ??= EnumUtils.Values; + this.lightShape ??= EnumUtils.Values; + this.selectedColorTextArea ??= new SSTextArea(ExampleId.SelectedColor, "Selected Color: None"); + + this.settings = + [ + new Slider(ExampleId.Intensity, "Intensity", 0, 100, (hub, _, _) => this.ReloadColorInfoForUser(hub), 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Slider(ExampleId.Range, "Range", 0, 100, null, 10, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Dropdown(ExampleId.Color, "Color (preset)", this.presets.Select(x => x.Name).ToArray(), (hub, _, _, _) => this.ReloadColorInfoForUser(hub)), + new Plaintext(ExampleId.CustomColor, "Custom Color (R G B)", (hub, _, _) => this.ReloadColorInfoForUser(hub), characterLimit: 11, hint: "Leave empty to use a preset."), this.selectedColorTextArea, + new Dropdown(ExampleId.ShadowType, "Shadows Type", this.shadowsType.Select(x => x.ToString()).ToArray()), + new Slider(ExampleId.ShadowStrength, "Shadow Strength", 0, 100), + new Dropdown(ExampleId.LightType, "Light Type", this.lightType.Select(x => x.ToString()).ToArray()), + new Dropdown(ExampleId.LightShape, "Light Shape", this.lightShape.Select(x => x.ToString()).ToArray()), + new Slider(ExampleId.SpotAngle, "Spot Angle", 0, 100, null, 30, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Slider(ExampleId.InnerSpotAngle, "Inner Spot Angle", 0, 100, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Button(ExampleId.ConfirmSpawning, "Confirm Spawning", "Spawn", (hub, _) => this.Spawn(hub)), + ]; + this.settings.AddRange(this.addedSettings); + return this.settings.ToArray(); + } + + private void ReloadColorInfoForUser(ReferenceHub hub) => this.selectedColorTextArea?.SendTextUpdate(this.GetColorInfoForUser(hub), receiveFilter: (h) => h == hub); - private void DestroyAll() + private void Spawn(ReferenceHub sender) + { + LightSourceToy? lightSourceToy = null; + foreach (GameObject gameObject in NetworkClient.prefabs.Values) { - foreach (LightSourceToy toy in _spawnedToys.ToList()) + if (!gameObject.TryGetComponent(out LightSourceToy component)) { - _spawnedToys.Remove(toy); - NetworkServer.Destroy(toy.gameObject); + continue; } - _addedSettings.Clear(); - ReloadAll(); + lightSourceToy = UnityEngine.Object.Instantiate(component); + lightSourceToy.OnSpawned(sender, new ArraySegment(Array.Empty())); + break; } - public override void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) + if (!lightSourceToy) { - if (setting.SettingId > ExampleId.DestroySpecific) - Destroy(setting.SettingId); - if (setting.SettingId == ExampleId.DestroyAll) - DestroyAll(); + return; + } - base.OnInput(hub, setting); + lightSourceToy!.NetworkLightIntensity = sender.GetParameter(ExampleId.Intensity) !.SyncFloatValue; + lightSourceToy.NetworkLightRange = sender.GetParameter(ExampleId.Range) !.SyncFloatValue; + Color color = this.GetColorInfo(sender); + lightSourceToy.NetworkLightColor = color; + lightSourceToy.NetworkShadowType = (LightShadows)sender.GetParameter(ExampleId.ShadowType) !.SyncSelectionIndexRaw; + lightSourceToy.NetworkShadowStrength = sender.GetParameter(ExampleId.ShadowStrength) !.SyncFloatValue; + lightSourceToy.NetworkLightType = (LightType)sender.GetParameter(ExampleId.LightType) !.SyncSelectionIndexRaw; + lightSourceToy.NetworkLightShape = (LightShape)sender.GetParameter(ExampleId.LightShape) !.SyncSelectionIndexRaw; + lightSourceToy.NetworkSpotAngle = sender.GetParameter(ExampleId.SpotAngle) !.SyncFloatValue; + lightSourceToy.NetworkInnerSpotAngle = sender.GetParameter(ExampleId.InnerSpotAngle) !.SyncFloatValue; + + if (!this.AnySpawned) + { + this.addedSettings.Add(new SSGroupHeader("Spawned Lights")); + this.addedSettings.Add(new Button(ExampleId.DestroyAll, "All Lights", "Destroy All (HOLD)", null, 2)); } + string hint = $"{lightSourceToy.LightType} Color: {color} SpawnPosition: {lightSourceToy.transform.position}" + "\n" + ("Spawned by " + sender.LoggedNameFromRefHub() + " at round time " + RoundStart.RoundLength.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture)); + this.addedSettings.Add(new Button(ExampleId.DestroySpecific + (int)lightSourceToy.netId, $"Primitive NetID#{lightSourceToy.netId}", "Destroy (HOLD)", null, 0.4f, hint)); + this.spawnedToys.Add(lightSourceToy); + this.ReloadAll(); + } - private void Destroy(int netId) + private void DestroyAll() + { + foreach (LightSourceToy toy in this.spawnedToys.ToList()) { - if (_addedSettings.Any(x => x.SettingId == netId)) - _addedSettings.Remove(_addedSettings.First(x => x.SettingId == netId)); - - foreach (LightSourceToy toy in _spawnedToys.ToList().Where(toy => toy.netId == netId - ExampleId.DestroySpecific)) - { - _spawnedToys.Remove(toy); - NetworkServer.Destroy(toy.gameObject); - break; - } + this.spawnedToys.Remove(toy); + NetworkServer.Destroy(toy.gameObject); + } - if (!AnySpawned) - _addedSettings.Clear(); + this.addedSettings.Clear(); + this.ReloadAll(); + } - ReloadAll(); + private void Destroy(int netId) + { + if (this.addedSettings.Any(x => x.SettingId == netId)) + { + this.addedSettings.Remove(this.addedSettings.First(x => x.SettingId == netId)); } - public string GetColorInfoForUser(ReferenceHub hub) + foreach (LightSourceToy toy in this.spawnedToys.ToList().Where(toy => toy.netId == netId - ExampleId.DestroySpecific)) { - return "Selected color: ███████████"; + this.spawnedToys.Remove(toy); + NetworkServer.Destroy(toy.gameObject); + break; } - private Color GetColorInfo(ReferenceHub hub) + if (!this.AnySpawned) { - string[] array = hub.GetParameter(ExampleId.CustomColor).SyncInputText.Split(' '); - int selectionIndex = hub.GetParameter(ExampleId.Color).SyncSelectionIndexRaw; - Color color = _presets[selectionIndex].Color; - - return new Color( - !array.TryGet(0, out string element1) || !float.TryParse(element1, out float result1) - ? color.r - : result1 / byte.MaxValue, - !array.TryGet(1, out string element2) || !float.TryParse(element2, out float result2) - ? color.g - : result2 / byte.MaxValue, - !array.TryGet(2, out string element3) || !float.TryParse(element3, out float result3) - ? color.b - : result3 / byte.MaxValue); + this.addedSettings.Clear(); } - public override bool CheckAccess(ReferenceHub hub) => PermissionsHandler.IsPermitted(hub.serverRoles.Permissions, PlayerPermissions.FacilityManagement); + this.ReloadAll(); + } - public override string Name { get; set; } = "Light Spawner"; - public override int Id { get; set; } = -5; + private Color GetColorInfo(ReferenceHub hub) + { + string[] array = hub.GetParameter(ExampleId.CustomColor) !.SyncInputText.Split(' '); + int selectionIndex = hub.GetParameter(ExampleId.Color) !.SyncSelectionIndexRaw; + Color color = this.presets![selectionIndex].Color; + +#pragma warning disable SA1118 // parameter spans multiple lines. + return new Color( + !array.TryGet(0, out string element1) || !float.TryParse(element1, out float result1) + ? color.r + : result1 / byte.MaxValue, + !array.TryGet(1, out string element2) || !float.TryParse(element2, out float result2) + ? color.g + : result2 / byte.MaxValue, + !array.TryGet(2, out string element3) || !float.TryParse(element3, out float result3) + ? color.b + : result3 / byte.MaxValue); +#pragma warning restore SA1118 + } + private static class ExampleId + { // ReSharper disable ConvertToConstant.Local - private static class ExampleId - { - internal static readonly int Intensity = 1; - internal static readonly int Range = 2; - internal static readonly int Color = 3; - internal static readonly int CustomColor = 4; - internal static readonly int SelectedColor = 5; - internal static readonly int ShadowType = 6; - internal static readonly int ShadowStrength = 7; - internal static readonly int LightType = 8; - internal static readonly int LightShape = 9; - internal static readonly int SpotAngle = 10; - internal static readonly int InnerSpotAngle = 11; - internal static readonly int ConfirmSpawning = 12; - internal static readonly int DestroyAll = 13; - internal static readonly int DestroySpecific = 14; - } + internal static readonly int Intensity = 1; + internal static readonly int Range = 2; + internal static readonly int Color = 3; + internal static readonly int CustomColor = 4; + internal static readonly int SelectedColor = 5; + internal static readonly int ShadowType = 6; + internal static readonly int ShadowStrength = 7; + internal static readonly int LightType = 8; + internal static readonly int LightShape = 9; + internal static readonly int SpotAngle = 10; + internal static readonly int InnerSpotAngle = 11; + internal static readonly int ConfirmSpawning = 12; + internal static readonly int DestroyAll = 13; + internal static readonly int DestroySpecific = 14; + // ReSharper restore ConvertToConstant.Local + } + +#pragma warning disable SA1201 // A struct should not follow a class + private readonly struct ColorPreset + { + public readonly string Name; + public readonly Color Color; - private readonly struct ColorPreset + public ColorPreset(string name, Color color) { - public ColorPreset(string name, Color color) - { - Name = name; - Color = color; - } - public readonly string Name; - public readonly Color Color; + this.Name = name; + this.Color = color; } - - public override Type MenuRelated { get; set; } = typeof(MainExample); } -} \ No newline at end of file +} +#pragma warning restore SA1011, SA1201 diff --git a/Examples/MainExample.cs b/Examples/MainExample.cs index 86e5bcf..f12476f 100644 --- a/Examples/MainExample.cs +++ b/Examples/MainExample.cs @@ -1,12 +1,28 @@ -using SSMenuSystem.Features; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +#pragma warning disable SA1010 // brackets. +namespace SSMenuSystem.Examples; + +using Features; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Examples +/// +/// The main example menu. +/// +internal class MainExample : Menu { - internal class MainExample : Menu - { - public override ServerSpecificSettingBase[] Settings => new ServerSpecificSettingBase[] { }; - public override string Name { get; set; } = "Examples"; - public override int Id { get; set; } = -200987; - } -} \ No newline at end of file + /// + public override ServerSpecificSettingBase[] Settings => []; + + /// + public override string Name { get; set; } = "Examples"; + + /// + public override int Id { get; set; } = -200987; +} +#pragma warning restore SA1010 diff --git a/Examples/PrimitiveSpawnerExample.cs b/Examples/PrimitiveSpawnerExample.cs index 270d70e..f7e4f1c 100644 --- a/Examples/PrimitiveSpawnerExample.cs +++ b/Examples/PrimitiveSpawnerExample.cs @@ -1,223 +1,277 @@ -using System; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Examples; + +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; + using AdminToys; -using Exiled.Events.Commands.Hub; using GameCore; using Mirror; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Wrappers; +using Features; +using Features.Wrappers; using UnityEngine; using UserSettings.ServerSpecific; using Utils.NonAllocLINQ; -using Log = SSMenuSystem.Features.Log; -namespace SSMenuSystem.Examples +using Log = Features.Log; + +/// +/// The primitive spawner example. +/// +internal class PrimitiveSpawnerExample : Menu { - internal class PrimitiveSpawnerExample : Menu - { - private List _settings; - private readonly List _addedSettings = new(); - private List _presets; - private SSTextArea _selectedColorTextArea; - private bool AnySpawned => !_spawnedToys.IsEmpty(); - private readonly List _spawnedToys = new(); + private readonly List addedSettings = new (); + private readonly List spawnedToys = new (); + private List? settings; + private List? presets; + private SSTextArea? selectedColorTextArea; + + /// + public override ServerSpecificSettingBase[] Settings => this.GetSettings(); + + /// + public override string Name { get; set; } = "Primitive Spawner"; + + /// + public override int Id { get; set; } = -4; - public override ServerSpecificSettingBase[] Settings => GetSettings(); + /// + public override Type? MenuRelated { get; set; } = typeof(MainExample); - public ServerSpecificSettingBase[] GetSettings() + private bool AnySpawned => !this.spawnedToys.IsEmpty(); + + /// + /// Triggered whenever a player uses an input. + /// + /// The player using the input. + /// The server specific setting being used. + public override void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) + { + if (setting.SettingId > ExampleId.DestroySpecific) { - _presets = new List - { - new("White", Color.white), - new("Black", Color.black), - new("Gray", Color.gray), - new("Red", Color.red), - new("Green", Color.green), - new("Blue", Color.blue), - new("Yellow", Color.yellow), - new("Cyan", Color.cyan), - new("Magenta", Color.magenta), - }; - - _selectedColorTextArea ??= new SSTextArea(ExampleId.SelectedColor, "Selected Color: None"); - - _settings = new List - { - new Dropdown(ExampleId.Type, "Type", EnumUtils.Values.Select(x => x.ToString()).ToArray(), (hub, _, _, _) => ReloadColorInfoForUser(hub)), - new Dropdown(ExampleId.Color, "Color (preset)", _presets.Select(x => x.Name).ToArray(), (hub, _, _, _) => ReloadColorInfoForUser(hub)), - new Slider(ExampleId.Opacity, "Opacity", 0, 100, (hub, _, _) => ReloadColorInfoForUser(hub), 100, true, finalDisplayFormat: "{0}%"), - new Plaintext(ExampleId.CustomColor, "Color", (hub, _, _) => ReloadColorInfoForUser(hub), characterLimit:11, hint: "Leave empty to use a preset."), - _selectedColorTextArea, - new YesNoButton(ExampleId.Collisions, "Collisions", "Enabled", "Disabled"), - new YesNoButton(ExampleId.Renderer, "Renderer", "Visible", "Invisible", null, false, "Invisible primitives can still receive collisions."), - new Slider(ExampleId.ScaleX, "Scale (X)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Slider(ExampleId.ScaleY, "Scale (Y)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Slider(ExampleId.ScaleZ, "Scale (Z)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), - new Button(ExampleId.ConfirmSpawning, "Confirm Spawning", "Spawn", (hub, _) => Spawn(hub)) - }; - - _settings.AddRange(_addedSettings); - - return _settings.ToArray(); + this.Destroy(setting.SettingId); } - public void ReloadColorInfoForUser(ReferenceHub hub) + if (setting.SettingId == ExampleId.DestroyAll) { - Log.Info("reload color info for user " + hub.nicknameSync.MyNick + " triggered."); - _selectedColorTextArea.SendTextUpdate(GetColorInfoForUser(hub), receiveFilter: (h) => h == hub); + this.DestroyAll(); } - public void Spawn(ReferenceHub sender) - { - PrimitiveObjectToy primitiveObjectToy = null; - foreach (GameObject gameObject in NetworkClient.prefabs.Values.ToList()) - { - if (gameObject.TryGetComponent(out PrimitiveObjectToy component)) - { - primitiveObjectToy = UnityEngine.Object.Instantiate(component); - primitiveObjectToy.OnSpawned(sender, new ArraySegment(Array.Empty())); - break; - } - } + base.OnInput(hub, setting); + } - if (!primitiveObjectToy) - return; - int selection = sender.GetParameter(ExampleId.Type).SyncSelectionIndexRaw; - primitiveObjectToy.NetworkPrimitiveType = (PrimitiveType)selection; - - Color color = GetColorInfo(sender); - primitiveObjectToy.NetworkMaterialColor = color; - Vector3 scale = new(sender.GetParameter(ExampleId.ScaleX).SyncFloatValue, - sender.GetParameter(ExampleId.ScaleY).SyncFloatValue, - sender.GetParameter(ExampleId.ScaleZ).SyncFloatValue); - primitiveObjectToy.transform.localScale = scale; - primitiveObjectToy.NetworkScale = scale; - PrimitiveFlags collisions = - sender.GetParameter(ExampleId.Collisions).SyncIsA - ? PrimitiveFlags.Collidable - : PrimitiveFlags.None; - - PrimitiveFlags visible = - sender.GetParameter(ExampleId.Renderer).SyncIsA - ? PrimitiveFlags.Visible - : PrimitiveFlags.None; - - primitiveObjectToy.NetworkPrimitiveFlags = collisions | visible; - - if (!AnySpawned) - { - _addedSettings.Add(new SSGroupHeader("Spawned Primitives")); - _addedSettings.Add(new Button(ExampleId.DestroyAll, "All Primitives", "Destroy All (HOLD)", null, 2)); - } - string hint = - $"{primitiveObjectToy.PrimitiveType} Color: {color} Size: {scale} SpawnPosition: {primitiveObjectToy.transform.position}" + "\n" + "Spawned by " + sender.LoggedNameFromRefHub() + " at round time " + RoundStart.RoundLength.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); - _addedSettings.Add(new Button(ExampleId.DestroySpecific + (int)primitiveObjectToy.netId, $"Primitive NetID#{primitiveObjectToy.netId}", "Destroy (HOLD)", null, 0.4f, hint)); - _spawnedToys.Add(primitiveObjectToy); ReloadAll(); - } + /// + public override bool CheckAccess(ReferenceHub hub) => PermissionsHandler.IsPermitted(hub.serverRoles.Permissions, PlayerPermissions.FacilityManagement); + + /// + /// Gets a color for a specific user. + /// + /// The player to get the color for. + /// The color string for the player. + // ReSharper disable once MemberCanBePrivate.Global + public string GetColorInfoForUser(ReferenceHub hub) + { + return "Selected color: ███████████"; + } + + /// + /// Gets the cached settings. + /// + /// The cached settings. + // ReSharper disable once MemberCanBePrivate.Global + public ServerSpecificSettingBase[] GetSettings() + { + this.presets = + [ + new ("White", Color.white), + new ("Black", Color.black), + new ("Gray", Color.gray), + new ("Red", Color.red), + new ("Green", Color.green), + new ("Blue", Color.blue), + new ("Yellow", Color.yellow), + new ("Cyan", Color.cyan), + new ("Magenta", Color.magenta), + ]; + + this.selectedColorTextArea ??= new SSTextArea(ExampleId.SelectedColor, "Selected Color: None"); + + this.settings = + [ + new Dropdown(ExampleId.Type, "Type", EnumUtils.Values.Select(x => x.ToString()).ToArray(), (hub, _, _, _) => this.ReloadColorInfoForUser(hub)), + new Dropdown(ExampleId.Color, "Color (preset)", this.presets.Select(x => x.Name).ToArray(), (hub, _, _, _) => this.ReloadColorInfoForUser(hub)), + new Slider(ExampleId.Opacity, "Opacity", 0, 100, (hub, _, _) => this.ReloadColorInfoForUser(hub), 100, true, finalDisplayFormat: "{0}%"), + new Plaintext(ExampleId.CustomColor, "Color", (hub, _, _) => this.ReloadColorInfoForUser(hub), characterLimit: 11, hint: "Leave empty to use a preset."), this.selectedColorTextArea, + new YesNoButton(ExampleId.Collisions, "Collisions", "Enabled", "Disabled"), + new YesNoButton(ExampleId.Renderer, "Renderer", "Visible", "Invisible", null, false, "Invisible primitives can still receive collisions."), + new Slider(ExampleId.ScaleX, "Scale (X)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Slider(ExampleId.ScaleY, "Scale (Y)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Slider(ExampleId.ScaleZ, "Scale (Z)", 0, 50, null, 1, valueToStringFormat: "0.00", finalDisplayFormat: "x{0}"), + new Button(ExampleId.ConfirmSpawning, "Confirm Spawning", "Spawn", (hub, _) => this.Spawn(hub)), + ]; + + this.settings.AddRange(this.addedSettings); + + return this.settings.ToArray(); + } + + /// + /// Reloads color info for a specific user. + /// + /// The user to reload the color info for. + // ReSharper disable once MemberCanBePrivate.Global + public void ReloadColorInfoForUser(ReferenceHub hub) + { + Log.Info("reload color info for user " + hub.nicknameSync.MyNick + " triggered."); + this.selectedColorTextArea?.SendTextUpdate(this.GetColorInfoForUser(hub), receiveFilter: (h) => h == hub); + } - private void DestroyAll() + /// + /// Spawns a primitive for a specific user. + /// + /// The user to spawn the primitive for. + // ReSharper disable once MemberCanBePrivate.Global + public void Spawn(ReferenceHub sender) + { + PrimitiveObjectToy? primitiveObjectToy = null; + foreach (GameObject gameObject in NetworkClient.prefabs.Values.ToList()) { - foreach (PrimitiveObjectToy toy in _spawnedToys.ToList()) + if (gameObject.TryGetComponent(out PrimitiveObjectToy component)) { - NetworkServer.Destroy(toy.gameObject); - _spawnedToys.Remove(toy); + primitiveObjectToy = UnityEngine.Object.Instantiate(component); + primitiveObjectToy.OnSpawned(sender, new ArraySegment([])); + break; } - - _addedSettings.Clear(); - ReloadAll(); } - public override void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) + if (!primitiveObjectToy) { - if (setting.SettingId > ExampleId.DestroySpecific) - Destroy(setting.SettingId); - if (setting.SettingId == ExampleId.DestroyAll) - DestroyAll(); - - base.OnInput(hub, setting); + return; } - private void Destroy(int netId) + int selection = sender.GetParameter(ExampleId.Type) !.SyncSelectionIndexRaw; + primitiveObjectToy!.NetworkPrimitiveType = (PrimitiveType)selection; + + Color color = this.GetColorInfo(sender); + primitiveObjectToy.NetworkMaterialColor = color; + Vector3 scale = new (sender.GetParameter(ExampleId.ScaleX) !.SyncFloatValue, + sender.GetParameter(ExampleId.ScaleY) !.SyncFloatValue, + sender.GetParameter(ExampleId.ScaleZ) !.SyncFloatValue); + primitiveObjectToy.transform.localScale = scale; + primitiveObjectToy.NetworkScale = scale; + PrimitiveFlags collisions = + sender.GetParameter(ExampleId.Collisions) !.SyncIsA + ? PrimitiveFlags.Collidable + : PrimitiveFlags.None; + + PrimitiveFlags visible = + sender.GetParameter(ExampleId.Renderer) !.SyncIsA + ? PrimitiveFlags.Visible + : PrimitiveFlags.None; + + primitiveObjectToy.NetworkPrimitiveFlags = collisions | visible; + + if (!this.AnySpawned) { - int buttonId = netId; + this.addedSettings.Add(new SSGroupHeader("Spawned Primitives")); + this.addedSettings.Add(new Button(ExampleId.DestroyAll, "All Primitives", "Destroy All (HOLD)", null, 2)); + } - if (_addedSettings.Any(x => x.SettingId == buttonId)) - _addedSettings.Remove(_addedSettings.First(x => x.SettingId == buttonId)); + string hint = $"{primitiveObjectToy.PrimitiveType} Color: {color} Size: {scale} SpawnPosition: {primitiveObjectToy.transform.position}" + "\n" + "Spawned by " + sender.LoggedNameFromRefHub() + " at round time " + RoundStart.RoundLength.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture); + this.addedSettings.Add(new Button(ExampleId.DestroySpecific + (int)primitiveObjectToy.netId, $"Primitive NetID#{primitiveObjectToy.netId}", "Destroy (HOLD)", null, 0.4f, hint)); + this.spawnedToys.Add(primitiveObjectToy); + this.ReloadAll(); + } - foreach (PrimitiveObjectToy toy in _spawnedToys.ToList().Where(toy => toy.netId == netId - ExampleId.DestroySpecific)) - { - _spawnedToys.Remove(toy); - NetworkServer.Destroy(toy.gameObject); - } + private void DestroyAll() + { + foreach (PrimitiveObjectToy toy in this.spawnedToys.ToList()) + { + NetworkServer.Destroy(toy.gameObject); + this.spawnedToys.Remove(toy); + } + + this.addedSettings.Clear(); + this.ReloadAll(); + } - if (!AnySpawned) - _addedSettings.Clear(); + private void Destroy(int netId) + { + int buttonId = netId; - ReloadAll(); + if (this.addedSettings.Any(x => x.SettingId == buttonId)) + { + this.addedSettings.Remove(this.addedSettings.First(x => x.SettingId == buttonId)); } - public string GetColorInfoForUser(ReferenceHub hub) + foreach (PrimitiveObjectToy toy in this.spawnedToys.ToList().Where(toy => toy.netId == netId - ExampleId.DestroySpecific)) { - return "Selected color: ███████████"; + this.spawnedToys.Remove(toy); + NetworkServer.Destroy(toy.gameObject); } - private Color GetColorInfo(ReferenceHub hub) + if (!this.AnySpawned) { - string[] array = hub.GetParameter(ExampleId.CustomColor) - .SyncInputText.Split(' '); - int selectionIndex = hub.GetParameter(ExampleId.Color) - .SyncSelectionIndexRaw; - Color color = _presets[selectionIndex].Color; - return new Color( - !array.TryGet(0, out string element1) || !float.TryParse(element1, out float result1) - ? color.r - : result1 / byte.MaxValue, - !array.TryGet(1, out string element2) || !float.TryParse(element2, out float result2) - ? color.g - : result2 / byte.MaxValue, - !array.TryGet(2, out string element3) || !float.TryParse(element3, out float result3) - ? color.b - : result3 / byte.MaxValue, - hub.GetParameter(ExampleId.Opacity) - .SyncFloatValue / 100f); + this.addedSettings.Clear(); } - public override bool CheckAccess(ReferenceHub hub) => PermissionsHandler.IsPermitted(hub.serverRoles.Permissions, PlayerPermissions.FacilityManagement); + this.ReloadAll(); + } - public override string Name { get; set; } = "Primitive Spawner"; - public override int Id { get; set; } = -4; + private Color GetColorInfo(ReferenceHub hub) + { + string[] array = hub.GetParameter(ExampleId.CustomColor) !.SyncInputText.Split(' '); + int selectionIndex = hub.GetParameter(ExampleId.Color) !.SyncSelectionIndexRaw; + Color color = this.presets![selectionIndex].Color; +#pragma warning disable SA1118 // Spans multiple lines. + return new Color( + !array.TryGet(0, out string element1) || !float.TryParse(element1, out float result1) + ? color.r + : result1 / byte.MaxValue, + !array.TryGet(1, out string element2) || !float.TryParse(element2, out float result2) + ? color.g + : result2 / byte.MaxValue, + !array.TryGet(2, out string element3) || !float.TryParse(element3, out float result3) + ? color.b + : result3 / byte.MaxValue, + hub.GetParameter(ExampleId.Opacity) !.SyncFloatValue / 100f); +#pragma warning restore SA1118 + } + private static class ExampleId + { // ReSharper disable ConvertToConstant.Local - private static class ExampleId - { - internal static readonly int Type = 1; - internal static readonly int Color = 2; - internal static readonly int Opacity = 3; - internal static readonly int CustomColor = 4; - internal static readonly int SelectedColor = 5; - internal static readonly int Collisions = 6; - internal static readonly int Renderer = 7; - internal static readonly int ScaleX = 8; - internal static readonly int ScaleY = 9; - internal static readonly int ScaleZ = 10; - internal static readonly int ConfirmSpawning = 11; - internal static readonly int DestroyAll = 12; - internal static readonly int DestroySpecific = 13; - } + internal static readonly int Type = 1; + internal static readonly int Color = 2; + internal static readonly int Opacity = 3; + internal static readonly int CustomColor = 4; + internal static readonly int SelectedColor = 5; + internal static readonly int Collisions = 6; + internal static readonly int Renderer = 7; + internal static readonly int ScaleX = 8; + internal static readonly int ScaleY = 9; + internal static readonly int ScaleZ = 10; + internal static readonly int ConfirmSpawning = 11; + internal static readonly int DestroyAll = 12; + internal static readonly int DestroySpecific = 13; + // ReSharper restore ConvertToConstant.Local + } - private readonly struct ColorPreset - { - public ColorPreset(string name, Color color) - { - Name = name; - Color = color; - } - public readonly string Name; - public readonly Color Color; - } - public override Type MenuRelated { get; set; } = typeof(MainExample); +#pragma warning disable SA1201 // Struct shouldn't follow a class. + private readonly struct ColorPreset(string name, Color color) + { + public readonly string Name = name; + public readonly Color Color = color; } -} \ No newline at end of file + +#pragma warning restore SA1201 +} diff --git a/Examples/TextAreaExample.cs b/Examples/TextAreaExample.cs index f565300..e10824b 100644 --- a/Examples/TextAreaExample.cs +++ b/Examples/TextAreaExample.cs @@ -1,67 +1,86 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Examples; + using System; using System.Collections.Generic; + using PlayerRoles; using PlayerRoles.FirstPersonControl; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Wrappers; +using Features; +using Features.Wrappers; using UnityEngine; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Examples +/// +/// The text area example menu. +/// +internal class TextAreaExample : Menu { - internal class TextAreaExample : Menu - { - public override ServerSpecificSettingBase[] Settings => GetSettings(); - private List _settings; - private SSTextArea _responseScan; + private List? settings; + private SSTextArea? responseScan; - private ServerSpecificSettingBase[] GetSettings() - { - _responseScan ??= new SSTextArea(null, "Not Scanned yet"); - _settings = new List - { - new SSGroupHeader("Different Text Area Types"), - new SSTextArea(1, - "This text area supports Rich Text Tags."), - new SSTextArea(2, "This is another multi-line text area, but this one features auto-generated preview text when collapsed, with ellipses appearing when the text no longer fits. It also has an option enabled to collapse automatically when you switch off this settings tab. In other words, you will need to re-expand this text area each time you visit this tab.", SSTextArea.FoldoutMode.CollapseOnEntry), - new SSTextArea(3, "This multi-line text area is expanded by default but can be collapsed if needed. It will retain its previous state when toggling this tab on and off.", SSTextArea.FoldoutMode.ExtendedByDefault), - new SSTextArea(4, "This multi-line text area is similar to the one above, but it will re-expand itself after collapsing each time you visit this tab.", SSTextArea.FoldoutMode.ExtendOnEntry), - new SSTextArea(5, "This multi-line text area cannot be collapsed.\nIt remains fully expanded at all times, but supports URL links.\nExample link: [Click]"), - new SSGroupHeader("Human Scanner", false, "Generates a list of alive humans with their distances to you. The size is automatically adjusted based on the number of results."), - _responseScan, - new Button(6, "Scan for players.", "Scan", (hub, _) => OnScannerButtonPressed(hub)) - }; + /// + public override ServerSpecificSettingBase[] Settings => this.GetSettings(); - return _settings.ToArray(); - } - public override string Name { get; set; } = "Text Area"; - public override int Id { get; set; } = -7; + /// + public override string Name { get; set; } = "Text Area"; + + /// + public override int Id { get; set; } = -7; + /// + public override Type? MenuRelated { get; set; } = typeof(MainExample); - public void OnScannerButtonPressed(ReferenceHub sender) + /// + /// Triggered whenever the scanner button is pressed. + /// + /// The player that pressed the button. + private void OnScannerButtonPressed(ReferenceHub sender) + { + if (!(sender.roleManager.CurrentRole is IFpcRole currentRole1)) { - if (!(sender.roleManager.CurrentRole is IFpcRole currentRole1)) - { - this._responseScan.SendTextUpdate("Your current role is not supported.", false, x => x == sender); - } - else + this.responseScan?.SendTextUpdate("Your current role is not supported.", false, x => x == sender); + } + else + { + string? str1 = null; + foreach (ReferenceHub allHub in ReferenceHub.AllHubs) { - string str1 = null; - foreach (ReferenceHub allHub in ReferenceHub.AllHubs) + if (allHub.roleManager.CurrentRole is HumanRole currentRole && !(allHub == sender)) { - if (allHub.roleManager.CurrentRole is HumanRole currentRole && !(allHub == sender)) - { - float num = Vector3.Distance(currentRole.FpcModule.Position, currentRole1.FpcModule.Position); - string str2 = - $"\n-{allHub.nicknameSync.DisplayName} ({currentRole.GetColoredName()}) - {num} m"; - if (str1 == null) - str1 = "Detected humans: "; - str1 += str2; - } + float num = Vector3.Distance(currentRole.FpcModule.Position, currentRole1.FpcModule.Position); + string str2 = $"\n-{allHub.nicknameSync.DisplayName} ({currentRole.GetColoredName()}) - {num} m"; + str1 ??= "Detected humans: "; + + str1 += str2; } - this._responseScan.SendTextUpdate(str1 ?? "No humans detected.", false, x => x == sender); } + + this.responseScan?.SendTextUpdate(str1 ?? "No humans detected.", false, x => x == sender); } - public override Type MenuRelated { get; set; } = typeof(MainExample); + } + + private ServerSpecificSettingBase[] GetSettings() + { + this.responseScan ??= new SSTextArea(null, "Not Scanned yet"); + this.settings = + [ + new SSGroupHeader("Different Text Area Types"), + new SSTextArea(1, "This text area supports Rich Text Tags."), + new SSTextArea(2, "This is another multi-line text area, but this one features auto-generated preview text when collapsed, with ellipses appearing when the text no longer fits. It also has an option enabled to collapse automatically when you switch off this settings tab. In other words, you will need to re-expand this text area each time you visit this tab.", SSTextArea.FoldoutMode.CollapseOnEntry), + new SSTextArea(3, "This multi-line text area is expanded by default but can be collapsed if needed. It will retain its previous state when toggling this tab on and off.", SSTextArea.FoldoutMode.ExtendedByDefault), + new SSTextArea(4, "This multi-line text area is similar to the one above, but it will re-expand itself after collapsing each time you visit this tab.", SSTextArea.FoldoutMode.ExtendOnEntry), + new SSTextArea(5, "This multi-line text area cannot be collapsed.\nIt remains fully expanded at all times, but supports URL links.\nExample link: [Click]"), + new SSGroupHeader("Human Scanner", false, "Generates a list of alive humans with their distances to you. The size is automatically adjusted based on the number of results."), this.responseScan, + new Button(6, "Scan for players.", "Scan", (hub, _) => this.OnScannerButtonPressed(hub)), + ]; + + return this.settings.ToArray(); } } \ No newline at end of file diff --git a/Features/AssemblyMenu.cs b/Features/AssemblyMenu.cs index 27dd53b..15767e7 100644 --- a/Features/AssemblyMenu.cs +++ b/Features/AssemblyMenu.cs @@ -1,20 +1,61 @@ -using System; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +#pragma warning disable SA1010, SA1011 // Opening square brackets should be spaced correctly. Closing square bracket should be followed by a space. +namespace SSMenuSystem.Features; + using System.Collections.Generic; using System.Reflection; + using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features +/// +/// The Assembly Menu. +/// +internal class AssemblyMenu : Menu { - internal class AssemblyMenu : Menu - { - internal Assembly Assembly { get; set; } - internal ServerSpecificSettingBase[] OverrideSettings { get; set; } - public override ServerSpecificSettingBase[] Settings => OverrideSettings ?? Array.Empty(); - public override string Name { get; set; } - public override int Id { get; set; } - public override bool CheckAccess(ReferenceHub hub) => - (ActuallySendedToClient.TryGetValue(hub, out ServerSpecificSettingBase[] settings) && settings != null && !settings.IsEmpty()) || - (OverrideSettings != null && !OverrideSettings.IsEmpty()); - internal Dictionary ActuallySendedToClient { get; set; } = new(); - } -} \ No newline at end of file + /// + /// Gets the server specific settings. + /// + public override ServerSpecificSettingBase[] Settings => this.OverrideSettings ?? []; + + /// + /// Gets or sets the name. + /// + public override string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the id of the menu. + /// + public override int Id { get; set; } + + /// + /// Gets or Sets the dictionary of server specific settings actually sent to the client. + /// + internal Dictionary ActuallySentToClient { get; set; } = new (); + + /// + /// Gets or sets the Assembly. + /// + internal Assembly? Assembly { get; set; } + + /// + /// Gets or sets the override settings. + /// + internal ServerSpecificSettingBase[]? OverrideSettings { get; set; } + + /// + /// Checks access for a specific player. + /// + /// The player to check. + /// True if the player should have access otherwise false. + public override bool CheckAccess(ReferenceHub hub) => + (this.ActuallySentToClient.TryGetValue(hub, out ServerSpecificSettingBase[] settings) && settings != null && !settings.IsEmpty()) || + (this.OverrideSettings != null && !this.OverrideSettings.IsEmpty()); +} + +#pragma warning restore SA1010, SA1011 diff --git a/Features/CompatibilityConfig.cs b/Features/CompatibilityConfig.cs new file mode 100644 index 0000000..1a9a3d8 --- /dev/null +++ b/Features/CompatibilityConfig.cs @@ -0,0 +1,25 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features; + +using System.ComponentModel; + +/// +/// The compatibility configs. +/// +public sealed class CompatibilityConfig +{ + /// + /// Gets or sets a value indicating whether the compatibility system will be enabled and all plugins that use SSSystem will be registered as menu. + /// + [Description("If enabled, the comptability system will be enabled and all plugins that use SSSystem will be registered as menu.")] + public bool CompatibilityEnabled { get; set; } = true; + + /*[Description("If enabled, all keybinds on compatibility menu will be marked as global (displayed on all screens).")] + public bool KeybindsAllGlobal { get; set; } = true;*/ +} \ No newline at end of file diff --git a/Features/ComptabilityConfig.cs b/Features/ComptabilityConfig.cs deleted file mode 100644 index cb20d5f..0000000 --- a/Features/ComptabilityConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.ComponentModel; - -namespace SSMenuSystem.Features -{ - /// - /// The comptability configs. - /// - public class ComptabilityConfig - { - /// - /// If enabled, the comptability system will be enabled and all plugins that use SSSystem will be registered as menu. - /// - [Description("If enabled, the comptability system will be enabled and all plugins that use SSSystem will be registered as menu.")] - public bool ComptabilityEnabled { get; set; } = true; - - /*[Description("If enabled, all keybinds on comptability menu will be marked as global (displayed on all screens).")] - public bool KeybindsAllGlobal { get; set; } = true;*/ - } -} \ No newline at end of file diff --git a/Features/Interfaces/ISetting.cs b/Features/Interfaces/ISetting.cs index f94fa99..b40f690 100644 --- a/Features/Interfaces/ISetting.cs +++ b/Features/Interfaces/ISetting.cs @@ -1,9 +1,21 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Interfaces; + using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Interfaces +/// +/// An interface for defining settings. +/// +internal interface ISetting { - internal interface ISetting - { - ServerSpecificSettingBase Base { get; } - } + /// + /// Gets the base component for this item. + /// + ServerSpecificSettingBase Base { get; } } \ No newline at end of file diff --git a/Features/Log.cs b/Features/Log.cs index a0edb4e..82df2f6 100644 --- a/Features/Log.cs +++ b/Features/Log.cs @@ -1,142 +1,161 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable MemberCanBePrivate.Global +namespace SSMenuSystem.Features; + using System; + using Discord; -namespace SSMenuSystem.Features +/// +/// An internal logging class. +/// +internal static class Log { - internal static class Log + /// + /// Sends a level messages to the game console. + /// + /// The message to be sent. + internal static void Info(object message) { - /// - /// Sends a level messages to the game console. - /// - /// The message to be sent. - internal static void Info(object message) - { - Send($"[SSMenuSystem] {message}", LogLevel.Info, ConsoleColor.Cyan); - } + Send($"[SSMenuSystem] {message}", LogLevel.Info, ConsoleColor.Cyan); + } - /// - /// Sends a level messages to the game console. - /// - /// The message to be sent. - internal static void Info(string message) - { - Send($"[SSMenuSystem] {message}", LogLevel.Info, ConsoleColor.Cyan); - } + /// + /// Sends a level messages to the game console. + /// + /// The message to be sent. + internal static void Info(string message) + { + Send($"[SSMenuSystem] {message}", LogLevel.Info, ConsoleColor.Cyan); + } - /// - /// Sends a level messages to the game console. - /// Server must have exiled_debug config enabled. - /// - /// The message to be sent. - internal static void Debug(object message) + /// + /// Sends a level messages to the game console. + /// Server must have exiled_debug config enabled. + /// + /// The message to be sent. + internal static void Debug(object message) + { + if (Plugin.Instance?.Config?.Debug ?? false) { - if (Plugin.Instance?.Config?.Debug ?? false) - Send($"[SSMenuSystem] {message}", LogLevel.Debug, ConsoleColor.Green); + Send($"[SSMenuSystem] {message}", LogLevel.Debug, ConsoleColor.Green); } + } - /// - /// Sends a level messages to the game console. - /// Server must have exiled_debug config enabled. - /// - /// The message to be sent. - internal static void Debug(string message) + /// + /// Sends a level messages to the game console. + /// Server must have exiled_debug config enabled. + /// + /// The message to be sent. + internal static void Debug(string message) + { + if (Plugin.Instance?.Config?.Debug ?? false) { - if (Plugin.Instance?.Config?.Debug ?? false) - Send($"[SSMenuSystem] {message}", LogLevel.Debug, ConsoleColor.Green); + Send($"[SSMenuSystem] {message}", LogLevel.Debug, ConsoleColor.Green); } + } - /// - /// Sends a level messages to the game console. - /// - /// The message to be sent. - internal static void Warn(object message) - { - Log.Send($"[SSMenuSystem] {message}", LogLevel.Warn, ConsoleColor.Magenta); - } + /// + /// Sends a level messages to the game console. + /// + /// The message to be sent. + internal static void Warn(object message) + { + Send($"[SSMenuSystem] {message}", LogLevel.Warn, ConsoleColor.Magenta); + } - /// - /// Sends a level messages to the game console. - /// - /// The message to be sent. - internal static void Warn(string message) - { - Log.Send($"[SSMenuSystem] {message}", LogLevel.Warn, ConsoleColor.Magenta); - } + /// + /// Sends a level messages to the game console. + /// + /// The message to be sent. + internal static void Warn(string message) + { + Send($"[SSMenuSystem] {message}", LogLevel.Warn, ConsoleColor.Magenta); + } - /// - /// Sends a level messages to the game console. - /// This should be used to send errors only. - /// It's recommended to send any messages in the catch block of a try/catch as errors with the exception string. - /// - /// The message to be sent. - internal static void Error(object message) - { - Log.Send($"[SSMenuSystem] {message}", LogLevel.Error, ConsoleColor.DarkRed); - } + /// + /// Sends a level messages to the game console. + /// This should be used to send errors only. + /// It's recommended to send any messages in the catch block of a try/catch as errors with the exception string. + /// + /// The message to be sent. + internal static void Error(object message) + { + Send($"[SSMenuSystem] {message}", LogLevel.Error, ConsoleColor.DarkRed); + } - /// - /// Sends a level messages to the game console. - /// This should be used to send errors only. - /// It's recommended to send any messages in the catch block of a try/catch as errors with the exception string. - /// - /// The message to be sent. - internal static void Error(string message) - { - Send($"[SSMenuSystem] {message}", LogLevel.Error, ConsoleColor.DarkRed); - } + /// + /// Sends a level messages to the game console. + /// This should be used to send errors only. + /// It's recommended to send any messages in the catch block of a try/catch as errors with the exception string. + /// + /// The message to be sent. + internal static void Error(string message) + { + Send($"[SSMenuSystem] {message}", LogLevel.Error, ConsoleColor.DarkRed); + } - /// Sends a log message to the game console. - /// The message to be sent. - /// The message level of importance. - /// The message color. - internal static void Send(object message, LogLevel level, ConsoleColor color = ConsoleColor.Gray) - { - SendRaw($"[{level.ToString().ToUpper()}] {message}", color); - } + /// Sends a log message to the game console. + /// The message to be sent. + /// The message level of importance. + /// The message color. + internal static void Send(object message, LogLevel level, ConsoleColor color = ConsoleColor.Gray) + { + SendRaw($"[{level.ToString().ToUpper()}] {message}", color); + } - /// Sends a log message to the game console. - /// The message to be sent. - /// The message level of importance. - /// The message color. - internal static void Send(string message, LogLevel level, ConsoleColor color = ConsoleColor.Gray) - { - SendRaw("[" + level.ToString().ToUpper() + "] " + message, color); - } + /// Sends a log message to the game console. + /// The message to be sent. + /// The message level of importance. + /// The message color. + internal static void Send(string message, LogLevel level, ConsoleColor color = ConsoleColor.Gray) + { + SendRaw("[" + level.ToString().ToUpper() + "] " + message, color); + } - /// Sends a raw log message to the game console. - /// The message to be sent. - /// The of the message. - internal static void SendRaw(object message, ConsoleColor color) - { - ServerConsole.AddLog(message.ToString(), color); - } + /// Sends a raw log message to the game console. + /// The message to be sent. + /// The of the message. + internal static void SendRaw(object message, ConsoleColor color) + { + ServerConsole.AddLog(message.ToString(), color); + } - /// Sends a raw log message to the game console. - /// The message to be sent. - /// The of the message. - internal static void SendRaw(string message, ConsoleColor color) - { - ServerConsole.AddLog(message, color); - } + /// Sends a raw log message to the game console. + /// The message to be sent. + /// The of the message. + internal static void SendRaw(string message, ConsoleColor color) + { + ServerConsole.AddLog(message, color); + } - /// - /// Sends an with the provided message if the condition is false and stops the execution. - /// For example: - /// - /// Player ply = Player.Get(2); - /// Log.Assert(ply is not null, "The player with the id 2 is null"); - /// - /// results in it logging an error if the player is null and not continuing. - /// - /// - /// The conditional expression to evaluate. If the condition is true it will continue. - /// The information message. The error and exception will show this message. - /// If the condition is false. It throws an exception stopping the execution. - internal static void Assert(bool condition, object message) + /// + /// Sends an with the provided message if the condition is false and stops the execution. + /// For example: + /// + /// Player ply = Player.Get(2); + /// Log.Assert(ply is not null, "The player with the id 2 is null"); + /// + /// results in it logging an error if the player is null and not continuing. + /// + /// + /// The conditional expression to evaluate. If the condition is true it will continue. + /// The information message. The error and exception will show this message. + /// If the condition is false. It throws an exception stopping the execution. + internal static void Assert(bool condition, object message) + { + if (condition) { - if (condition) return; - Error(message); - throw new Exception(message.ToString()); + return; } + + Error(message); + throw new Exception(message.ToString()); } } \ No newline at end of file diff --git a/Features/Menu.cs b/Features/Menu.cs index ac941af..7023958 100644 --- a/Features/Menu.cs +++ b/Features/Menu.cs @@ -1,576 +1,714 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- +#pragma warning disable SA1018, SA1124, SA1401 // Nullable symbol should be preceded with a space. Do not use regions. Field should be made private. +namespace SSMenuSystem.Features; + using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; + using MEC; -using SSMenuSystem.Examples; -using SSMenuSystem.Features.Interfaces; -using SSMenuSystem.Features.Wrappers; +using Examples; +using Interfaces; +using JetBrains.Annotations; +using Wrappers; using UnityEngine; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features +/// +/// The Menu system. +/// +public abstract class Menu { +#region Fields + + /// + /// All synced parameters for a specified . + /// + internal readonly Dictionary> InternalSettingsSync = new (); + + /// + /// Gets all settings sent to the refHub. (only in the case of one GetSettings is not null or empty). + /// + internal readonly Dictionary SentSettings = new (); + + private static readonly Dictionary MenuSync = new (); + private static readonly List LoadedMenus = new (); + private static readonly Dictionary Pinned = new (); + + // ReSharper disable once FieldCanBeMadeReadOnly.Local + private static Queue waitingAssemblies = new (); + + // private static readonly Dictionary> GlobalKeybindingSync = new(); +#endregion +#region Properties + + /// + /// Gets all loaded menus. + /// + public static List Menus => LoadedMenus; + + /// + /// Gets all global keybinds registered for each menu. + /// + [PublicAPI] + public static Dictionary> GlobalKeybindingSync => Menus.ToDictionary(menu => menu, menu => menu.Settings?.Where(x => x is Keybind { IsGlobal: true }).Select(x => x as Keybind).ToList()) !; + + /// + /// Gets all global keybinds registered for each menu. + /// + [Obsolete("Use GlobalKeybindingSync instead", true)] + public static ReadOnlyDictionary> GlobalKeybindings => new (GlobalKeybindingSync); + + /// + /// Gets or Sets the id of Menu. Must be above 0 and must not be equal to 0. + /// + public abstract int Id { get; set; } + + /// + /// Gets or Sets the name of Menu. + /// + public abstract string Name { get; set; } + + /// + /// Gets In-Build Settings. + /// + public abstract ServerSpecificSettingBase[] ? Settings { get; } + + /// + /// Gets or Sets if this menu is related to a (will be shown as a SubMenu). + /// + public virtual Type? MenuRelated { get; set; } = null; + + /// + /// Gets all synced parameters for a specified . + /// + public ReadOnlyDictionary> SettingsSync => new (this.InternalSettingsSync); + + /// + /// Gets the Hash of menu, based on . Mainly used to separate menu settings for client. + /// + public int Hash => Mathf.Abs(this.Name.GetHashCode() % 100000); + /// - /// The Menu system. + /// Gets or Sets the description of Menu. Will be shown as . /// - public abstract class Menu + protected virtual string Description { get; set; } = string.Empty; + +#endregion +#region Public Methods + + /// + /// Register all menus in the . + /// + public static void RegisterAll() => Register(Assembly.GetCallingAssembly()); + + /// + /// Register specific menu. + /// + /// The target menu. + /// One of parameters of target menu is invalid. please check the to find the invalid parameter. + public static void Register(Menu? menu) { - private static readonly Dictionary MenuSync = new(); - private static readonly List LoadedMenus = new(); - private static readonly Dictionary Pinned = new(); - - /// - /// All menus loaded. - /// - public static List Menus => LoadedMenus; - // private static readonly Dictionary> GlobalKeybindingSync = new(); - - /// - /// All Global Keybinds registered for each menu. - /// - public static Dictionary> GlobalKeybindingSync => Menus.ToDictionary( - menu => menu, - menu => menu.Settings.Where(x => x is Keybind k && k.IsGlobal).Select(x => x as Keybind).ToList()); - - /// - /// All Global Keybinds registered for each menu. - /// - [Obsolete("Use GlobalKeybindingSync instead", true)] - public static ReadOnlyDictionary> GlobalKeybindings => new(GlobalKeybindingSync); - - /// - /// All synced parameters for a specified . - /// - internal readonly Dictionary> InternalSettingsSync = new(); - - /// - /// All synced parameters for a specified . - /// - public ReadOnlyDictionary> SettingsSync => new(InternalSettingsSync); - - /// - /// This is used to see if can use or not. - /// - /// The target - /// True => Player can view and use the menu, and False => can't saw and use. - public virtual bool CheckAccess(ReferenceHub hub) => true; - - private static Queue _waitingAssemblies = new(); - - /// - /// Register all waiting assemblies when plugin is loaded. - /// - internal static void RegisterQueuedAssemblies() - { - while (_waitingAssemblies.TryDequeue(out Assembly assembly)) - Register(assembly); - } - - /// - /// Calling assembly will be, loaded if SSMenuSystem is already initialized, or queued if not, and loaded when the plugin is initialized. - /// - public static void QueueOrRegister() - { - if (Plugin.Instance?.Config is null) - { - Assembly assembly = Assembly.GetCallingAssembly(); - if (!_waitingAssemblies.Contains(assembly)) - _waitingAssemblies.Enqueue(assembly); - } - else - Register(Assembly.GetCallingAssembly()); + if (menu is null) + { + return; + } + + if (menu.MenuRelated == typeof(MainExample) && !Plugin.Instance!.Config.EnableExamples) + { + return; + } + + Log.Debug($"loading Server Specific menu {menu.Name}..."); + if (CheckSameId(menu)) + { + throw new ArgumentException($"another menu with id {menu.Id} is already registered. can't load {menu.Name}."); + } + + if (menu.Id >= 0) + { + throw new ArgumentException($"menus ids must be < 0 (to let space for parameters and 0 is only for Main Menu)."); } - /// - /// Register all menus in the . - /// - public static void RegisterAll() => Register(Assembly.GetCallingAssembly()); + if (string.IsNullOrEmpty(menu.Name)) + { + throw new ArgumentException($"menus name cannot be null or empty."); + } + + if (Menus.Any(x => x.Name == menu.Name)) + { + throw new ArgumentException($"two menus can't have the same name."); + } - /// - /// Register all menus of indicated assembly. - /// - /// The target . - private static void Register(Assembly assembly) + Dictionary> ids = new (); + + foreach (ServerSpecificSettingBase action in menu.Settings!) { - if (Plugin.Instance?.Config is null) // plugin is not loaded. + if (action is SSGroupHeader) { - if (!_waitingAssemblies.Contains(assembly)) - _waitingAssemblies.Enqueue(assembly); - return; + continue; } - try + + ServerSpecificSettingBase setting = action; + if (setting is ISetting isSetting) { - Log.Debug($"loading assembly {assembly.GetName().Name}..."); - List loadedMenus = new(); - foreach (Type type in assembly.GetTypes()) - { - if (type == typeof(AssemblyMenu)) // only used for comptability (throw error when loaded) - continue; - - if (type == typeof(MainExample) && (!Plugin.Instance.Config.EnableExamples)) - continue; - - if (type.IsAbstract || type.IsInterface) - continue; - if (type.BaseType != typeof(Menu)) - continue; - Menu menu = Activator.CreateInstance(type) as Menu; - loadedMenus.Add(menu); - } + setting = isSetting.Base; + } - IOrderedEnumerable orderedMenus = loadedMenus.OrderBy(x => x.MenuRelated == null ? 0 : 1).ThenBy(x => x.Id); - List registeredMenus = new(); - foreach (Menu menu in orderedMenus) - { - try - { - Register(menu); - registeredMenus.Add(menu); - } - catch (Exception e) - { - Log.Error( - $"there is a error while loading menu {menu.Name}: {e.Message}\nEnable Debugger to show full details."); -#if DEBUG - Log.Error(e.ToString()); - Log.Error("menu path: " + menu.GetType().FullName); -#else - Log.Debug(e.ToString()); -#endif - } - } + Type type = setting.GetType(); + + ids.GetOrAdd(type, () => new List()); - Log.Info( - $"loaded assembly {assembly.GetName().Name} with {registeredMenus.Count} menus. A total of {LoadedMenus.Count} menus."); + if (ids[type].Contains(setting.SettingId)) + { + throw new ArgumentException($"id {setting.SettingId} for menu {menu.Name} is duplicated."); } - catch (Exception e) + + if (setting.SettingId < 0) { - Log.Error($"failed to load assembly {assembly.GetName().Name}: {e.Message}"); -#if DEBUG - Log.Error(e.ToString()); -#else - Log.Debug(e.ToString()); -#endif + throw new ArgumentException($"id above and equal to 0 is reserved for menus and main menu."); } + + ids[type].Add(setting.SettingId); } - /// - /// Register specific menu. - /// - /// The target menu. - /// One of parameters of target menu is invalid. please check the to find the invalid parameter. - public static void Register(Menu menu) + if (menu.MenuRelated != null) { - if (menu == null) - return; - if (menu.MenuRelated == typeof(MainExample) && !Plugin.Instance.Config.EnableExamples) - return; + if (LoadedMenus.All(m => m.GetType() != menu.MenuRelated)) + { + throw new ArgumentException($"menu {menu.Name} has a invalid related menu ({menu.MenuRelated.FullName} has not been loaded."); + } + } - Log.Debug($"loading Server Specific menu {menu.Name}..."); - if (CheckSameId(menu)) - throw new ArgumentException($"another menu with id {menu.Id} is already registered. can't load {menu.Name}."); - if (menu.Id >= 0) - throw new ArgumentException($"menus ids must be < 0 (to let space for parameters and 0 is only for Main Menu)."); - if (string.IsNullOrEmpty(menu.Name)) - throw new ArgumentException($"menus name cannot be null or empty."); - if (Menus.Any(x => x.Name == menu.Name)) - throw new ArgumentException($"two menus can't have the same name."); + LoadedMenus.Add(menu); + menu.OnRegistered(); + Log.Debug($"Server Specific menu {menu.Name} is now registered!"); + } + /// + /// Calling assembly will be, loaded if SSMenuSystem is already initialized, or queued if not, and loaded when the plugin is initialized. + /// + [PublicAPI] + public static void QueueOrRegister() + { + if (Plugin.Instance?.Config is null) + { + Assembly assembly = Assembly.GetCallingAssembly(); + if (!waitingAssemblies.Contains(assembly)) + { + waitingAssemblies.Enqueue(assembly); + } + } + else + { + Register(Assembly.GetCallingAssembly()); + } + } - Dictionary> ids = new(); + /// + /// Unload a menu. + /// + /// The target menu. + [PublicAPI] + public static void Unregister(Menu menu) + { + if (LoadedMenus.Contains(menu)) + { + LoadedMenus.Remove(menu); + } - foreach (ServerSpecificSettingBase action in menu.Settings) + GlobalKeybindingSync.Remove(menu); + foreach (KeyValuePair sync in MenuSync) + { + if (sync.Value == menu) { - if (action is SSGroupHeader) - continue; - ServerSpecificSettingBase setting = action; - if (setting is ISetting isSetting) - setting = isSetting.Base; + LoadForPlayer(sync.Key, null); + } + } + } - Type type = setting.GetType(); + /// + /// Register that will be displayed on the top of all pages. + /// + /// the list of to pin. + public static void RegisterPin(SSTextArea[] toPin) => + Pinned[Assembly.GetCallingAssembly()] = toPin; - ids.GetOrAdd(type, () => new List()); + /// + /// Remove pins that was registered by . + /// + public static void UnregisterPin() => Pinned.Remove(Assembly.GetCallingAssembly()); - if (ids[type].Contains(setting.SettingId)) - throw new ArgumentException($"id {setting.SettingId} for menu {menu.Name} is duplicated."); - if (setting.SettingId < 0) - throw new ArgumentException($"id above and equal to 0 is reserved for menus and main menu."); + /// + /// Gets the loaded menu. (menu that been displayed on the ). + /// + /// The target hub. + /// if opened a menu, null if he was on the main menu. + public static Menu? GetCurrentPlayerMenu(ReferenceHub hub) => MenuSync.TryGetValue(hub, out Menu? menu) ? menu : null; - ids[type].Add(setting.SettingId); - } + /// + /// Gets the first keybind linked to . + /// + /// The target hub. + /// The specified or . + /// The current loaded menu, to get global or local keybinds. + /// if found or not. + public static Keybind? TryGetKeybinding(ReferenceHub hub, ServerSpecificSettingBase ss, Menu? menu = null) + { + int id = ss.SettingId; + if (ss is Keybind) + { + id -= 10000; + } - if (menu.MenuRelated != null) + foreach (KeyValuePair> bind in GlobalKeybindingSync.Where(x => x.Key.CheckAccess(hub))) + { + foreach (Keybind key in bind.Value) { - if (!LoadedMenus.Any(m => m.GetType() == menu.MenuRelated)) - throw new ArgumentException($"menu {menu.Name} has a invalid related menu ({menu.MenuRelated.FullName} has not been loaded."); + if (key.SettingId == id) + { + return key; + } } + } - LoadedMenus.Add(menu); - menu.OnRegistered(); - Log.Debug($"Server Specific menu {menu.Name} is now registered!"); + if (menu == null) + { + return null; } - private static bool CheckSameId(Menu menu) + if (!menu.CheckAccess(hub)) { - if (menu.MenuRelated == null) - return LoadedMenus.Any(x => x.Id == menu.Id && menu.MenuRelated == null); - return LoadedMenus.Where(x => x.MenuRelated == menu.MenuRelated).Any(x => x.Id == menu.Id); + return null; } - /// - /// Unload a menu. - /// - /// The target menu. - public static void Unregister(Menu menu) + return menu.Settings?.FirstOrDefault(x => x.SettingId == id) as Keybind; + } + + /// + /// Get a menu by . + /// + /// The type. + /// if found. + public static Menu? GetMenu(Type type) => LoadedMenus.FirstOrDefault(x => x.GetType() == type); + + /// + /// This is used to see if can use or not. + /// + /// The target . + /// True => Player can view and use the menu, and False => can't saw and use. + public virtual bool CheckAccess(ReferenceHub hub) => true; + + /// + /// Executed when action is executed on. + /// + /// The players reference hub. + /// The setting being input. + public virtual void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) + { + } + + /// + /// Get settings for the specific . + /// + /// The target . + /// A list of that will be sent into the player (like but only for the ). + public virtual ServerSpecificSettingBase[] ? GetSettingsFor(ReferenceHub hub) + { + return null; + } + + /// + /// Trys to get the sub menu related to this menu. + /// + /// The sub menu id. + /// the sub- if found. + public Menu? TryGetSubMenu(int id) => LoadedMenus.FirstOrDefault(x => x.Id == id && x.MenuRelated == this.GetType() && x != this); + + /// + /// Reload the menu of the specified . + /// + /// The target hub. + public void Reload(ReferenceHub hub) => LoadForPlayer(hub, this); + + /// + /// Reload all that loaded the . + /// + public void ReloadAll() + { + foreach (ReferenceHub hub in MenuSync.Where(x => x.Value == this).Select(x => x.Key).ToList()) { - if (LoadedMenus.Contains(menu)) - LoadedMenus.Remove(menu); - GlobalKeybindingSync.Remove(menu); - foreach (KeyValuePair sync in MenuSync) - { - if (sync.Value == menu) - LoadForPlayer(sync.Key, null); - } + LoadForPlayer(hub, this); } + } + +#endregion +#region Internal Methods - /// - /// Unload all menus loaded. - /// - internal static void UnregisterAll() + /// + /// Register all waiting assemblies when plugin is loaded. + /// + internal static void RegisterQueuedAssemblies() + { + while (waitingAssemblies.TryDequeue(out Assembly assembly)) { - foreach (KeyValuePair sync in MenuSync) - LoadForPlayer(sync.Key, null); - LoadedMenus.Clear(); - foreach (Menu menu in LoadedMenus.ToList()) - Unregister(menu); - } - - /// - /// Gets or Sets if this menu is related to a (will be shown as a SubMenu). - /// - #nullable enable - public virtual Type? MenuRelated { get; set; } = null; - #nullable disable - - /// - /// Gets In-Build Settings. - /// - public abstract ServerSpecificSettingBase[] Settings { get; } - - /// - /// Gets all settings sent to the refHub. (only in the case of one GetSettings is not null or empty) - /// - /// - internal readonly Dictionary SentSettings = new(); - - /// - /// Gets the Hash of menu, based on . Mainly used to seperate menu settings for client. - /// - public int Hash => Mathf.Abs(Name.GetHashCode() % 100000); - - /// - /// Gets or Sets the name of Menu. - /// - public abstract string Name { get; set; } - - /// - /// Gets or Sets the description of Menu. Will be shown as . - /// - protected virtual string Description { get; set; } = string.Empty; - - /// - /// Gets or Sets the id of Menu. Must be above 0 and must not be equal to 0. - /// - public abstract int Id { get; set; } - - /// - /// Get the main menu for target hub. - /// - /// The target referenceHub - /// In-build parameters that will be shown to hub. - private static ServerSpecificSettingBase[] GetMainMenu(ReferenceHub hub) - { - List mainMenu = new(); - - if (Plugin.Instance.Config.AllowPinnedContent) - mainMenu.AddRange(Pinned.Values.SelectMany(pin => pin)); - - if (LoadedMenus.Where(x => x.CheckAccess(hub)).IsEmpty()) - return mainMenu.ToArray(); - - mainMenu.Add(new SSGroupHeader("Main Menu")); - foreach (Menu menu in LoadedMenus.Where(x => x.CheckAccess(hub))) - { - if (menu.MenuRelated == null) - mainMenu.Add(new SSButton(menu.Id, string.Format(Plugin.Instance.Translation.OpenMenu.Label, menu.Name), Plugin.Instance.Translation.OpenMenu.ButtonText, null, string.IsNullOrEmpty(menu.Description) ? null : menu.Description)); - } + Register(assembly); + } + } - mainMenu.AddRange(GetGlobalKeybindings(hub, null)); + /// + /// Unload all menus loaded. + /// + internal static void UnregisterAll() + { + foreach (KeyValuePair sync in MenuSync) + { + LoadForPlayer(sync.Key, null); + } - return mainMenu.ToArray(); + LoadedMenus.Clear(); + foreach (Menu menu in LoadedMenus.ToList()) + { + Unregister(menu); } + } - private List GetSettings(ReferenceHub hub) + /// + /// Load for (only if he has access). + /// + /// The players reference hub. + /// The menu to load. + internal static void LoadForPlayer(ReferenceHub hub, Menu? menu) + { + GetCurrentPlayerMenu(hub)?.ProperlyDisable(hub); + Log.Debug("try loading " + (menu?.Name ?? "main menu") + " for player " + hub.nicknameSync.MyNick); + if (menu != null && !menu.CheckAccess(hub)) { - List settings = new(); + menu = null; + } - if (Plugin.Instance.Config.AllowPinnedContent) - settings.AddRange(Pinned.Values.SelectMany(pin => pin)); + if (menu == null && LoadedMenus.Count(x => x.CheckAccess(hub) && x.MenuRelated == null) == 1 && !Plugin.Instance!.Config!.ForceMainMenuEvenIfOnlyOne) + { + menu = Menus.First(x => x.CheckAccess(hub) && x.MenuRelated == null); + Log.Debug($"triggered The only menu registered: {menu.Name}."); + } - if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) != this || Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) - { - if (MenuRelated != null) - settings.Add(new SSButton(0, string.Format(Plugin.Instance.Translation.ReturnTo.Label, Menu.GetMenu(MenuRelated)?.Name ?? "Unknown"), - Plugin.Instance.Translation.ReturnTo.ButtonText)); - else - settings.Add(new SSButton(0, Plugin.Instance.Translation.ReturnToMenu.Label, - Plugin.Instance.Translation.ReturnToMenu.ButtonText)); - } + if (menu == null) + { + Utils.SendToPlayer(hub, null, GetMainMenu(hub)); + MenuSync[hub] = null; + return; + } + + List settings = menu.GetSettings(hub); + settings.AddRange(GetGlobalKeybindings(hub, menu)); + MenuSync[hub] = menu; + + if (!menu.SettingsSync.ContainsKey(hub)) + { + Timing.RunCoroutine(Parameters.Sync(hub, menu, settings.ToArray())); + } + else + { + Utils.SendToPlayer(hub, menu, settings.ToArray()); + } - if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) == this && !Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) - settings.Add(new SSGroupHeader(Name)); - else + menu.ProperlyEnable(hub); + } + + /// + /// Only used when player has left the server. + /// + /// The target hub. + internal static void DeletePlayer(ReferenceHub hub) + { + GetCurrentPlayerMenu(hub)?.ProperlyDisable(hub); + MenuSync.Remove(hub); + } + +#endregion + +#region Protected Methods + + /// + /// Executed when opened the menu. + /// + /// the target hub. + protected virtual void ProperlyEnable(ReferenceHub hub) + { + } + + /// + /// Executed when closed the menu. + /// + /// The target hub. + protected virtual void ProperlyDisable(ReferenceHub hub) + { + } + + /// + /// Called when the is registered. + /// + protected virtual void OnRegistered() + { + } + +#endregion +#region Private Methods + + private static bool CheckSameId(Menu menu) + { + if (menu.MenuRelated == null) + { + return LoadedMenus.Any(x => x.Id == menu.Id && menu.MenuRelated == null); + } + + return LoadedMenus.Where(x => x.MenuRelated == menu.MenuRelated).Any(x => x.Id == menu.Id); + } + + /// + /// Get the main menu for target hub. + /// + /// The target referenceHub. + /// In-build parameters that will be shown to hub. + private static ServerSpecificSettingBase[] GetMainMenu(ReferenceHub hub) + { + List mainMenu = new (); + + if (Plugin.Instance!.Config.AllowPinnedContent) + { + mainMenu.AddRange(Pinned.Values.SelectMany(pin => pin)); + } + + if (LoadedMenus.Where(x => x.CheckAccess(hub)).IsEmpty()) + { + return mainMenu.ToArray(); + } + + mainMenu.Add(new SSGroupHeader("Main Menu")); + foreach (Menu menu in LoadedMenus.Where(x => x.CheckAccess(hub))) + { + if (menu.MenuRelated == null) { - if (LoadedMenus.Count(x => x.MenuRelated == GetType() && x != this) > 0) - settings.Add(new SSGroupHeader(Plugin.Instance.Translation.SubMenuTitle.Label, hint: Plugin.Instance.Translation.SubMenuTitle.Hint)); + mainMenu.Add(new SSButton(menu.Id, string.Format(Plugin.Instance.Translation.OpenMenu.Label, menu.Name), Plugin.Instance.Translation.OpenMenu.ButtonText, null, string.IsNullOrEmpty(menu.Description) ? null : menu.Description)); } + } + + mainMenu.AddRange(GetGlobalKeybindings(hub, null)); - foreach (Menu s in LoadedMenus.Where(x => x.MenuRelated == GetType() && x != this)) - settings.Add(new SSButton(s.Id, string.Format(Plugin.Instance.Translation.OpenMenu.Label, s.Name), Plugin.Instance.Translation.OpenMenu.ButtonText, null, string.IsNullOrEmpty(Description) ? null : Description)); + return mainMenu.ToArray(); + } - if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) != this || Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) - settings.Add(new SSGroupHeader(Name, false, Description)); + /// + /// Gets global keybindings for hub. + /// + /// The target hub. + /// The loaded menu of ReferenceHub, so keys will not been duplicated. + /// In-build parameters that will be shown to hub. + private static ServerSpecificSettingBase[] GetGlobalKeybindings(ReferenceHub hub, Menu? menu) + { + List keybindings = new (); - if (this is AssemblyMenu assemblyMenu && - assemblyMenu.ActuallySendedToClient.TryGetValue(hub, out ServerSpecificSettingBase[] overrideSettings) && overrideSettings != null) + if (GlobalKeybindingSync.Any(x => x.Key.CheckAccess(hub) && x.Key != menu && x.Value.Any())) + { + keybindings.Add(new SSGroupHeader(Plugin.Instance!.Translation.GlobalKeybindingTitle.Label, hint: Plugin.Instance.Translation.GlobalKeybindingTitle.Hint)); + foreach (KeyValuePair> menuKeybinds in GlobalKeybindingSync.Where(x => x.Key.CheckAccess(hub) && x.Key != menu)) { - if (overrideSettings.IsEmpty()) - settings.RemoveAt(settings.Count - 1); - settings.AddRange(overrideSettings); - return settings; + foreach (Keybind keybind in menuKeybinds.Value) + { + keybindings.Add(new SSKeybindSetting(keybind.SettingId + menuKeybinds.Key.Hash, keybind.Label, keybind.SuggestedKey, keybind.PreventInteractionOnGUI, keybind.HintDescription)); + } } + } - ServerSpecificSettingBase[] oSettings = GetSettingsFor(hub); + return keybindings.ToArray(); + } - if ((Settings == null || Settings.IsEmpty()) && (oSettings == null || oSettings.IsEmpty())) + /// + /// Register all menus of indicated assembly. + /// + /// The target . + private static void Register(Assembly assembly) + { + if (Plugin.Instance?.Config is null) + { + // plugin is not loaded. + if (!waitingAssemblies.Contains(assembly)) { - settings.RemoveAt(settings.Count - 1); - return settings; + waitingAssemblies.Enqueue(assembly); } - List sentSettings = new(); - if (Settings != null) + return; + } + + try + { + Log.Debug($"loading assembly {assembly.GetName().Name}..."); + List loadedMenus = new (); + foreach (Type type in assembly.GetTypes()) { - foreach (ServerSpecificSettingBase t in Settings) + // only used for compatibility (throw error when loaded) + if (type == typeof(AssemblyMenu)) { - sentSettings.Add(t); - if (t is ISetting setting) - settings.Add(setting.Base); - else - settings.Add(t); + continue; } - } - if (oSettings != null && !oSettings.IsEmpty()) - { - foreach (ServerSpecificSettingBase t in oSettings) + if (type == typeof(MainExample) && (!Plugin.Instance.Config.EnableExamples)) { - sentSettings.Add(t); - if (t is ISetting setting) - settings.Add(setting.Base); - else - settings.Add(t); + continue; } - } - SentSettings[hub] = sentSettings.ToArray(); + if (type.IsAbstract || type.IsInterface) + { + continue; + } - return settings; - } + if (type.BaseType != typeof(Menu)) + { + continue; + } - /// - /// Gets global keybindings for hub. - /// - /// The target hub. - /// The loaded menu of ReferenceHub, so keys will not been duplicated. - /// In-build parameters that will be shown to hub. - private static ServerSpecificSettingBase[] GetGlobalKeybindings(ReferenceHub hub, Menu menu) - { - List keybindings = new(); + Menu? menu = Activator.CreateInstance(type) as Menu; + loadedMenus.Add(menu!); + } - if (GlobalKeybindingSync.Any(x => x.Key.CheckAccess(hub) && x.Key != menu && x.Value.Any())) + IOrderedEnumerable orderedMenus = loadedMenus.OrderBy(x => x.MenuRelated == null ? 0 : 1).ThenBy(x => x.Id); + List registeredMenus = new (); + foreach (Menu menu in orderedMenus) { - keybindings.Add(new SSGroupHeader(Plugin.Instance.Translation.GlobalKeybindingTitle.Label, hint:Plugin.Instance.Translation.GlobalKeybindingTitle.Hint)); - foreach (KeyValuePair> menuKeybinds in GlobalKeybindingSync.Where(x => x.Key.CheckAccess(hub) && x.Key != menu)) + try { - foreach (Keybind keybind in menuKeybinds.Value) - keybindings.Add(new SSKeybindSetting(keybind.SettingId + menuKeybinds.Key.Hash, keybind.Label, keybind.SuggestedKey, keybind.PreventInteractionOnGUI, keybind.HintDescription)); + Register(menu); + registeredMenus.Add(menu); + } + catch (Exception e) + { + Log.Error( + $"there is a error while loading menu {menu.Name}: {e.Message}\nEnable Debugger to show full details."); + #if DEBUG + Log.Error(e.ToString()); + Log.Error("menu path: " + menu.GetType().FullName); + #else + Log.Debug(e.ToString()); + #endif } } - return keybindings.ToArray(); - } - - /// - /// Executed when action is executed on - /// - /// - /// - public virtual void OnInput(ReferenceHub hub, ServerSpecificSettingBase setting) {} - - /// - /// Executed when opened the menu. - /// - /// the target hub. - protected virtual void ProperlyEnable(ReferenceHub hub) {} - - /// - /// Executed when closed the menu. - /// - /// The target hub. - protected virtual void ProperlyDisable(ReferenceHub hub) {} - - /// - /// Gets the loaded menu. (menu that been displayed on the ). - /// - /// The target hub - /// if opened a menu, null if he was on the main menu. - public static Menu GetCurrentPlayerMenu(ReferenceHub hub) => MenuSync.TryGetValue(hub, out Menu menu) ? menu : null; - - /// - /// Load for (only if he has access). - /// - /// - /// - internal static void LoadForPlayer(ReferenceHub hub, Menu menu) - { - GetCurrentPlayerMenu(hub)?.ProperlyDisable(hub); - Log.Debug("try loading " + (menu?.Name ?? "main menu") + " for player " + hub.nicknameSync.MyNick); - if (menu != null && !menu.CheckAccess(hub)) - menu = null; - - if (menu == null && LoadedMenus.Count(x => x.CheckAccess(hub) && x.MenuRelated == null) == 1 && !Plugin.Instance!.Config!.ForceMainMenuEvenIfOnlyOne) - { - menu = Menus.First(x => x.CheckAccess(hub) && x.MenuRelated == null); - Log.Debug($"triggered The only menu registered: {menu.Name}."); - } + Log.Info( + $"loaded assembly {assembly.GetName().Name} with {registeredMenus.Count} menus. A total of {LoadedMenus.Count} menus."); + } + catch (Exception e) + { + Log.Error($"failed to load assembly {assembly.GetName().Name}: {e.Message}"); + #if DEBUG + Log.Error(e.ToString()); + #else + Log.Debug(e.ToString()); + #endif + } + } - if (menu == null) - { - Utils.SendToPlayer(hub, null, GetMainMenu(hub)); - MenuSync[hub] = null; - return; - } + private List GetSettings(ReferenceHub hub) + { + List settings = new (); + + if (Plugin.Instance!.Config.AllowPinnedContent) + { + settings.AddRange(Pinned.Values.SelectMany(pin => pin)); + } - List settings = menu.GetSettings(hub); - settings.AddRange(GetGlobalKeybindings(hub, menu)); - MenuSync[hub] = menu; + if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) != this || Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) + { + settings.Add(this.MenuRelated != null ? new SSButton(0, string.Format(Plugin.Instance.Translation.ReturnTo.Label, GetMenu(this.MenuRelated)?.Name ?? "Unknown"), Plugin.Instance.Translation.ReturnTo.ButtonText) : new SSButton(0, Plugin.Instance.Translation.ReturnToMenu.Label, Plugin.Instance.Translation.ReturnToMenu.ButtonText)); + } - if (!menu.SettingsSync.ContainsKey(hub)) + if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) == this && !Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) + { + settings.Add(new SSGroupHeader(this.Name)); + } + else + { + if (LoadedMenus.Count(x => x.MenuRelated == this.GetType() && x != this) > 0) { - Timing.RunCoroutine(Parameters.Sync(hub, menu, settings.ToArray())); + settings.Add(new SSGroupHeader(Plugin.Instance.Translation.SubMenuTitle.Label, hint: Plugin.Instance.Translation.SubMenuTitle.Hint)); } - else - Utils.SendToPlayer(hub, menu, settings.ToArray()); + } - menu.ProperlyEnable(hub); + foreach (Menu s in LoadedMenus.Where(x => x.MenuRelated == this.GetType() && x != this)) + { + settings.Add(new SSButton(s.Id, string.Format(Plugin.Instance.Translation.OpenMenu.Label, s.Name), Plugin.Instance.Translation.OpenMenu.ButtonText, null, string.IsNullOrEmpty(this.Description) ? null : this.Description)); + } + + if (LoadedMenus.First(x => x.CheckAccess(hub) && x.MenuRelated == null) != this || Plugin.Instance.Config.ForceMainMenuEvenIfOnlyOne) + { + settings.Add(new SSGroupHeader(this.Name, false, this.Description)); } - /// - /// Only used when player has left the server - /// - /// The target hub. - internal static void DeletePlayer(ReferenceHub hub) + if (this is AssemblyMenu assemblyMenu && + assemblyMenu.ActuallySentToClient.TryGetValue(hub, out ServerSpecificSettingBase[] overrideSettings) && overrideSettings != null) { - GetCurrentPlayerMenu(hub)?.ProperlyDisable(hub); - MenuSync.Remove(hub); + if (overrideSettings.IsEmpty()) + { + settings.RemoveAt(settings.Count - 1); + } + + settings.AddRange(overrideSettings); + return settings; } - /// - /// Try get sub menu related to this menu. - /// - /// The sub menu id. - /// the sub- if found. - public Menu TryGetSubMenu(int id) => LoadedMenus.FirstOrDefault(x => x.Id == id && x.MenuRelated == GetType() && x != this); + ServerSpecificSettingBase[] ? oSettings = this.GetSettingsFor(hub); - /// - /// Gets the first keybind linked to . - /// - /// The target hub. - /// The specified or . - /// The current loaded menu, to get global or local keybinds. - /// if found or not. - public static Keybind TryGetKeybinding(ReferenceHub hub, ServerSpecificSettingBase ss, Menu menu = null) + if ((this.Settings == null || this.Settings.IsEmpty()) && (oSettings == null || oSettings.IsEmpty())) { - int id = ss.SettingId; - if (ss is Keybind) - id -= 10000; + settings.RemoveAt(settings.Count - 1); + return settings; + } - foreach (KeyValuePair> bind in GlobalKeybindingSync.Where(x => x.Key.CheckAccess(hub))) + List sentSettings = new (); + if (this.Settings != null) + { + foreach (ServerSpecificSettingBase t in this.Settings) { - foreach (Keybind key in bind.Value) + sentSettings.Add(t); + if (t is ISetting setting) + { + settings.Add(setting.Base); + } + else { - if (key.SettingId == id) - return key; + settings.Add(t); } } + } - if (menu == null) - return null; - if (!menu.CheckAccess(hub)) - return null; - return menu.Settings.FirstOrDefault(x => x.SettingId == id) as Keybind; - } - - /// - /// Get a menu by . - /// - /// The type - /// if found. - public static Menu GetMenu(Type type) => LoadedMenus.FirstOrDefault(x => x.GetType() == type); - - /// - /// Reload the menu of the specified . - /// - /// The target hub. - public void Reload(ReferenceHub hub) => LoadForPlayer(hub, this); - - /// - /// Reload all that loaded the . - /// - public void ReloadAll() - { - foreach (ReferenceHub hub in MenuSync.Where(x => x.Value == this).Select(x => x.Key).ToList()) - LoadForPlayer(hub, this); - } - - /// - /// Register that will be displayed on the top of all pages. - /// - /// the list of to pin. - public static void RegisterPin(SSTextArea[] toPin) => - Pinned[Assembly.GetCallingAssembly()] = toPin; - - /// - /// Remove pins that was registered by . - /// - public static void UnregisterPin() => Pinned.Remove(Assembly.GetCallingAssembly()); - - /// - /// Called when the is registered. - /// - protected virtual void OnRegistered(){} - - /// - /// Get settings for the specific - /// The target . - /// A list of that will be sent into the player (like but only for the ) - /// - public virtual ServerSpecificSettingBase[] GetSettingsFor(ReferenceHub hub) + if (oSettings != null && !oSettings.IsEmpty()) { - return null; + foreach (ServerSpecificSettingBase t in oSettings) + { + sentSettings.Add(t); + if (t is ISetting setting) + { + settings.Add(setting.Base); + } + else + { + settings.Add(t); + } + } } + + this.SentSettings[hub] = sentSettings.ToArray(); + + return settings; } -} \ No newline at end of file + +#endregion +} +#pragma warning restore SA1018, SA1124, SA1401 diff --git a/Features/Parameters.cs b/Features/Parameters.cs index 1c7904b..e8d9bb5 100644 --- a/Features/Parameters.cs +++ b/Features/Parameters.cs @@ -1,189 +1,206 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features; + using System.Collections.Generic; using System.Linq; + using MEC; -using SSMenuSystem.Features.Interfaces; +using Interfaces; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features +/// +/// Parameters class for getting parameters from . +/// +public static class Parameters { /// - /// Parameters class for getting parameters from . + /// A cache, updated on if hub is inside, when syncing parameters. /// - public static class Parameters + internal static readonly Dictionary> SyncCache = new (); + + /// + /// Get synced parameter value for . + /// + /// The target hub. + /// The id of setting. + /// The Menu to get parameter. + /// The setting type. + /// An instance of That contains synced value, or null if not found. + public static TSs? GetParameter(this ReferenceHub hub, int settingId) + where TMenu : Menu + where TSs : ServerSpecificSettingBase { - /// - /// Get synced parameter value for . - /// - /// The target hub. - /// The id of setting. - /// The Menu to get parameter. - /// The setting type. - /// An instance of That contains synecd value, or null if not found. - public static TSs GetParameter(this ReferenceHub hub, int settingId) - where TMenu : Menu - where TSs : ServerSpecificSettingBase + if (typeof(TSs).BaseType == typeof(ISetting)) { - if (typeof(TSs).BaseType == typeof(ISetting)) - { - Log.Error(nameof(TSs) + " need to be of base type (example: Plaintext became SSPlaintextSetting)."); - return null; - } + Log.Error(nameof(TSs) + " need to be of base type (example: Plaintext became SSPlaintextSetting)."); + return null; + } - foreach (Menu menu in Menu.Menus.Where(x => x is TMenu)) + foreach (Menu menu in Menu.Menus.Where(x => x is TMenu)) + { + if (!menu.SettingsSync.TryGetValue(hub, out List settings)) { - if (!menu.SettingsSync.TryGetValue(hub, out List settings)) - continue; - - ServerSpecificSettingBase t = settings.Where(x => x is TSs).FirstOrDefault(x => x.SettingId == settingId); - return t as TSs; + continue; } - return null; + ServerSpecificSettingBase? t = settings.Where(x => x is TSs).FirstOrDefault(x => x.SettingId == settingId); + return t as TSs; } + return null; + } - /// - /// Sync all paramters for all menus for . - /// - /// The target hub - /// a enumerator for . - internal static IEnumerator SyncAll(ReferenceHub hub) + /// + /// Get ALL synced parameters from ALL menus for . + /// + /// The target hub. + /// all synced parameters from ALL menus for . + public static List GetAllSyncedParameters(ReferenceHub referenceHub) + { + List toReturn = new (); + foreach (Menu menu in Menu.Menus.Where(x => x.InternalSettingsSync.ContainsKey(referenceHub))) { - SyncCache.Add(hub, new List()); - List sendSettings = new(); - float timeout = 0; - List menus = Menu.Menus.ToList(); + toReturn.AddRange(menu.InternalSettingsSync[referenceHub]); + } - foreach (Menu menu in menus) - { - if (!menu.CheckAccess(hub)) - { - Log.Debug(hub.nicknameSync.MyNick + " don't have access to " + menu.Name + ". Skipping."); - continue; - } + return toReturn; + } - Log.Debug($"syncing {hub.nicknameSync.MyNick} registered parameters for menu {menu.Name} {(menu.MenuRelated != null ? $"SubMenu of {Menu.GetMenu(menu.MenuRelated).Name} ({menu.MenuRelated.Name})" : string.Empty)}."); - foreach (ServerSpecificSettingBase t in menu.Settings.Concat(menu.GetSettingsFor(hub))) - { - if (t.ResponseMode != ServerSpecificSettingBase.UserResponseMode.AcquisitionAndChange) - continue; - ServerSpecificSettingBase @base; - if (t is ISetting setting) - @base = setting.Base; - else - @base = t; - - sendSettings.Add(@base); - } + /// + /// Get All synced parameter for target menu. + /// + /// The target hub. + /// The target menu. + /// A list that contains all synced parameters. + public static List GetMenuSpecificSyncedParameters(ReferenceHub referenceHub) + where T : Menu + { + List toReturn = new (); + foreach (Menu menu in Menu.Menus.Where(x => x.InternalSettingsSync.ContainsKey(referenceHub) && x is T)) + { + toReturn.AddRange(menu.InternalSettingsSync[referenceHub]); + } - Utils.SendToPlayer(hub, menu, sendSettings.ToArray()); - const int waitTimeMs = 10; - while (SyncCache[hub].Count < sendSettings.Count && timeout < 10) - { - timeout += waitTimeMs / 1000f; - yield return Timing.WaitForSeconds(10/1000f); - //await Task.Delay(waitTimeMs); - } + return toReturn; + } - if (SyncCache[hub].Count < sendSettings.Count || timeout >= 10) - { - Log.Error( - $"timeout exceeded to sync value for hub {hub.nicknameSync.MyNick} menu {menu.Name}. Stopping the process."); - break; - } + /// + /// Sync all parameters from for . + /// + /// The target hub. + /// The target menu. + /// All parameters to send when ended. + /// An enumerator for . + internal static IEnumerator Sync(ReferenceHub hub, Menu menu, ServerSpecificSettingBase[] toSendWhenEnded) + { + SyncCache.Add(hub, new List()); + List sendSettings = new (); + float timeout = 0; - foreach (ServerSpecificSettingBase setting in SyncCache[hub]) - { - if (sendSettings.Any(s => s.SettingId == setting.SettingId)) - { - ServerSpecificSettingBase set = sendSettings.First(s => s.SettingId == setting.SettingId); - setting.Label = set.Label; - setting.SettingId -= menu.Hash; - setting.HintDescription = set.HintDescription; - } - } + if (!menu.CheckAccess(hub)) + { + Log.Debug(hub.nicknameSync.MyNick + " don't have access to " + menu.Name + ". Skipping."); + yield break; + } - menu.InternalSettingsSync[hub] = new List(SyncCache[hub]); - sendSettings.Clear(); - SyncCache[hub].Clear(); - Log.Debug( - $"synced settings for {hub.nicknameSync.MyNick} to the menu {menu.Name}. {menu.InternalSettingsSync[hub].Count} settings have been synced."); + Log.Debug($"syncing {hub.nicknameSync.MyNick} registered parameters for menu {menu.Name}"); + foreach (ServerSpecificSettingBase t in menu.Settings!) + { + if (t.ResponseMode != ServerSpecificSettingBase.UserResponseMode.AcquisitionAndChange) + { + continue; } - SyncCache.Remove(hub); - Log.Debug("Hub Synced parameters. Stat of his cache: " + - (SyncCache.ContainsKey(hub) ? "active" : "disabled")); - - if (Menu.Menus.Where(x => x.CheckAccess(hub)).IsEmpty()) + ServerSpecificSettingBase @base; + if (t is ISetting setting) + { + @base = setting.Base; + } + else { - Log.Warn("no valid menu found for " + hub.nicknameSync.MyNick + "."); - yield break; + @base = t; } - Menu.LoadForPlayer(hub, null); + sendSettings.Add(@base); } - /// - /// A cache, updated on if hub is inside, when syncing parameters. - /// - internal static readonly Dictionary> SyncCache = new(); - - /// - /// Get ALL synced parameters from ALL menus for - /// - /// The target hub. - /// all synced parameters from ALL menus for . - public static List GetAllSyncedParameters(ReferenceHub referenceHub) + Utils.SendToPlayer(hub, menu, sendSettings.ToArray()); + const int waitTimeMs = 10; + while (SyncCache[hub].Count < sendSettings.Count && timeout < 10) { - List toReturn = new(); - foreach (Menu menu in Menu.Menus.Where(x => x.InternalSettingsSync.ContainsKey(referenceHub))) - toReturn.AddRange(menu.InternalSettingsSync[referenceHub]); - return toReturn; + timeout += waitTimeMs / 1000f; + yield return Timing.WaitForSeconds(10 / 1000f); } - /// - /// Get All synced parameter for target menu. - /// - /// The target hub. - /// The target menu. - /// A list that contains all synced parameters. - public static List GetMenuSpecificSyncedParameters(ReferenceHub referenceHub) where T : Menu + if (SyncCache[hub].Count < sendSettings.Count || timeout >= 10) { - List toReturn = new(); - foreach (Menu menu in Menu.Menus.Where(x => x.InternalSettingsSync.ContainsKey(referenceHub) && x is T)) - toReturn.AddRange(menu.InternalSettingsSync[referenceHub]); - return toReturn; + Log.Error( + $"timeout exceeded to sync value for hub {hub.nicknameSync.MyNick} menu {menu.Name}. Stopping the process."); + yield break; } - /// - /// Sync all parameters from for . - /// - /// The target hub. - /// The target menu. - /// All parameters to send when ended. - /// an enumerator for - internal static IEnumerator Sync(ReferenceHub hub, Menu menu, ServerSpecificSettingBase[] toSendWhenEnded) + foreach (ServerSpecificSettingBase setting in SyncCache[hub]) { - SyncCache.Add(hub, new List()); - List sendSettings = new(); - float timeout = 0; + if (sendSettings.Any(s => s.SettingId == setting.SettingId)) + { + ServerSpecificSettingBase set = sendSettings.First(s => s.SettingId == setting.SettingId); + setting.Label = set.Label; + setting.SettingId -= menu.Hash; + setting.HintDescription = set.HintDescription; + } + } + + menu.InternalSettingsSync[hub] = new List(SyncCache[hub]); + sendSettings.Clear(); + SyncCache.Remove(hub); + Log.Debug( + $"synced settings for {hub.nicknameSync.MyNick} to the menu {menu.Name}. {menu.InternalSettingsSync[hub].Count} settings have been synced."); + Utils.SendToPlayer(hub, menu, toSendWhenEnded); + } + + /// + /// Sync all parameters for all menus for . + /// + /// The target hub. + /// An enumerator for . + internal static IEnumerator SyncAll(ReferenceHub hub) + { + SyncCache.Add(hub, new List()); + List sendSettings = new (); + float timeout = 0; + List menus = Menu.Menus.ToList(); + foreach (Menu menu in menus) + { if (!menu.CheckAccess(hub)) { Log.Debug(hub.nicknameSync.MyNick + " don't have access to " + menu.Name + ". Skipping."); - yield break; + continue; } - Log.Debug($"syncing {hub.nicknameSync.MyNick} registered parameters for menu {menu.Name}"); - foreach (ServerSpecificSettingBase t in menu.Settings) + Log.Debug($"syncing {hub.nicknameSync.MyNick} registered parameters for menu {menu.Name} {(menu.MenuRelated != null ? $"SubMenu of {Menu.GetMenu(menu.MenuRelated)?.Name} ({menu.MenuRelated.Name})" : string.Empty)}."); + foreach (ServerSpecificSettingBase t in menu.Settings!.Concat(menu.GetSettingsFor(hub) !)) { if (t.ResponseMode != ServerSpecificSettingBase.UserResponseMode.AcquisitionAndChange) + { continue; + } + ServerSpecificSettingBase @base; if (t is ISetting setting) + { @base = setting.Base; + } else + { @base = t; + } sendSettings.Add(@base); } @@ -193,14 +210,16 @@ internal static IEnumerator Sync(ReferenceHub hub, Menu menu, ServerSpeci while (SyncCache[hub].Count < sendSettings.Count && timeout < 10) { timeout += waitTimeMs / 1000f; - yield return Timing.WaitForSeconds(10/1000f); + yield return Timing.WaitForSeconds(10 / 1000f); + + // await Task.Delay(waitTimeMs); } if (SyncCache[hub].Count < sendSettings.Count || timeout >= 10) { Log.Error( $"timeout exceeded to sync value for hub {hub.nicknameSync.MyNick} menu {menu.Name}. Stopping the process."); - yield break; + break; } foreach (ServerSpecificSettingBase setting in SyncCache[hub]) @@ -216,10 +235,22 @@ internal static IEnumerator Sync(ReferenceHub hub, Menu menu, ServerSpeci menu.InternalSettingsSync[hub] = new List(SyncCache[hub]); sendSettings.Clear(); - SyncCache.Remove(hub); + SyncCache[hub].Clear(); Log.Debug( $"synced settings for {hub.nicknameSync.MyNick} to the menu {menu.Name}. {menu.InternalSettingsSync[hub].Count} settings have been synced."); - Utils.SendToPlayer(hub, menu, toSendWhenEnded); } + + SyncCache.Remove(hub); + + Log.Debug("Hub Synced parameters. Stat of his cache: " + + (SyncCache.ContainsKey(hub) ? "active" : "disabled")); + + if (Menu.Menus.Where(x => x.CheckAccess(hub)).IsEmpty()) + { + Log.Warn("no valid menu found for " + hub.nicknameSync.MyNick + "."); + yield break; + } + + Menu.LoadForPlayer(hub, null); } -} +} \ No newline at end of file diff --git a/Features/Utils.cs b/Features/Utils.cs index abfab0d..fc8862c 100644 --- a/Features/Utils.cs +++ b/Features/Utils.cs @@ -1,27 +1,54 @@ -using System.Linq; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features; + +using System.Linq; using System.Reflection; + using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features +/// +/// Utility Methods. +/// +internal static class Utils { - internal static class Utils + /// + /// Sends a server specific setting to a player. + /// + /// The players reference hub. + /// The related menu. + /// The collection of server specific settings to send. + /// The version to override. + internal static void SendToPlayer(ReferenceHub hub, Menu? relatedMenu, ServerSpecificSettingBase[] collection, int? versionOverride = null) { - internal static void SendToPlayer(ReferenceHub hub, Menu relatedMenu, ServerSpecificSettingBase[] collection, int? versionOverride = null) + if (relatedMenu != null) { - if (relatedMenu != null) + foreach (ServerSpecificSettingBase c in collection) { - foreach (ServerSpecificSettingBase c in collection) + if (c is SSGroupHeader && c.Label == Plugin.Instance!.Translation.GlobalKeybindingTitle.Label && c.HintDescription == Plugin.Instance.Translation.GlobalKeybindingTitle.Hint) { - if (c is SSGroupHeader && c.Label == Plugin.Instance.Translation.GlobalKeybindingTitle.Label && c.HintDescription == Plugin.Instance.Translation.GlobalKeybindingTitle.Hint) - break; - if (c.SettingId < relatedMenu.Hash) - c.SetId(c.SettingId + relatedMenu.Hash, c.Label); + break; } - } - hub.connectionToClient.Send(new SSSEntriesPack(collection, versionOverride ?? ServerSpecificSettingsSync.Version)); + if (c.SettingId < relatedMenu.Hash) + { + c.SetId(c.SettingId + relatedMenu.Hash, c.Label); + } + } } - internal static AssemblyMenu GetMenu(Assembly assembly) => Menu.Menus.OfType().FirstOrDefault(x => x.Assembly == assembly); + hub.connectionToClient.Send(new SSSEntriesPack(collection, versionOverride ?? ServerSpecificSettingsSync.Version)); } + + /// + /// Gets a menu from an assembly. + /// + /// The assembly to search. + /// The menu if found or null. + internal static AssemblyMenu? GetMenu(Assembly assembly) => Menu.Menus.OfType().FirstOrDefault(x => x.Assembly == assembly); } \ No newline at end of file diff --git a/Features/Wrappers/Button.cs b/Features/Wrappers/Button.cs index fc1628c..f2ca05a 100644 --- a/Features/Wrappers/Button.cs +++ b/Features/Wrappers/Button.cs @@ -1,39 +1,47 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for . This setting make a dropdown where player can select an input. +/// +public class Button : SSButton, ISetting { /// - /// Initialize a new instance of wrapper for . This setting make a dropdown where player can select an input. + /// Initializes a new instance of the class. /// - public class Button : SSButton, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// The text displayed on the button. + /// Triggered when the player press the button. + /// When value is not equal to null, player will need to press specified time in seconds before the click will actually happen. + /// The hint (located in "?"). If null, no hint will be displayed. + public Button(int? id, string label, string buttonText, Action? onClick, float? holdTimeSeconds = null, string? hint = null) + : base(id, label, buttonText, holdTimeSeconds, hint) { - /// - /// The method that will be executed when the index is updated. It's contains two parameters:



- /// - , the player concerned by the press.

- /// - , where it's the class synced. - ///
- public Action Action { get; } + this.Action = onClick; + this.Base = new SSButton(id, label, buttonText, holdTimeSeconds, hint); + } - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; } + /// + /// Gets the method that will be executed when the index is updated. It's contains two parameters:



+ /// - , the player concerned by the press.

+ /// - , where it's the class synced. + ///
+ public Action? Action { get; } - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// The text displayed on the button. - /// Triggered when the player press the button. - /// When value is not equal to null, player will need to press specified time in seconds before the click will actually happen. - /// The hint (located in "?"). If null, no hint will be displayed. - public Button(int? id, string label, string buttonText, Action onClick, float? holdTimeSeconds = null, string hint = null) : base(id, label, buttonText, holdTimeSeconds, hint) - { - Action = onClick; - Base = new SSButton(id, label, buttonText, holdTimeSeconds, hint); - } - } + /// + /// Gets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; } } \ No newline at end of file diff --git a/Features/Wrappers/Dropdown.cs b/Features/Wrappers/Dropdown.cs index 8a188a4..c162391 100644 --- a/Features/Wrappers/Dropdown.cs +++ b/Features/Wrappers/Dropdown.cs @@ -1,44 +1,51 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for . This setting make a dropdown where player can select an input. +/// +public class Dropdown : SSDropdownSetting, ISetting { /// - /// Initialize a new instance of wrapper for . This setting make a dropdown where player can select an input. + /// Initializes a new instance of the class. /// - public class Dropdown : SSDropdownSetting, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// All options available. + /// Triggered when the player selected a new index. + /// Select per default an option, according to . + /// The different available types NW gave. + /// The hint (located in "?"). If null, no hint will be displayed. + public Dropdown(int? id, string label, string[] options, Action? onChanged = null, int defaultOptionIndex = 0, DropdownEntryType entryType = DropdownEntryType.Regular, string? hint = null) + : base(id, label, options, defaultOptionIndex, entryType, hint) { + this.Action = onChanged; + this.Base = new SSDropdownSetting(id, label, options, defaultOptionIndex, entryType, hint); + } - /// - /// The method that will be executed when the index is updated. It's contains four parameters:



- /// - , the player concerned by the change

- /// - , The new selected text, according to the new index.

- /// - , The new selected index.

- /// - , where it's the class synced. - ///
- /// No errors will be thrown if is null. - public Action Action { get; } - - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; set; } + /// + /// Gets the method that will be executed when the index is updated. It's contains four parameters:



+ /// - , the player concerned by the change

+ /// - , The new selected text, according to the new index.

+ /// - , The new selected index.

+ /// - , where it's the class synced. + ///
+ /// No errors will be thrown if is null. + public Action? Action { get; } - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// All options avaiable. - /// Triggered when the player selected a new index. - /// Select per default an option, according to . - /// The different avaiable types NW gave. - /// The hint (located in "?"). If null, no hint will be displayed. - public Dropdown(int? id, string label, string[] options, Action onChanged = null, int defaultOptionIndex = 0, DropdownEntryType entryType = DropdownEntryType.Regular, string hint = null) : base(id, label, options, defaultOptionIndex, entryType, hint) - { - Action = onChanged; - Base = new SSDropdownSetting(id, label, options, defaultOptionIndex, entryType, hint); - } - } + /// + /// Gets or sets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; set; } } \ No newline at end of file diff --git a/Features/Wrappers/Keybind.cs b/Features/Wrappers/Keybind.cs index 7358ac4..3db9c34 100644 --- a/Features/Wrappers/Keybind.cs +++ b/Features/Wrappers/Keybind.cs @@ -1,51 +1,68 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using UnityEngine; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for with the addition (according to menu). This setting can allow player to bind a key (like cmd_bind) but for an action, and not a command. Because of pages, I added paameter, that determinate if the setting should be seen in all pages or not. +/// +public class Keybind : SSKeybindSetting, ISetting { /// - /// Initialize a new instance of wrapper for with the addition (according to menu). This setting can allow player to bind a key (like cmd_bind) but for an action, and not an command. Because of pages, i added paameter, that determinate if the setting will be saw in all pages or not. + /// The increment?. + /// + internal const int Increment = 0; + + /// + /// Initializes a new instance of the class. /// - public class Keybind : SSKeybindSetting, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// Triggered when the player press or release the keybind. + /// The key server will suggest to the client (displayed with a star). Does not mean the default value. + /// If false, the keybind won't work when any gui (like Main menu, Inventory, RA, etc...) is opened. + /// The hint (located in "?"). If null, no hint will be displayed. + /// + public Keybind( + int? id, + string label, + Action? onUsed = null, + KeyCode suggestedKey = KeyCode.None, + bool preventInteractionOnGui = true, + string? hint = null, + bool isGlobal = true) + : base(id + Increment, label, suggestedKey, preventInteractionOnGui, hint) { - internal const int Increment = 0; - - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// Triggered when the player press or release the keybind. - /// The key server will suggest to the client (displayed with a star). Does not mean the default value. - /// If false, the keybind won't work when any gui (like Main menu, Inventory, RA, etc...) is opened. - /// The hint (located in "?"). If null, no hint will be displayed. - /// - public Keybind(int? id, string label, Action onUsed = null, KeyCode suggestedKey = KeyCode.None, bool preventInteractionOnGui = true, - string hint = null, bool isGlobal = true) : base(id+Increment, label, suggestedKey, preventInteractionOnGui, hint) - { - IsGlobal = isGlobal; - Action = onUsed; - Base = new SSKeybindSetting(id + Increment, label, suggestedKey, preventInteractionOnGui, hint); - } - - /// - /// Gets or Sets whether the would be shown and enabled on all pages or not. - /// - public bool IsGlobal { get; } - - /// - /// The method that will be executed when the value is updated. It's contains two parameters:



- /// - , the player concerned by the change

- /// - , True if button is pressed, False if not (triggered every time this value change). - ///
- /// No errors will be thrown if is null. - public Action Action { get; } - - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; } + this.IsGlobal = isGlobal; + this.Action = onUsed; + this.Base = new SSKeybindSetting(id + Increment, label, suggestedKey, preventInteractionOnGui, hint); } + + /// + /// Gets a value indicating whether the would be shown and enabled on all pages or not. + /// + public bool IsGlobal { get; } + + /// + /// Gets the method that will be executed when the value is updated. It's contains two parameters:



+ /// - , the player concerned by the change

+ /// - , True if button is pressed, False if not (triggered every time this value change). + ///
+ /// No errors will be thrown if is null. + public Action? Action { get; } + + /// + /// Gets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; } } \ No newline at end of file diff --git a/Features/Wrappers/Plaintext.cs b/Features/Wrappers/Plaintext.cs index 1ade230..e347db1 100644 --- a/Features/Wrappers/Plaintext.cs +++ b/Features/Wrappers/Plaintext.cs @@ -1,43 +1,51 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using TMPro; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for . This setting make a plaintext where player can put text. When the player leave the plaintext (with "Esc", clicking anywhere or press "Enter"), the value will be updated on the server. +/// +public class Plaintext : SSPlaintextSetting, ISetting { /// - /// Initialize a new instance of wrapper for . This setting make a plaintext where player can put text. When the player leave the plaintext (with "Esc", clicking anywhere or press "Enter"), the value will be updated on the server. + /// Initializes a new instance of the class. /// - public class Plaintext : SSPlaintextSetting, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// Triggered when the player update the value. + /// The placeholder value (if content is empty, a gray placeholder will be shown (if not empty or null). + /// The maximum characters a plaintext can take. + /// The type of content the plaintext can take. + /// The hint (located in "?"). If null, no hint will be displayed. + public Plaintext(int? id, string label, Action? onChanged = null, string placeholder = "...", int characterLimit = 64, TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, string? hint = null) + : base(id, label, placeholder, characterLimit, contentType, hint) { - /// - /// The method that will be executed when the value is updated. It's contains three parameters:



- /// - , the player concerned by the change

- /// - , The new value specified by

- /// - , where it's the class synced. - ///
- /// No errors will be thrown if is null. - public Action OnChanged { get; } + this.Base = new SSPlaintextSetting(id, label, placeholder, characterLimit, contentType, hint); + this.OnChanged = onChanged; + } - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; set; } + /// + /// Gets the method that will be executed when the value is updated. It's contains three parameters:



+ /// - , the player concerned by the change

+ /// - , The new value specified by

+ /// - , where it's the class synced. + ///
+ /// No errors will be thrown if is null. + public Action? OnChanged { get; } - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// Triggered when the player update the value. - /// The placeholder value (if content is empty, a gray placeholder will be shown (if not empty or null). - /// The maximum characters a plaintext can take. - /// The type of content the plaintext can take. - /// The hint (located in "?"). If null, no hint will be displayed. - public Plaintext(int? id, string label, Action onChanged = null, string placeholder = "...", int characterLimit = 64, TMP_InputField.ContentType contentType = TMP_InputField.ContentType.Standard, string hint = null) : base(id, label, placeholder, characterLimit, contentType, hint) - { - Base = new SSPlaintextSetting(id, label, placeholder, characterLimit, contentType, hint); - OnChanged = onChanged; - } - } + /// + /// Gets or sets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; set; } } \ No newline at end of file diff --git a/Features/Wrappers/Slider.cs b/Features/Wrappers/Slider.cs index 74265e2..ffeee00 100644 --- a/Features/Wrappers/Slider.cs +++ b/Features/Wrappers/Slider.cs @@ -1,47 +1,54 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for . This setting make a slider. Every time the value is changed (so even if the player continue clicking on the slider), the value will be updated and triggered. +/// +public class Slider : SSSliderSetting, ISetting { /// - /// Initialize a new instance of wrapper for . This setting make a slider. Every time the value is changed (so even if the player continue clicking on the slider), the value will be updated and triggered. + /// Initializes a new instance of the class. + /// Initialize a new instance of . /// - public class Slider : SSSliderSetting, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// The minimum value can support. Does not change the size. + /// The maximum value can support. Does not change the size. + /// Triggered when the player update the value. + /// The default value of . It is between and . + /// Declare if the slider can only accept integer (so there is no dot or decimal value on the ). + /// The slider contains a little text on the left, that contains the value of the slider. The value returned will be in this format (see :: for more information). + /// The slider contains a little text on the left, what is the format of this ? (Example: if ) is equal to "{0}%", is equal to "0.0", and value is 50.647874, this little plaintext will show "50.7%"). + /// The hint (located in "?"). If null, no hint will be displayed. + public Slider(int? id, string label, float minValue, float maxValue, Action? onChanged = null, float defaultValue = 0, bool integer = false, string valueToStringFormat = "0.##", string finalDisplayFormat = "{0}", string? hint = null) + : base(id, label, minValue, maxValue, defaultValue, integer, valueToStringFormat, finalDisplayFormat, hint) { - /// - /// The method that will be executed when the value is updated. It's contains three parameters:



- /// - , the player concerned by the change

- /// - , The new value specified by

- /// - , where it's the class synced. - ///
- /// No errors will be thrown if is null. - - public Action Action { get; } + this.Base = new SSSliderSetting(id, label, minValue, maxValue, defaultValue, integer, valueToStringFormat, finalDisplayFormat, hint); + this.Action = onChanged; + } - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; set; } + /// + /// Gets the method that will be executed when the value is updated. It's contains three parameters:



+ /// - , the player concerned by the change

+ /// - , The new value specified by

+ /// - , where it's the class synced. + ///
+ /// No errors will be thrown if is null. + public Action? Action { get; } - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// The minimum value can support. Does not change the size. - /// The maximum value can support. Does not change the size. - /// Triggered when the player update the value. - /// The default value of . It is between and - /// Declare if the slider can only accept integer (so there is no dot or decimal value on the . - /// The slider contains a little text on the left, that contains the value of the slider. The value returned will be in this format (see :: for more informations). - /// The slider contains a little text on the left, what is the format of this ? (Example: if ) is equal to "{0}%", is queal to "0.0", and value is 50.647874, this litle plaintext will show "50.7%"). - /// The hint (located in "?"). If null, no hint will be displayed. - public Slider(int? id, string label, float minValue, float maxValue, Action onChanged = null, float defaultValue = 0, bool integer = false, string valueToStringFormat = "0.##", string finalDisplayFormat = "{0}", string hint = null) : base(id, label, minValue, maxValue, defaultValue, integer, valueToStringFormat, finalDisplayFormat, hint) - { - Base = new SSSliderSetting(id, label, minValue, maxValue, defaultValue, integer, valueToStringFormat, finalDisplayFormat, - hint); - Action = onChanged; - } - } + /// + /// Gets or sets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; set; } } \ No newline at end of file diff --git a/Features/Wrappers/YesNoButton.cs b/Features/Wrappers/YesNoButton.cs index 3b109d8..e18503d 100644 --- a/Features/Wrappers/YesNoButton.cs +++ b/Features/Wrappers/YesNoButton.cs @@ -1,42 +1,50 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem.Features.Wrappers; + using System; -using SSMenuSystem.Features.Interfaces; + +using Interfaces; using UserSettings.ServerSpecific; -namespace SSMenuSystem.Features.Wrappers +/// +/// Initialize a new instance of wrapper for . This setting makes two buttons (like a checkbox). You can know with (or ) if the button selected by the client is the first button or the second button. +/// +public class YesNoButton : SSTwoButtonsSetting, ISetting { /// - /// Initialize a new instance of wrapper for . This setting make a two buttons (like a checkbox). You can know with (or ) if the button selectionned by the client is the first button or the second button. + /// Initializes a new instance of the class. /// - public class YesNoButton : SSTwoButtonsSetting, ISetting + /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. + /// The label of . displayed at left in the row. + /// The content of the first button. + /// The content of the second button. + /// Triggered when player changed the button (by clicking on the first or on the second). + /// Is, if the player doesn't have an override value saved, the B is selected by default. + /// The hint (located in "?"). If null, no hint will be displayed. + public YesNoButton(int? id, string label, string optionA, string optionB, Action? onChanged = null, bool defaultIsB = false, string? hint = null) + : base(id, label, optionA, optionB, defaultIsB, hint) { - /// - /// The method that will be executed when the button is updated. It's contains three parameters:



- /// - , the player concerned by the change

- /// - , where the value is basicaly the

- /// - , where it's the class synced. - ///
- /// No errors will be thrown if is null. - public Action Action { get; } + this.Base = new SSTwoButtonsSetting(id, label, optionA, optionB, defaultIsB, hint); + this.Action = onChanged; + } - /// - /// The base instance (sent in to the client). - /// - public ServerSpecificSettingBase Base { get; private set; } + /// + /// Gets the method that will be executed when the button is updated. It's contains three parameters:



+ /// - , the player concerned by the change

+ /// - , where the value is basically the

+ /// - , where it's the class synced. + ///
+ /// No errors will be thrown if is null. + public Action? Action { get; } - /// - /// Initialize a new instance of . - /// - /// The id of . value "null" and is not supported, even if you can set it to null, and it will result of client crash. - /// The label of . displayed at left in the row. - /// The content of the first button. - /// The content of the second button. - /// Triggered when player changed the button (by clicking on the first or on the second). - /// Is, if the player doesn't have an override value saved, the B is selected by default. - /// The hint (located in "?"). If null, no hint will be displayed. - public YesNoButton(int? id, string label, string optionA, string optionB, Action onChanged = null, bool defaultIsB = false, string hint = null) : base(id, label, optionA, optionB, defaultIsB, hint) - { - Base = new SSTwoButtonsSetting(id, label, optionA, optionB, defaultIsB, hint); - Action = onChanged; - } - } + /// + /// Gets the base instance (sent in to the client). + /// + public ServerSpecificSettingBase Base { get; private set; } } \ No newline at end of file diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..8106c86 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Patches/CompatibilityPatches/Compatibility.cs b/Patches/CompatibilityPatches/Compatibility.cs new file mode 100644 index 0000000..bff5bd1 --- /dev/null +++ b/Patches/CompatibilityPatches/Compatibility.cs @@ -0,0 +1,137 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +#pragma warning disable SA1118 +namespace SSMenuSystem.Patches.CompatibilityPatches; + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using Mirror; +using NorthwoodLib.Pools; +using Features; +using UnityEngine; +using UserSettings.ServerSpecific; +using static HarmonyLib.AccessTools; + +using Log = Features.Log; + +/// +/// Patches for compatibility. +/// +[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Setter)] +internal static class Compatibility +{ + private static readonly HashSet LockedAssembly = new (); + + /// + /// Loads the SpecificServerSettings. + /// + /// The settings to load. + internal static void Load(ServerSpecificSettingBase[] settings) + { + if (!Plugin.Instance!.Config.CompatibilitySystem.CompatibilityEnabled) + { + return; + } + + Assembly assembly = Assembly.GetCallingAssembly(); + Log.Debug(assembly.GetName().Name + " tried to set " + nameof(ServerSpecificSettingsSync.DefinedSettings) + ". Game Assembly: " + typeof(ReferenceHub).Assembly.GetName().Name); + if (LockedAssembly.Contains(assembly) || assembly == typeof(ReferenceHub).Assembly) + { + Log.Debug("Assembly is locked or is a part of base game. Skipping..."); + return; + } + + if (Menu.Menus.OfType().Any(x => x.Assembly == assembly)) + { + AssemblyMenu m = Menu.Menus.OfType().First(x => x.Assembly == assembly); + m.OverrideSettings = settings; + if (m.OverrideSettings?.First() is SSGroupHeader) + { + m.Name = m.OverrideSettings.First().Label; + m.OverrideSettings = m.OverrideSettings.Skip(1).ToArray(); + } + + foreach (ReferenceHub hub in ReferenceHub.AllHubs.Where(x => Menu.GetCurrentPlayerMenu(x) == null)) + { + Menu.LoadForPlayer(hub, null); + } + + m.ReloadAll(); + return; + } + + string name = assembly.GetName().Name; + + AssemblyMenu menu = new () + { + Assembly = assembly, + OverrideSettings = settings, + Name = name, + }; + + if (menu.OverrideSettings?.First() is SSGroupHeader) + { + menu.Name = menu.OverrideSettings.First().Label; + menu.OverrideSettings = menu.OverrideSettings.Skip(1).ToArray(); + } + else if (LabApi.Loader.PluginLoader.Plugins.Any(x => x.Value == assembly)) + { + menu.Name = LabApi.Loader.PluginLoader.Plugins.First(x => x.Value == assembly).Key.Name; + } + else if (LabApi.Loader.PluginLoader.Plugins.Any(x => x.Key.Name == "Exiled Loader") && Exiled.Loader.Loader.Plugins.Any(x => x.Assembly == assembly)) + { + menu.Name = Exiled.Loader.Loader.Plugins.First(x => x.Assembly == assembly).Name; + } + + if (Menu.Menus.Any(x => x.Name == menu.Name)) + { + Log.Warn($"Assembly {name} tried to register with the compatibility interface [menu {menu.Name}] but a menu already exists with this name. Using assembly name..."); + menu.Name = name; + } + + if (Menu.Menus.Any(x => x.Name == menu.Name)) + { + Log.Error($"Assembly {name} tried to register with the compatibility interface but a menu was already registered with this name. Aborting."); + LockedAssembly.Add(assembly); + return; + } + + menu.Id = -Mathf.Abs(menu.Name.GetStableHashCode()); + Menu.Register(menu); + foreach (ReferenceHub hub in ReferenceHub.AllHubs.Where(x => Menu.GetCurrentPlayerMenu(x) == null)) + { + Menu.LoadForPlayer(hub, null); + } + } + + // ReSharper disable UnusedMember.Local UnusedParameter.Local + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Shared.Rent(); + newInstructions.InsertRange( + 0, + [ + + // Compatibility.Load(value); + new (OpCodes.Ldarg_0), + new (OpCodes.Call, Method(typeof(Compatibility), nameof(Load))), + new (OpCodes.Ret), + ]); + + foreach (CodeInstruction z in newInstructions) + { + yield return z; + } + + ListPool.Shared.Return(newInstructions); + } +} +#pragma warning restore SA1118 diff --git a/Patches/CompatibilityPatches/CompatibilityGetter.cs b/Patches/CompatibilityPatches/CompatibilityGetter.cs new file mode 100644 index 0000000..4af0a89 --- /dev/null +++ b/Patches/CompatibilityPatches/CompatibilityGetter.cs @@ -0,0 +1,71 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable UnusedParameter.Local UnusedMember.Local +#pragma warning disable SA1010, SA1011, SA1118 // Square brackets should be spaced correctly. Opening Square brackets should not be proceeded with a space. Parameter must not span multiple lines. +namespace SSMenuSystem.Patches.CompatibilityPatches; + +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NorthwoodLib.Pools; +using Features; +using UserSettings.ServerSpecific; +using static HarmonyLib.AccessTools; + +/// +/// A compatibility getter patch. +/// +[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Getter)] +internal static class CompatibilityGetter +{ + /// + /// Gets the Server Specific Setting for an assembly. + /// + /// The assembly to search. + /// The server specific settings for an assembly. + public static ServerSpecificSettingBase[] Get(Assembly assembly) + { + if (assembly == typeof(ReferenceHub).Assembly) + { + return []; + } + + if (Menu.Menus.OfType().All(x => x.Assembly != assembly)) + { + return []; + } + + AssemblyMenu m = Menu.Menus.OfType().First(x => x.Assembly == assembly); + return m.OverrideSettings ?? []; + } + + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Shared.Rent(); + + newInstructions.InsertRange( + 0, + [ + + // CompatibilizerGetter.Get(Assembly.GetCallingAssembly()); + new (OpCodes.Call, Method(typeof(Assembly), nameof(Assembly.GetCallingAssembly))), + new (OpCodes.Call, Method(typeof(CompatibilityGetter), nameof(Get))), + new (OpCodes.Ret), + ]); + + foreach (CodeInstruction z in newInstructions) + { + yield return z; + } + + ListPool.Shared.Return(newInstructions); + } +} +#pragma warning restore SA1010, SA1011, SA1118 diff --git a/Patches/CompatibilityPatches/SendToPlayerDSPatch.cs b/Patches/CompatibilityPatches/SendToPlayerDSPatch.cs new file mode 100644 index 0000000..8d0bd97 --- /dev/null +++ b/Patches/CompatibilityPatches/SendToPlayerDSPatch.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable UnusedParameter.Local UnusedMember.Local +namespace SSMenuSystem.Patches.CompatibilityPatches +{ + using System.Collections.Generic; + using System.Reflection.Emit; + + using HarmonyLib; + using UserSettings.ServerSpecific; + + /// + /// Send to player DS Patch. + /// + [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.SendToPlayer), typeof(ReferenceHub))] + internal static class SendToPlayerDSPatch + { + private static IEnumerable Transpiler(IEnumerable transpiler, ILGenerator generator) + { + yield return new CodeInstruction(OpCodes.Ret); + } + } +} \ No newline at end of file diff --git a/Patchs/CompatibilizerPatchs/SendToPlayerPatch.cs b/Patches/CompatibilityPatches/SendToPlayerPatch.cs similarity index 61% rename from Patchs/CompatibilizerPatchs/SendToPlayerPatch.cs rename to Patches/CompatibilityPatches/SendToPlayerPatch.cs index a958449..96e4332 100644 --- a/Patchs/CompatibilizerPatchs/SendToPlayerPatch.cs +++ b/Patches/CompatibilityPatches/SendToPlayerPatch.cs @@ -1,36 +1,44 @@ -using System; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable UnusedMember.Local UnusedParameter.Local +namespace SSMenuSystem.Patches.CompatibilityPatches; + using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; using NorthwoodLib.Pools; -using SSMenuSystem.Features; +using Features; using UserSettings.ServerSpecific; using static HarmonyLib.AccessTools; -using Log = SSMenuSystem.Features.Log; -namespace SSMenuSystem.Patchs.CompatibilizerPatchs +using Log = Features.Log; + +/// +/// Send to players patch. +/// +[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.SendToPlayer), typeof(ReferenceHub), typeof(ServerSpecificSettingBase[]), typeof(int?))] +internal static class SendToPlayerPatch { - [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.SendToPlayer))] - [HarmonyPatch(new[] { typeof(ReferenceHub), typeof(ServerSpecificSettingBase[]), typeof(int?) })] - internal class SendToPlayerPatch + private static IEnumerable Transpiler(IEnumerable transpiler, ILGenerator generator) { - private static IEnumerable Transpiler(IEnumerable transpiler, - ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(); - - newInstructions.AddRange(new CodeInstruction[] - { - new(OpCodes.Ldarg_0), - new(OpCodes.Ldarg_1), - new(OpCodes.Ldarg_2), - new(OpCodes.Call, Method(typeof(Assembly), nameof(Assembly.GetCallingAssembly))), - new(OpCodes.Call, Method(typeof(SendToPlayerPatch), nameof(SendToPlayer))), - new(OpCodes.Ret) - }); - -#if false + List newInstructions = ListPool.Shared.Rent(); + + newInstructions.AddRange([ + new (OpCodes.Ldarg_0), + new (OpCodes.Ldarg_1), + new (OpCodes.Ldarg_2), + new (OpCodes.Call, Method(typeof(Assembly), nameof(Assembly.GetCallingAssembly))), + new (OpCodes.Call, Method(typeof(SendToPlayerPatch), nameof(SendToPlayer))), + new (OpCodes.Ret), + ]); + + #if false Label continueLabel = generator.DefineLabel(); Label removePlayerLabel = generator.DefineLabel(); @@ -65,7 +73,7 @@ private static IEnumerable Transpiler(IEnumerable Transpiler(IEnumerable), nameof(HashSet.Remove))), new(OpCodes.Ret), }); -#endif + #endif - foreach (CodeInstruction z in newInstructions) - yield return z; - - ListPool.Shared.Return(newInstructions); + foreach (CodeInstruction z in newInstructions) + { + yield return z; } - private static void SendToPlayer(ReferenceHub hub, ServerSpecificSettingBase[] settings, int? versionOverride, Assembly assembly) + ListPool.Shared.Return(newInstructions); + } + + private static void SendToPlayer(ReferenceHub hub, ServerSpecificSettingBase[] settings, int? versionOverride, Assembly assembly) + { + AssemblyMenu? menu = Utils.GetMenu(assembly); + if (menu is null) { - AssemblyMenu menu = Features.Utils.GetMenu(assembly); - if (menu == null) - { - Log.Warn($"assembly {assembly.GetName().Name} tried to send a couple of {settings.Length} settings but doesn't have a valid/registered menu! creating new one..."); - Compatibilizer.Load(Array.Empty()); - menu = Features.Utils.GetMenu(assembly); - } + Log.Warn($"assembly {assembly.GetName().Name} tried to send a couple of {settings.Length} settings but doesn't have a valid/registered menu! creating new one..."); + Compatibility.Load([]); + menu = Utils.GetMenu(assembly); + } - menu.ActuallySendedToClient[hub] = settings; + menu!.ActuallySentToClient[hub] = settings; - if (Menu.GetCurrentPlayerMenu(hub) == menu) - menu.Reload(hub); - else - Menu.LoadForPlayer(hub, null); + if (Menu.GetCurrentPlayerMenu(hub) == menu) + { + menu.Reload(hub); + } + else + { + Menu.LoadForPlayer(hub, null); } } } \ No newline at end of file diff --git a/Patches/CompatibilityPatches/SetIdPatch.cs b/Patches/CompatibilityPatches/SetIdPatch.cs new file mode 100644 index 0000000..6b7ffe1 --- /dev/null +++ b/Patches/CompatibilityPatches/SetIdPatch.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable UnusedMember.Local UnusedParameter.Local +#pragma warning disable SA1010 // Opening square brackets should be spaced correctly +namespace SSMenuSystem.Patches.CompatibilityPatches; + +using System.Collections.Generic; +using System.Reflection.Emit; +using HarmonyLib; +using NorthwoodLib.Pools; +using UnityEngine; +using UserSettings.ServerSpecific; +using static HarmonyLib.AccessTools; + +/// +/// Set ID Patch. +/// +[HarmonyPatch(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.SetId))] +internal static class SetIdPatch +{ + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Shared.Rent(instructions); + + int index = newInstructions.FindIndex(x => x.Is(OpCodes.Call, Method(typeof(Mirror.Extensions), nameof(Mirror.Extensions.GetStableHashCode)))) + 1; + + newInstructions.Insert(index, new CodeInstruction(OpCodes.Call, Method(typeof(Mathf), nameof(Mathf.Abs), [typeof(int)]))); + + foreach (CodeInstruction z in newInstructions) + { + yield return z; + } + + ListPool.Shared.Return(newInstructions); + } +} +#pragma warning restore SA1010 diff --git a/Patches/ExiledPatch.cs b/Patches/ExiledPatch.cs new file mode 100644 index 0000000..69dc278 --- /dev/null +++ b/Patches/ExiledPatch.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +#if EXILED + +// ReSharper disable RedundantAssignment InconsistentNaming UnusedMember.Local UnusedParameter.Local +#pragma warning disable SA1313, CS8625 // Parameter names should begin with lower-case letter. Cannot convert null literal to non-nullable reference type. +namespace SSMenuSystem.Patches; + +using Exiled.API.Features.Core.UserSettings; +using HarmonyLib; + +/// +/// Patch OriginalDefinition from EXILED to avoid NRE. +/// +[HarmonyPatch(typeof(SettingBase), nameof(SettingBase.OriginalDefinition), MethodType.Getter)] +internal static class ExiledPatch +{ + private static bool Prefix(SettingBase __instance, ref SettingBase __result) + { + __result = null; + return false; + } +} + + +#pragma warning restore SA1313, CS8625 // Cannot convert null literal to non-nullable reference type. +#endif diff --git a/Patches/OriginalDefinitionPatch.cs b/Patches/OriginalDefinitionPatch.cs new file mode 100644 index 0000000..9a708e8 --- /dev/null +++ b/Patches/OriginalDefinitionPatch.cs @@ -0,0 +1,76 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable UnusedMember.Local UnusedParameter.Local +namespace SSMenuSystem.Patches; + +using System.Collections.Generic; +using System.Reflection.Emit; + +using HarmonyLib; +using NorthwoodLib.Pools; +using Features; +using Features.Interfaces; +using UserSettings.ServerSpecific; + +using static HarmonyLib.AccessTools; + +/// +/// A patch for OriginalDefinition. +/// +[HarmonyPatch(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.OriginalDefinition), MethodType.Getter)] +internal class OriginalDefinitionPatch +{ + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) + { + List newInstructions = ListPool.Shared.Rent(); + + newInstructions.AddRange([ + new (OpCodes.Ldarg_0), + new (OpCodes.Callvirt, PropertyGetter(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.SettingId))), + new (OpCodes.Call, Method(typeof(OriginalDefinitionPatch), nameof(GetFirstSetting))), + new (OpCodes.Ret), + ]); + + // ReSharper disable once ForCanBeConvertedToForeach + for (int z = 0; z < newInstructions.Count; z++) + { + yield return newInstructions[z]; + } + + ListPool.Shared.Return(newInstructions); + } + + /// + /// Get the first setting corresponding to the . + /// + /// id of . + /// if found, null if not. + private static ServerSpecificSettingBase? GetFirstSetting(int id) + { + foreach (Menu menu in Menu.Menus) + { + foreach (ServerSpecificSettingBase ss in menu.Settings!) + { + int settingId = ss.SettingId + menu.Hash; + if (settingId != id) + { + continue; + } + + if (ss is ISetting setting) + { + return setting.Base; + } + + return ss; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Patches/PrevalidateResponsePatch.cs b/Patches/PrevalidateResponsePatch.cs new file mode 100644 index 0000000..0afcdbd --- /dev/null +++ b/Patches/PrevalidateResponsePatch.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +// ReSharper disable RedundantAssignment InconsistentNaming UnusedMember.Local UnusedParameter.Local +#pragma warning disable SA1313 +namespace SSMenuSystem.Patches; + +using HarmonyLib; +using UserSettings.ServerSpecific; + +/// +/// Patch to avoid checking . +/// +[HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.ServerPrevalidateClientResponse))] +internal static class PrevalidateResponsePatch +{ + private static bool Prefix(SSSClientResponse msg, ref bool __result) + { + __result = true; + return false; + } +} + +#pragma warning restore SA1313 diff --git a/Patchs/CompatibilizerPatchs/Compatibilizer.cs b/Patchs/CompatibilizerPatchs/Compatibilizer.cs deleted file mode 100644 index ef93b7b..0000000 --- a/Patchs/CompatibilizerPatchs/Compatibilizer.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; -using Mirror; -using NorthwoodLib.Pools; -using SSMenuSystem.Features; -using UnityEngine; -using UserSettings.ServerSpecific; -using static HarmonyLib.AccessTools; -using Log = SSMenuSystem.Features.Log; - -namespace SSMenuSystem.Patchs.CompatibilizerPatchs -{ - [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Setter)] - internal static class Compatibilizer - { - private static IEnumerable Transpiler(IEnumerable instructions, - ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(); - newInstructions.InsertRange(0, new CodeInstruction[] - { - // Comptabilisater.Load(value); - new(OpCodes.Ldarg_0), - new(OpCodes.Call, Method(typeof(Compatibilizer), nameof(Load))), - new(OpCodes.Ret), - }); - - foreach (CodeInstruction z in newInstructions) - yield return z; - - ListPool.Shared.Return(newInstructions); - } - - private static readonly HashSet LockedAssembly = new(); - - internal static void Load(ServerSpecificSettingBase[] settings) - { - if (!Plugin.Instance.Config.ComptabilitySystem.ComptabilityEnabled) - return; - Assembly assembly = Assembly.GetCallingAssembly(); - Log.Debug(assembly.GetName().Name + " tried to set " + nameof(ServerSpecificSettingsSync.DefinedSettings) + ". Game Assembly: " + typeof(ReferenceHub).Assembly.GetName().Name); - if (LockedAssembly.Contains(assembly) || assembly == typeof(ReferenceHub).Assembly) - { - Log.Debug("Assembly is locked or is a part of base game. Skipping..."); - return; - } - - if (Menu.Menus.OfType().Any(x => x.Assembly == assembly)) - { - AssemblyMenu m = Menu.Menus.OfType().First(x => x.Assembly == assembly); - m.OverrideSettings = settings; - if (m.OverrideSettings?.First() is SSGroupHeader) - { - m.Name = m.OverrideSettings.First().Label; - m.OverrideSettings = m.OverrideSettings.Skip(1).ToArray(); - } - foreach (ReferenceHub hub in ReferenceHub.AllHubs.Where(x => Menu.GetCurrentPlayerMenu(x) == null)) - Menu.LoadForPlayer(hub, null); - m.ReloadAll(); - return; - } - - string name = assembly.GetName().Name; - - AssemblyMenu menu = new() - { - Assembly = assembly, - OverrideSettings = settings, - Name = name, - }; - - if (menu.OverrideSettings?.First() is SSGroupHeader) - { - menu.Name = menu.OverrideSettings.First().Label; - menu.OverrideSettings = menu.OverrideSettings.Skip(1).ToArray(); - } - else if (LabApi.Loader.PluginLoader.Plugins.Any(x => x.Value == assembly)) - menu.Name = LabApi.Loader.PluginLoader.Plugins.First(x => x.Value == assembly).Key.Name; - else if (LabApi.Loader.PluginLoader.Plugins.Any(x => x.Key.Name == "Exiled Loader") && Exiled.Loader.Loader.Plugins.Any(x => x.Assembly == assembly)) - menu.Name = Exiled.Loader.Loader.Plugins.First(x => x.Assembly == assembly).Name; - - if (Menu.Menus.Any(x => x.Name == menu.Name)) - { - Log.Warn($"assembly {name} tried to register by compatibilisation menu {menu.Name} but a menu already exist with this name. using assembly name..."); - menu.Name = name; - } - - if (Menu.Menus.Any(x => x.Name == menu.Name)) - { - Log.Error($"assembly {name} tried to register by compatibilisation but a menu was already registered with this name. Aborting needed."); - LockedAssembly.Add(assembly); - return; - } - - menu.Id = -Mathf.Abs(menu.Name.GetStableHashCode()); - Menu.Register(menu); - foreach (ReferenceHub hub in ReferenceHub.AllHubs.Where(x => Menu.GetCurrentPlayerMenu(x) == null)) - Menu.LoadForPlayer(hub, null); - } - } -} \ No newline at end of file diff --git a/Patchs/CompatibilizerPatchs/CompatibilizerGetter.cs b/Patchs/CompatibilizerPatchs/CompatibilizerGetter.cs deleted file mode 100644 index ee579ae..0000000 --- a/Patchs/CompatibilizerPatchs/CompatibilizerGetter.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; -using NorthwoodLib.Pools; -using SSMenuSystem.Features; -using UserSettings.ServerSpecific; -using static HarmonyLib.AccessTools; - -namespace SSMenuSystem.Patchs.CompatibilizerPatchs -{ - [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.DefinedSettings), MethodType.Getter)] - internal class CompatibilizerGetter - { - private static IEnumerable Transpiler(IEnumerable instructions, - ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(); - - newInstructions.InsertRange(0, new CodeInstruction[] - { - // CompatibilizerGetter.Get(Assembly.GetCallingAssembly()); - new(OpCodes.Call, Method(typeof(Assembly), nameof(Assembly.GetCallingAssembly))), - new(OpCodes.Call, Method(typeof(CompatibilizerGetter), nameof(Get))), - new(OpCodes.Ret), - }); - - foreach (CodeInstruction z in newInstructions) - yield return z; - - ListPool.Shared.Return(newInstructions); - } - - public static ServerSpecificSettingBase[] Get(Assembly assembly) - { - if (assembly == typeof(ReferenceHub).Assembly) - return Array.Empty(); - if (!Menu.Menus.OfType().Any(x => x.Assembly == assembly)) return null; - AssemblyMenu m = Menu.Menus.OfType().First(x => x.Assembly == assembly); - return m.OverrideSettings; - - } - } -} \ No newline at end of file diff --git a/Patchs/CompatibilizerPatchs/SendToPlayerDSPatch.cs b/Patchs/CompatibilizerPatchs/SendToPlayerDSPatch.cs deleted file mode 100644 index 34e7b04..0000000 --- a/Patchs/CompatibilizerPatchs/SendToPlayerDSPatch.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.Reflection.Emit; -using HarmonyLib; -using UserSettings.ServerSpecific; - -namespace SSMenuSystem.Patchs.CompatibilizerPatchs -{ - - [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.SendToPlayer), new[] { typeof(ReferenceHub) })] - internal class SendToPlayerDSPatch - { - private static IEnumerable Transpiler(IEnumerable transpiler, - ILGenerator generator) - { - yield return new CodeInstruction(OpCodes.Ret); - } - } -} \ No newline at end of file diff --git a/Patchs/CompatibilizerPatchs/SetIdPatch.cs b/Patchs/CompatibilizerPatchs/SetIdPatch.cs deleted file mode 100644 index 0a2a30a..0000000 --- a/Patchs/CompatibilizerPatchs/SetIdPatch.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Reflection.Emit; -using HarmonyLib; -using NorthwoodLib.Pools; -using UnityEngine; -using UserSettings.ServerSpecific; -using static HarmonyLib.AccessTools; - -namespace SSMenuSystem.Patchs.CompatibilizerPatchs -{ - [HarmonyPatch(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.SetId))] - internal class SetIdPatch - { - private static IEnumerable Transpiler(IEnumerable instructions, - ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(instructions); - - int index = newInstructions.FindIndex(x => x.Is(OpCodes.Call, Method(typeof(Mirror.Extensions), nameof(Mirror.Extensions.GetStableHashCode)))) + 1; - - newInstructions.Insert(index, new CodeInstruction(OpCodes.Call, Method(typeof(Mathf), nameof(Mathf.Abs), new[] {typeof(int)}))); - - foreach (CodeInstruction z in newInstructions) - yield return z; - - ListPool.Shared.Return(newInstructions); - } - } -} \ No newline at end of file diff --git a/Patchs/ExiledPatch.cs b/Patchs/ExiledPatch.cs deleted file mode 100644 index 1225207..0000000 --- a/Patchs/ExiledPatch.cs +++ /dev/null @@ -1,22 +0,0 @@ -#if EXILED -using Exiled.API.Features.Core.UserSettings; -using HarmonyLib; -// ReSharper disable RedundantAssignment -// ReSharper disable InconsistentNaming - -namespace SSMenuSystem.Patchs -{ - /// - /// Patch OriginalDefinition from EXILED to avoid NRE. - /// - [HarmonyPatch(typeof(SettingBase), nameof(SettingBase.OriginalDefinition), MethodType.Getter)] - public static class ExiledPatch - { - private static bool Prefix(SettingBase __instance, ref SettingBase __result) - { - __result = null; - return false; - } - } -} -#endif \ No newline at end of file diff --git a/Patchs/OriginalDefinitionPatch.cs b/Patchs/OriginalDefinitionPatch.cs deleted file mode 100644 index e0c6f86..0000000 --- a/Patchs/OriginalDefinitionPatch.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Collections.Generic; -using System.Reflection.Emit; -using HarmonyLib; -using NorthwoodLib.Pools; -using SSMenuSystem.Features; -using SSMenuSystem.Features.Interfaces; -using UserSettings.ServerSpecific; -using static HarmonyLib.AccessTools; - -namespace SSMenuSystem.Patchs -{ - /// - /// A patch for OriginalDefinition. - /// - [HarmonyPatch(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.OriginalDefinition), MethodType.Getter)] - internal class OriginalDefinitionPatch - { - private static IEnumerable Transpiler(IEnumerable instructions, - ILGenerator generator) - { - List newInstructions = ListPool.Shared.Rent(); - - newInstructions.AddRange(new CodeInstruction[] - { - new (OpCodes.Ldarg_0), - new (OpCodes.Callvirt, PropertyGetter(typeof(ServerSpecificSettingBase), nameof(ServerSpecificSettingBase.SettingId))), - new (OpCodes.Call, Method(typeof(OriginalDefinitionPatch), nameof(GetFirstSetting))), - new (OpCodes.Ret), - }); - - for (int z = 0; z < newInstructions.Count; z++) - yield return newInstructions[z]; - ListPool.Shared.Return(newInstructions); - } - - /// - /// Get the first setting correspondig to the . - /// - /// id of . - /// if found, null if not. - private static ServerSpecificSettingBase GetFirstSetting(int id) - { - foreach (Menu menu in Menu.Menus) - { - foreach (ServerSpecificSettingBase ss in menu.Settings) - { - int settingId = ss.SettingId + menu.Hash; - if (settingId != id) continue; - if (ss is ISetting setting) - return setting.Base; - return ss; - } - } - return null; - } - } -} \ No newline at end of file diff --git a/Patchs/PrevalidateResponsePatch.cs b/Patchs/PrevalidateResponsePatch.cs deleted file mode 100644 index 616a4b9..0000000 --- a/Patchs/PrevalidateResponsePatch.cs +++ /dev/null @@ -1,18 +0,0 @@ -using HarmonyLib; -using UserSettings.ServerSpecific; - -namespace SSMenuSystem.Patchs -{ - /// - /// Patch to avoid checking . - /// - [HarmonyPatch(typeof(ServerSpecificSettingsSync), nameof(ServerSpecificSettingsSync.ServerPrevalidateClientResponse))] - internal class PrevalidateResponsePatch - { - private static bool Prefix(SSSClientResponse msg, ref bool __result) - { - __result = true; - return false; - } - } -} \ No newline at end of file diff --git a/Plugin.cs b/Plugin.cs index 60fe0c8..5a850a7 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -1,158 +1,163 @@ -using System.Linq; -using System.Reflection; -using HarmonyLib; -using LabApi.Events.CustomHandlers; -using SSMenuSystem.Features; -using UserSettings.ServerSpecific; -using Log = SSMenuSystem.Features.Log; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem; + using System; -using JetBrains.Annotations; -using LabApi.Loader; #if EXILED using Exiled.API.Features; -#else +#endif +using Features; +using HarmonyLib; +using LabApi.Events.CustomHandlers; +#if LABAPI +using LabApi.Features; +using LabApi.Loader; using LabApi.Loader.Features.Plugins; #endif +using UserSettings.ServerSpecific; +using Log = Features.Log; -namespace SSMenuSystem +/// +/// Load the plugin to send data to player. +/// +// ReSharper disable once ClassNeverInstantiated.Global +#if EXILED +public class Plugin : Plugin +#else +public class Plugin : Plugin +#endif { + private Harmony? harmony; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + private EventHandler handler; +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + /// - /// Load the plugin to send datas to player + /// Gets the instance. can be null if the plugin is not enabled. /// - // ReSharper disable once ClassNeverInstantiated.Global - public class Plugin : Plugin - { - /// - /// Gets the author of the plugin. - /// - public override string Author => "Sky"; + public static Plugin? Instance { get; private set; } - /// - /// Gets the name shown in the Loader. - /// - public override string Name => "SS-Menu System"; + /// + /// Gets the author of the plugin. + /// + public override string Author => "Sky"; - /// - /// Gets the version of the plugin. - /// - public override Version Version => new(2, 0, 7); + /// + /// Gets the name shown in the Loader. + /// + public override string Name => "SS-Menu System"; -#if EXILED - /// - public override Version RequiredExiledVersion => new(9, 5, 0); + /// + /// Gets the version of the plugin. + /// + public override Version Version => new (2, 0, 7); - /// - /// Gets the prefix used for configs. - /// - public override string Prefix => "ss_menu_system"; -#else +#if EXILED + /// + public override Version RequiredExiledVersion => new(9, 5, 0); - /// - /// Gets the plugin translations. - /// - public Translation Translation { get; private set; } + /// + /// Gets the prefix used for configs. + /// + public override string Prefix => "ss_menu_system"; - /// - public override string Description => "Convert all Server-Specifics Settings created by plugins into menu. Help for multi-plugin comptability and organization."; + /// + public override void OnEnabled() + { + GenericEnable(); + base.OnEnabled(); + } - /// - public override Version RequiredApiVersion => new(1, 0, 0); -#endif + /// + public override void OnDisabled() + { + GenericDisable(); + base.OnDisabled(); + } +#else + /// + /// Gets the plugin translations. + /// + public Translation? Translation { get; private set; } - /// - /// Gets the instance. can be null if the plugin is not enabled. - /// - [CanBeNull] - public static Plugin Instance { get; private set; } + /// + public override string Description => "Convert all Server-Specifics Settings created by plugins into menu. Help for multi-plugin comptability and organization."; - private Harmony _harmony; - private EventHandler _handler; + /// + public override Version RequiredApiVersion => LabApiProperties.CurrentVersion; -#if EXILED - /// - public override void OnEnabled() + /// + public override void Enable() + { + if (this.Config == null) { - GenericEnable(); - base.OnEnabled(); + Log.Error("can't load plugin, because Config is malformed/invalid."); + return; } - /// - public override void OnDisabled() + if (!this.Config.IsEnabled) { - GenericDisable(); - base.OnDisabled(); + return; } -#else - - /// - public override void Enable() + this.handler = new EventHandler(); + if (!this.TryLoadConfig("translation.yml", out Translation? translation)) { - if (Config == null) - { - Log.Error("can't load plugin, because Config is malformed/invalid."); - return; - } - - if (!Config.IsEnabled) - return; - - _handler = new EventHandler(); - if (!this.TryLoadConfig("translation.yml", out Translation translation)) - { - Log.Error("There is an error while loading translation. Using default one."); - translation = new(); - } - - Translation = translation; - - CustomHandlersManager.RegisterEventsHandler(_handler); - GenericEnable(); - Log.Info($"{Name}@{Version} has been enabled!"); + Log.Error("There is an error while loading translation. Using default one."); + translation = new (); } - /// - public override void Disable() - { - GenericDisable(); - Log.Info($"{Name}@{Version} has been disabled!"); - } -#endif - private void GenericEnable() - { - Menu.RegisterAll(); - Instance = this; - _harmony = new Harmony("fr.sky.patches"); - _harmony.PatchAll(); - _handler = new EventHandler(); - CustomHandlersManager.RegisterEventsHandler(_handler); - Menu.RegisterQueuedAssemblies(); + this.Translation = translation; + CustomHandlersManager.RegisterEventsHandler(this.handler); -#if DEBUG - Log.Warn("EXPERIMENTAL VERSION IS ACTIVATED. BE AWARD OF BUGS CAN BE DONE. NOT STABLE VERSION."); - Menu.RegisterPin(new[]{new SSTextArea(null, "this pinned content is related to the called assembly\nwith Menu.UnregisterPin() you just unregister ONLY pinned settings by the called assembly.", SSTextArea.FoldoutMode.CollapsedByDefault, "This is a pinned content.")}); - Config!.Debug = true; + this.GenericEnable(); + } + + /// + public override void Disable() + { + this.GenericDisable(); + } #endif - ServerSpecificSettingsSync.ServerOnSettingValueReceived += EventHandler.OnReceivingInput; - } + private void GenericEnable() + { + Menu.RegisterAll(); + Instance = this; + this.harmony = new Harmony("fr.sky.patches"); + this.harmony.PatchAll(); + this.handler = new EventHandler(); + CustomHandlersManager.RegisterEventsHandler(this.handler); + Menu.RegisterQueuedAssemblies(); - private void GenericDisable() - { - Menu.UnregisterAll(); - CustomHandlersManager.UnregisterEventsHandler(_handler); - ServerSpecificSettingsSync.ServerOnSettingValueReceived -= EventHandler.OnReceivingInput; +#if DEBUG + Log.Warn("EXPERIMENTAL VERSION IS ACTIVATED. BE AWARD OF BUGS CAN BE DONE. NOT STABLE VERSION."); + Menu.RegisterPin([new SSTextArea(null, "this pinned content is related to the called assembly\nwith Menu.UnregisterPin() you just unregister ONLY pinned settings by the called assembly.", SSTextArea.FoldoutMode.CollapsedByDefault, "This is a pinned content.")]); + this.Config!.Debug = true; +#endif - Instance = null; - _harmony.UnpatchAll(_harmony.Id); - _harmony = null; - _handler = null; + ServerSpecificSettingsSync.ServerOnSettingValueReceived += EventHandler.OnReceivingInput; + Log.Info($"{this.Name}@{this.Version} has been enabled!"); + } - Log.Info($"{Name}@{Version} has been disabled!"); - } + private void GenericDisable() + { + Menu.UnregisterAll(); + CustomHandlersManager.UnregisterEventsHandler(this.handler); + ServerSpecificSettingsSync.ServerOnSettingValueReceived -= EventHandler.OnReceivingInput; + + Instance = null; + this.harmony?.UnpatchSelf(); + this.harmony = null; +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + this.handler = null; +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + + Log.Info($"{this.Name}@{this.Version} has been disabled!"); } } \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 0fa6149..5f28270 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,35 +1 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SSMenuSystem")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Sky")] -[assembly: AssemblyProduct("SSMenuSystem")] -[assembly: AssemblyCopyright("Copyright © Sky 2024")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("523A1ABE-6297-47F1-8075-32E523CA5FD0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.7")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file + \ No newline at end of file diff --git a/SSMenuSystem.csproj b/SSMenuSystem.csproj index 9e82033..461a376 100644 --- a/SSMenuSystem.csproj +++ b/SSMenuSystem.csproj @@ -1,153 +1,94 @@ - - - - + - Debug - AnyCPU + net481 {523A1ABE-6297-47F1-8075-32E523CA5FD0} - Library - Properties - SSMenuSystem - v4.8 + SSMenuSystem SSMenuSystem - 512 - latest + SSMenuSystem + Copyright © 2025 + true + true + bin\$(Platform)\$(Configuration)\ + embedded + enable + preview + + https://api.nuget.org/v3/index.json; + https://nuget.bepinex.dev/v3/index.json + + AnyCPU + Debug EXILED;Debug LABAPI;Release LABAPI;Release EXILED + $(OutputPath)$(AssemblyName).xml + $(MSBuildThisFileDirectory)SSMenuSystem.ruleset + $(NoWarn),1573,1591,1712 true true - win-x64 + 512 + Properties + false + None - - - SSMenuSystem-EXILED - bin\Release EXILED\ - TRACE;EXILED - true - true - pdbonly - bin\Release EXILED\SSMenuSystem-EXILED.xml - false + + TRACE; DEBUG; EXILED; - - - SSMenuSystem-EXILED - bin\Debug EXILED\ - DEBUG;TRACE;EXILED - true - bin\Debug EXILED\SSMenuSystem-EXILED.xml - 0 + + TRACE; DEBUG; LABAPI; - - - - SSMenuSystem-LABAPI - bin\Release LABAPI\ - TRACE;LABAPI - true - true - pdbonly - bin\Release LABAPI\SSMenuSystem-LABAPI.xml - false + + TRACE; EXILED; - - - SSMenuSystem-LABAPI - bin\Debug LABAPI\ - DEBUG;TRACE;LABAPI - true - bin\Debug LABAPI\SSMenuSystem-LABAPI.xml - 0 + + TRACE; LABAPI; - - - $(EXILED_REFERENCES)\Assembly-CSharp-firstpass.dll - - - $(EXILED_REFERENCES)\Mirror.dll - - - $(EXILED_REFERENCES)\UnityEngine.CoreModule.dll - - - $(EXILED_REFERENCES)\Unity.TextMeshPro.dll - - - - - - - $(EXILED_REFERENCES)\0Harmony.dll - - - $(EXILED_REFERENCES)\Pooling.dll - - - $(EXILED_REFERENCES)\LabApi.dll - - - $(EXILED_REFERENCES)\UnityEngine.PhysicsModule.dll - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/SSMenuSystem.sln.DotSettings b/SSMenuSystem.sln.DotSettings new file mode 100644 index 0000000..a8066b0 --- /dev/null +++ b/SSMenuSystem.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/SSMenuSystem.sln.DotSettings.user b/SSMenuSystem.sln.DotSettings.user index c627ee1..6454f69 100644 --- a/SSMenuSystem.sln.DotSettings.user +++ b/SSMenuSystem.sln.DotSettings.user @@ -26,6 +26,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -41,6 +42,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -129,6 +131,7 @@ ForceIncluded ForceIncluded ForceIncluded + ShowAndRun <AssemblyExplorer> <Assembly Path="/home/sky/Projects/Tools/C/SCPSL plugins/Assembly-CSharp-Publicized.dll" /> <Assembly Path="/home/sky/.nuget/packages/exmod.exiled/9.6.0-beta3/lib/net48/Exiled.Loader.dll" /> diff --git a/Translation.cs b/Translation.cs index c3a4e68..256c11c 100644 --- a/Translation.cs +++ b/Translation.cs @@ -1,65 +1,72 @@ -using System.ComponentModel; -using SSMenuSystem.Configs; +// ----------------------------------------------------------------------- +// +// Copyright (c) Skyfr0676 and Redforce04. All rights reserved. +// Licensed under the Undetermined license. +// +// ----------------------------------------------------------------------- + +namespace SSMenuSystem; + +using System.ComponentModel; + #if EXILED using Exiled.API.Interfaces; #endif +using Configs; -namespace SSMenuSystem +/// +/// The translation configs. +/// +public class Translation +#if EXILED + : ITranslation +#endif { /// - /// The translation configs. + /// Gets or sets the text on the main-menu, button displayed to open a menu where {0} = menu name. /// - public class Translation -#if EXILED - : ITranslation -#endif - { - /// - /// On the main-menu, button displayed to open a menu where {0} = menu name. - /// - [Description("On the main-menu, button displayed to open a menu where {0} = menu name.")] - public LabelButton OpenMenu { get; set; } = new("Open {0}", "Open"); + [Description("On the main-menu, button displayed to open a menu where {0} = menu name.")] + public LabelButton OpenMenu { get; set; } = new ("Open {0}", "Open"); - /// - /// the button displayed when menu is opened. - /// - [Description("the button displayed when menu is opened.")] - public LabelButton ReturnToMenu { get; set; } = new("Return to menu", "Return"); + /// + /// Gets or sets the button displayed when menu is opened. + /// + [Description("the button displayed when menu is opened.")] + public LabelButton ReturnToMenu { get; set; } = new ("Return to menu", "Return"); - /// - /// The button that displayed when sub-menu is opened (return to related menu) where {0} = menu name. - /// - [Description("The button that displayed when sub-menu is opened (return to related menu) where {0} = menu name.")] - public LabelButton ReturnTo { get; set; } = new("Return to {0}", "Return"); + /// + /// Gets or sets the button that displayed when sub-menu is opened (return to related menu) where {0} = menu name. + /// + [Description("The button that displayed when sub-menu is opened (return to related menu) where {0} = menu name.")] + public LabelButton ReturnTo { get; set; } = new ("Return to {0}", "Return"); - /// - /// The reload button. - /// - [Description("The reload button.")] - public LabelButton ReloadButton { get; set; } = new("Reload menus", "Reload"); + /// + /// Gets or sets the reload button. + /// + [Description("The reload button.")] + public LabelButton ReloadButton { get; set; } = new ("Reload menus", "Reload"); - /// - /// The global keybinding header, with label and hint. Disabled temporary. - /// - [Description("The global keybinding header, with label and hint. Disabled temporary.")] - public GroupHeader GlobalKeybindingTitle { get; set; } = new("Global Keybinding", "don't take a look at this (nah seriously it's just to make some keybindings global)"); + /// + /// Gets or sets the global keybinding header, with label and hint. Disabled temporary. + /// + [Description("The global keybinding header, with label and hint. Disabled temporary.")] + public GroupHeader GlobalKeybindingTitle { get; set; } = new ("Global Keybinding", "don't take a look at this (nah seriously it's just to make some keybindings global)"); - /// - /// Text displayed when an error is occured (to avoid client crash + explain why it's don't work). Can accept TextMeshPro tags. - /// - [Description("Text displayed when an error is occured (to avoid client crash + explain why it's don't work). Can accept TextMeshPro tags.")] - public string ServerError { get; set; } = "INTERNAL SERVER ERROR"; + /// + /// Gets or sets the text displayed when an error is occured (to avoid client crash + explain why it didn't work). Can accept TextMeshPro tags. + /// + [Description("Text displayed when an error is occured (to avoid client crash + explain why it's don't work). Can accept TextMeshPro tags.")] + public string ServerError { get; set; } = "INTERNAL SERVER ERROR"; - /// - /// Title of sub-menus when there is one. - /// - [Description("Title of sub-menus when there is one.")] - public GroupHeader SubMenuTitle { get; set; } = new("Sub-Menus", null); + /// + /// Gets or sets the title of sub-menus when there is one. + /// + [Description("Title of sub-menus when there is one.")] + public GroupHeader SubMenuTitle { get; set; } = new ("Sub-Menus", null); - /// - /// Translation when player doesn't have permission to see total errors (= see a part of code name). - /// - [Description("Translation when player doesn't have permission to see total errors (= see a part of code name).")] - public string NoPermission { get; set; } = "insufficient permissions to see the full errors"; - } + /// + /// Gets or sets the Translation when player doesn't have permission to see total errors (= see a part of code name). + /// + [Description("Translation when player doesn't have permission to see total errors (= see a part of code name).")] + public string NoPermission { get; set; } = "insufficient permissions to see the full errors"; } \ No newline at end of file diff --git a/stylecop.json b/stylecop.json index e69de29..4e0011f 100644 --- a/stylecop.json +++ b/stylecop.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Skyfr0676 and Redforce04", + "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license.", + "headerDecoration": "-----------------------------------------------------------------------", + "variables": { + "licenseFile": "LICENSE", + "licenseName": "Undetermined" + } + }, + "indentation": { + "indentationSize": 4, + "useTabs": false + }, + "orderingRules": { + "blankLinesBetweenUsingGroups": "require" + } + } +} \ No newline at end of file