diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs index e9d9b9ad3..7ee6e04ef 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs @@ -9,7 +9,7 @@ namespace SwiftlyS2.Core.Menus; internal sealed class MenuAPI : IMenuAPI, IDisposable { - private IMenuAPI? parent; + private (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) parent; /// /// The menu manager that this menu belongs to. @@ -42,9 +42,9 @@ internal sealed class MenuAPI : IMenuAPI, IDisposable public IMenuBuilderAPI? Builder { get; init; } /// - /// The parent menu in a hierarchical menu structure, or null if this is a top-level menu. + /// The parent hierarchy information in a hierarchical menu structure. /// - public IMenuAPI? Parent { + public (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) Parent { get => parent; internal set { if (parent == value) @@ -52,9 +52,9 @@ internal set { return; } - if (value == null || value == this) + if (value.ParentMenu == this) { - Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"Parent cannot be null or self.", nameof(value))); + Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"Parent cannot be self.", nameof(value))); } else { @@ -318,41 +318,49 @@ MenuOptionScrollStyle.LinearScroll when clampedDesiredIndex < maxVisibleItems - private string BuildMenuHtml( IPlayer player, IReadOnlyList visibleOptions, int arrowPosition, int selectedIndex, int maxOptions, int maxVisibleItems ) { - var titleSection = Configuration.HideTitle - ? string.Empty - : string.Concat( - $"{Configuration.Title}", - maxOptions > maxVisibleItems - ? $" [{selectedIndex + 1}/{maxOptions}]
──────────────────────────
" - : "
──────────────────────────
" - ); - - var menuItems = visibleOptions.Select(( option, index ) => - { - var prefix = index == arrowPosition - ? $"{core.MenusAPI.Configuration.NavigationPrefix} " - : "\u00A0\u00A0\u00A0 "; - return $"{prefix}{option.GetDisplayText(player, 0)}"; - }); + var guideLineColor = Configuration.VisualGuideLineColor ?? "#FFFFFF"; + var navigationColor = Configuration.NavigationMarkerColor ?? "#FFFFFF"; + var footerColor = Configuration.FooterColor ?? "#FF0000"; + var guideLine = $"──────────────────────────"; + + var titleSection = Configuration.HideTitle ? string.Empty : string.Concat( + $"{Configuration.Title}", + maxOptions > maxVisibleItems + ? string.Concat($" [{selectedIndex + 1}/{maxOptions}]
", guideLine, "
") + : string.Concat("
", guideLine, "
") + ); - var footerSection = Configuration.HideFooter ? string.Empty : new Func(() => - { - var isWasd = core.MenusAPI.Configuration.InputMode == "wasd"; - var moveKey = isWasd ? "W/S" : $"{KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()}"; - var useKey = isWasd ? "D" : (KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse).ToUpper(); - var exitKey = isWasd ? "A" : (KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit).ToUpper(); - return string.Concat( - $"
", - $"──────────────────────────", - $"
", - $"Move: {moveKey} | Use: {useKey} | Exit: {exitKey}" - ); - })(); + var menuItems = string.Join("
", visibleOptions.Select(( option, index ) => string.Concat( + index == arrowPosition + ? $"{core.MenusAPI.Configuration.NavigationPrefix} " + : "\u00A0\u00A0\u00A0 ", + option.GetDisplayText(player, 0) + ))); + + var footerSection = Configuration.HideFooter ? string.Empty : + core.MenusAPI.Configuration.InputMode switch { + "wasd" => string.Concat( + "
", guideLine, "
", + "", + $"Move: W/S | ", + $"Use: D | ", + $"Exit: A", + "" + ), + _ => string.Concat( + "
", guideLine, "
", + "", + $"Move: {KeybindOverrides.Move?.ToString() ?? core.MenusAPI.Configuration.ButtonsScroll.ToUpper()}/{KeybindOverrides.MoveBack?.ToString() ?? core.MenusAPI.Configuration.ButtonsScrollBack.ToUpper()} | ", + $"Use: {KeybindOverrides.Select?.ToString() ?? core.MenusAPI.Configuration.ButtonsUse.ToUpper()} | ", + $"Exit: {KeybindOverrides.Exit?.ToString() ?? core.MenusAPI.Configuration.ButtonsExit.ToUpper()}", + "" + ) + }; return string.Concat( titleSection, "", - string.Join("
", menuItems), + menuItems, "
", footerSection ); diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs index 5a2bc016b..855945237 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilderAPI.cs @@ -87,7 +87,7 @@ public IMenuBuilderAPI SetExitButton( KeyBind keyBind ) public IMenuAPI Build() { - var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = parent }; + var menu = new MenuAPI(core, configuration, keybindOverrides, this/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = (parent, null) }; options.ForEach(option => menu.AddOption(option)); return menu; } diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs index 0bc6bfbae..18a3af879 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesignAPI.cs @@ -59,6 +59,78 @@ public IMenuBuilderAPI SetGlobalScrollStyle( MenuOptionScrollStyle style ) return builder; } + public IMenuBuilderAPI SetNavigationMarkerColor( string? hexColor = null ) + { + configuration.NavigationMarkerColor = hexColor; + return builder; + } + + public IMenuBuilderAPI SetNavigationMarkerColor( Shared.Natives.Color color ) + { + configuration.NavigationMarkerColor = color.ToHex(); + return builder; + } + + public IMenuBuilderAPI SetNavigationMarkerColor( System.Drawing.Color color ) + { + configuration.NavigationMarkerColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + return builder; + } + + public IMenuBuilderAPI SetMenuFooterColor( string? hexColor = null ) + { + configuration.FooterColor = hexColor; + return builder; + } + + public IMenuBuilderAPI SetMenuFooterColor( Shared.Natives.Color color ) + { + configuration.FooterColor = color.ToHex(); + return builder; + } + + public IMenuBuilderAPI SetMenuFooterColor( System.Drawing.Color color ) + { + configuration.FooterColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + return builder; + } + + public IMenuBuilderAPI SetVisualGuideLineColor( string? hexColor = null ) + { + configuration.VisualGuideLineColor = hexColor; + return builder; + } + + public IMenuBuilderAPI SetVisualGuideLineColor( Shared.Natives.Color color ) + { + configuration.VisualGuideLineColor = color.ToHex(); + return builder; + } + + public IMenuBuilderAPI SetVisualGuideLineColor( System.Drawing.Color color ) + { + configuration.VisualGuideLineColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + return builder; + } + + public IMenuBuilderAPI SetDisabledColor( string? hexColor = null ) + { + configuration.DisabledColor = hexColor; + return builder; + } + + public IMenuBuilderAPI SetDisabledColor( Shared.Natives.Color color ) + { + configuration.DisabledColor = color.ToHex(); + return builder; + } + + public IMenuBuilderAPI SetDisabledColor( System.Drawing.Color color ) + { + configuration.DisabledColor = $"#{color.R:X2}{color.G:X2}{color.B:X2}"; + return builder; + } + // public IMenuBuilderAPI SetGlobalOptionTextStyle( MenuOptionTextStyle style ) // { // setTextStyle(style); diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs index 9477bea5b..236bcf3ce 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManagerAPI.cs @@ -168,7 +168,7 @@ private void KeyStateChange( IOnClientKeyStateChangedEvent @event ) else if (useKey.HasFlag(@event.Key.ToKeyBind())) { var option = menu.GetCurrentOption(player); - if (option != null && option.Enabled && option.GetEnabled(player)) + if (option != null && option.Enabled && option.GetEnabled(player) && option.IsClickTaskCompleted(player)) { _ = Task.Run(async () => await option.OnClickAsync(player)); @@ -218,7 +218,7 @@ private void KeyStateChange( IOnClientKeyStateChangedEvent @event ) else if (KeyBind.D.HasFlag(@event.Key.ToKeyBind())) { var option = menu.GetCurrentOption(player); - if (option != null && option.Enabled && option.GetEnabled(player)) + if (option != null && option.Enabled && option.GetEnabled(player) && option.IsClickTaskCompleted(player)) { _ = Task.Run(async () => await option.OnClickAsync(player)); @@ -283,7 +283,7 @@ public IMenuAPI CreateMenu( MenuConfiguration configuration, MenuKeybindOverride } } - return new MenuAPI(Core, configuration, keybindOverrides, null/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = parent }; + return new MenuAPI(Core, configuration, keybindOverrides, null/*, parent*/, optionScrollStyle/*, optionTextStyle*/) { Parent = (parent, null) }; } public IMenuAPI? GetCurrentMenu( IPlayer player ) @@ -326,9 +326,9 @@ public void CloseMenuForPlayer( IPlayer player, IMenuAPI menu ) menu.HideForPlayer(player); MenuClosed?.Invoke(this, new MenuManagerEventArgs { Player = player, Menu = menu }); - if (menu.Parent != null) + if (menu.Parent.ParentMenu != null) { - OpenMenuForPlayer(player, menu.Parent); + OpenMenuForPlayer(player, menu.Parent.ParentMenu); } } } @@ -342,7 +342,7 @@ public void CloseAllMenus() { currentMenu.HideForPlayer(kvp.Key); MenuClosed?.Invoke(this, new MenuManagerEventArgs { Player = kvp.Key, Menu = currentMenu }); - currentMenu = currentMenu.Parent; + currentMenu = currentMenu.Parent.ParentMenu; } _ = openMenus.TryRemove(kvp.Key, out _); }); diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ButtonMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ButtonMenuOption.cs index aebb77377..a4b14d9f7 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ButtonMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ButtonMenuOption.cs @@ -5,6 +5,21 @@ namespace SwiftlyS2.Core.Menus.OptionsBase; ///
public sealed class ButtonMenuOption : MenuOptionBase { + /// + /// Creates an instance of with dynamic text updating capabilities. + /// + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// + public ButtonMenuOption( + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) + { + PlaySound = true; + } + /// /// Creates an instance of with dynamic text updating capabilities. /// @@ -14,9 +29,8 @@ public sealed class ButtonMenuOption : MenuOptionBase public ButtonMenuOption( string text, int updateIntervalMs = 120, - int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) + int pauseIntervalMs = 1000 ) : this(updateIntervalMs, pauseIntervalMs) { Text = text; - PlaySound = true; } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ChoiceMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ChoiceMenuOption.cs index e249ffe4a..6fc6190aa 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ChoiceMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ChoiceMenuOption.cs @@ -26,19 +26,19 @@ public sealed class ChoiceMenuOption : MenuOptionBase /// /// Creates an instance of with a list of choices. /// - /// The text content to display. /// The list of available choices. /// The default choice to select. If null or not found, defaults to first choice. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public ChoiceMenuOption( - string text, IEnumerable choices, string? defaultChoice = null, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; PlaySound = true; this.choices = choices.ToList(); @@ -54,6 +54,24 @@ public ChoiceMenuOption( Click += OnChoiceClick; } + /// + /// Creates an instance of with a list of choices. + /// + /// The text content to display. + /// The list of available choices. + /// The default choice to select. If null or not found, defaults to first choice. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public ChoiceMenuOption( + string text, + IEnumerable choices, + string? defaultChoice = null, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(choices, defaultChoice, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + public override string GetDisplayText( IPlayer player, int displayLine = 0 ) { var text = base.GetDisplayText(player, displayLine); diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/InputMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/InputMenuOption.cs index de8b932f0..b5d51224f 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/InputMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/InputMenuOption.cs @@ -28,19 +28,20 @@ public sealed class InputMenuOption : MenuOptionBase /// /// Creates an instance of . /// - /// The text content to display. /// The default input value. Defaults to empty string. /// Maximum input length. Defaults to 16. /// Optional hint message to display when waiting for input. Defaults to English prompt. /// Optional function to validate input. Returns true if valid. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public InputMenuOption( - string text, - string defaultValue = "", int maxLength = 16, - string? hintMessage = null, Func? validator = null, + string defaultValue = "", + string? hintMessage = null, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { @@ -50,17 +51,38 @@ public InputMenuOption( maxLength = 16; } - Text = text; PlaySound = true; - this.defaultValue = defaultValue; this.maxLength = maxLength; - this.hintMessage = hintMessage ?? $"Please type your input (max {maxLength} characters)"; this.validator = validator; + this.defaultValue = defaultValue; + this.hintMessage = hintMessage ?? $"Please type your input (max {maxLength} characters)"; values.Clear(); Click += OnInputClick; } + /// + /// Creates an instance of . + /// + /// The text content to display. + /// Maximum input length. Defaults to 16. + /// Optional function to validate input. Returns true if valid. + /// The default input value. Defaults to empty string. + /// Optional hint message to display when waiting for input. Defaults to English prompt. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public InputMenuOption( + string text, + int maxLength = 16, + Func? validator = null, + string defaultValue = "", + string? hintMessage = null, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(maxLength, validator, defaultValue, hintMessage, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + public override string GetDisplayText( IPlayer player, int displayLine = 0 ) { if (inputStates.TryGetValue(player, out var state)) @@ -70,7 +92,7 @@ public override string GetDisplayText( IPlayer player, int displayLine = 0 ) var text = base.GetDisplayText(player, displayLine); var value = values.GetOrAdd(player, defaultValue); - var displayValue = string.IsNullOrEmpty(value) ? "(empty)" : $"{value}"; + var displayValue = string.IsNullOrEmpty(value) ? $"(empty)" : $"{value}"; return $"{text}: {displayValue}"; } diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs index 5532d7df9..901b7b1a2 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/MenuOptionBase.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Text.RegularExpressions; using SwiftlyS2.Shared.Menus; using SwiftlyS2.Shared.Players; @@ -17,6 +18,9 @@ public abstract partial class MenuOptionBase : IMenuOption, IDisposable private bool visible = true; private bool enabled = true; private readonly DynamicTextUpdater? dynamicTextUpdater; + private readonly ConcurrentDictionary playerVisible = new(); + private readonly ConcurrentDictionary playerEnabled = new(); + private readonly ConcurrentDictionary playerClickTask = new(); private volatile bool disposed; @@ -255,19 +259,46 @@ public MenuOptionTextStyle TextStyle { /// public event EventHandler? AfterFormat; + /// + /// Determines whether the click task for the specified player is completed. + /// + /// The player to check. + /// True if the click task is completed; otherwise, false. + public virtual bool IsClickTaskCompleted( IPlayer player ) => !playerClickTask.TryGetValue(player, out var value) || value.IsCompleted; + /// /// Determines whether this option is visible to the specified player. /// /// The player to check visibility for. /// True if the option is visible to the player; otherwise, false. - public virtual bool GetVisible( IPlayer player ) => Visible; + public virtual bool GetVisible( IPlayer player ) => playerVisible.TryGetValue(player, out var value) ? value : Visible; + + /// + /// Sets the visibility of this option for a specific player. + /// + /// The player to set visibility for. + /// True to make the option visible to the player; false to hide it. + /// + /// The per-player visibility has lower priority than the global property. + /// + public virtual void SetVisible( IPlayer player, bool visible ) => playerVisible.AddOrUpdate(player, visible, ( key, value ) => visible); /// /// Determines whether this option is enabled for the specified player. /// /// The player to check enabled state for. /// True if the option is enabled for the player; otherwise, false. - public virtual bool GetEnabled( IPlayer player ) => Enabled; + public virtual bool GetEnabled( IPlayer player ) => playerEnabled.TryGetValue(player, out var value) ? value : Enabled; + + /// + /// Sets the enabled state of this option for a specific player. + /// + /// The player to set enabled state for. + /// True to enable the option for the player; false to disable it. + /// + /// The per-player enabled state has lower priority than the global property. + /// + public virtual void SetEnabled( IPlayer player, bool enabled ) => playerEnabled.AddOrUpdate(player, enabled, ( key, value ) => enabled); // /// // /// Gets the text to display for this option for the specified player. @@ -335,30 +366,37 @@ public virtual string GetDisplayText( IPlayer player, int displayLine = 0 ) BeforeFormat?.Invoke(this, args); - var displayText = args.CustomText ?? dynamicText ?? Text; - - if (displayLine > 0) + if (playerClickTask.TryGetValue(player, out var value) && !value.IsCompleted) { - var lines = BrTagRegex().Split(displayText); - if (displayLine <= lines.Length) + args.CustomText = "Waiting..."; + } + else + { + var displayText = args.CustomText ?? dynamicText ?? Text; + + if (displayLine > 0) { - displayText = lines[displayLine - 1]; + var lines = BrTagRegex().Split(displayText); + if (displayLine <= lines.Length) + { + displayText = lines[displayLine - 1]; + } } - } - var isEnabled = Enabled && GetEnabled(player); - var sizeClass = TextSize.ToCssClass(); + var isEnabled = Enabled && GetEnabled(player); + var sizeClass = TextSize.ToCssClass(); - if (!isEnabled) - { - displayText = ColorTagRegex().Replace(displayText, string.Empty); - } + if (!isEnabled) + { + displayText = ColorTagRegex().Replace(displayText, string.Empty); + } - var colorStyle = isEnabled ? string.Empty : " color='#666666'"; - var result = $"{displayText}"; - // Console.WriteLine($"displayText: {displayText}"); + var colorStyle = isEnabled ? string.Empty : $" color='{Menu?.Configuration.DisabledColor ?? "#666666"}'"; + var result = $"{displayText}"; + // Console.WriteLine($"displayText: {displayText}"); - args.CustomText = result; + args.CustomText = result; + } AfterFormat?.Invoke(this, args); return args.CustomText; @@ -423,14 +461,14 @@ public virtual ValueTask OnValidatingAsync( IPlayer player ) /// A task that represents the asynchronous operation. public virtual async ValueTask OnClickAsync( IPlayer player ) { - if (!visible || !enabled) + if (!visible || !enabled || !GetVisible(player) || !GetEnabled(player)) { return; } - if (CloseAfterClick) + if (playerClickTask.TryGetValue(player, out var value) && !value.IsCompleted) { - Menu?.MenuManager.CloseMenuForPlayer(player, Menu!); + return; } if (!await OnValidatingAsync(player)) @@ -442,11 +480,25 @@ public virtual async ValueTask OnClickAsync( IPlayer player ) { var args = new MenuOptionClickEventArgs { Player = player, - Option = this, + // Option = this, CloseMenu = CloseAfterClick }; - await Click.Invoke(this, args); + try + { + var clickTask = Click.Invoke(this, args).AsTask(); + _ = playerClickTask.AddOrUpdate(player, clickTask, ( _, _ ) => clickTask); + await clickTask; + } + finally + { + _ = playerClickTask.TryRemove(player, out _); + } + } + + if (CloseAfterClick) + { + Menu?.MenuManager.CloseMenuForPlayer(player, Menu!); } } diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ProgressBarMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ProgressBarMenuOption.cs index 015973459..37547bbb4 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ProgressBarMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ProgressBarMenuOption.cs @@ -29,7 +29,6 @@ public sealed class ProgressBarMenuOption : MenuOptionBase /// /// Creates an instance of . /// - /// The text content to display. /// Function that returns progress value (0.0 to 1.0). /// If true, uses 2 lines; if false, uses 1 line. Defaults to false. /// Whether to show percentage text. Defaults to true. @@ -37,8 +36,10 @@ public sealed class ProgressBarMenuOption : MenuOptionBase /// Character for empty portion. Defaults to "░". /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public ProgressBarMenuOption( - string text, Func progressProvider, bool multiLine = false, bool showPercentage = true, @@ -47,7 +48,6 @@ public ProgressBarMenuOption( int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; PlaySound = false; this.defaultProgressProvider = progressProvider; this.multiLine = multiLine; @@ -59,6 +59,30 @@ public ProgressBarMenuOption( progressProviders.Clear(); } + /// + /// Creates an instance of . + /// + /// The text content to display. + /// Function that returns progress value (0.0 to 1.0). + /// If true, uses 2 lines; if false, uses 1 line. Defaults to false. + /// Whether to show percentage text. Defaults to true. + /// Character for filled portion. Defaults to "█". + /// Character for empty portion. Defaults to "░". + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public ProgressBarMenuOption( + string text, + Func progressProvider, + bool multiLine = false, + bool showPercentage = true, + string filledChar = "█", + string emptyChar = "░", + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(progressProvider, multiLine, showPercentage, filledChar, emptyChar, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + public override string GetDisplayText( IPlayer player, int displayLine = 0 ) { var provider = progressProviders.GetOrAdd(player, defaultProgressProvider); @@ -68,7 +92,7 @@ public override string GetDisplayText( IPlayer player, int displayLine = 0 ) var bar = string.Concat( Enumerable.Range(0, filledCount).Select(_ => $"{filledChar}") - .Concat(Enumerable.Range(0, emptyCount).Select(_ => $"{emptyChar}")) + .Concat(Enumerable.Range(0, emptyCount).Select(_ => $"{emptyChar}")) ); var progressBar = $"({bar}){(ShowPercentage ? $" {(int)(progress * 100)}%" : string.Empty)}"; diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SliderMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SliderMenuOption.cs index 479962f96..346e3e7e3 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SliderMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SliderMenuOption.cs @@ -36,7 +36,6 @@ public sealed class SliderMenuOption : MenuOptionBase /// /// Creates an instance of . /// - /// The text content to display. /// The minimum value. Defaults to 0. /// The maximum value. Defaults to 100. /// The default starting value. Defaults to the minimum value. @@ -44,8 +43,10 @@ public sealed class SliderMenuOption : MenuOptionBase /// The number of visual bars to display. Defaults to 10. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public SliderMenuOption( - string text, float min = 0f, float max = 100f, float? defaultValue = null, @@ -72,7 +73,6 @@ public SliderMenuOption( totalBars = 10; } - Text = text; PlaySound = true; this.Min = min; this.Max = max; @@ -84,6 +84,30 @@ public SliderMenuOption( Click += OnSliderClick; } + /// + /// Creates an instance of . + /// + /// The text content to display. + /// The minimum value. Defaults to 0. + /// The maximum value. Defaults to 100. + /// The default starting value. Defaults to the minimum value. + /// The increment/decrement step. Defaults to 5. + /// The number of visual bars to display. Defaults to 10. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public SliderMenuOption( + string text, + float min = 0f, + float max = 100f, + float? defaultValue = null, + float step = 5f, + int totalBars = 10, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(min, max, defaultValue, step, totalBars, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + public override string GetDisplayText( IPlayer player, int displayLine = 0 ) { var text = base.GetDisplayText(player, displayLine); @@ -92,7 +116,7 @@ public override string GetDisplayText( IPlayer player, int displayLine = 0 ) var filledBars = (int)(percentage * totalBars); var bars = string.Concat( - Enumerable.Range(0, totalBars).Select(i => i < filledBars ? "" : "") + Enumerable.Range(0, totalBars).Select(i => i < filledBars ? "" : $"") ); var slider = $"({bars}) {value:F1}"; diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SubmenuMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SubmenuMenuOption.cs index 8ba669dfa..15c04e2f8 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SubmenuMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/SubmenuMenuOption.cs @@ -10,7 +10,6 @@ namespace SwiftlyS2.Core.Menus.OptionsBase; public sealed class SubmenuMenuOption : MenuOptionBase { private readonly Func>? submenuBuilderAsync; - private readonly ConcurrentDictionary isLoading = new(); // /// // /// Occurs when the submenu is ready to be opened. @@ -20,79 +19,123 @@ public sealed class SubmenuMenuOption : MenuOptionBase /// /// Creates an instance of with a pre-built submenu. /// - /// The text content to display. /// The submenu to open when this option is clicked. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public SubmenuMenuOption( - string text, IMenuAPI submenu, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; PlaySound = true; this.submenuBuilderAsync = () => Task.FromResult(submenu); - Click += OnSubmenuClick; } /// - /// Creates an instance of with a synchronous builder. + /// Creates an instance of with a pre-built submenu. /// /// The text content to display. - /// Function that builds and returns the submenu. + /// The submenu to open when this option is clicked. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. public SubmenuMenuOption( string text, + IMenuAPI submenu, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(submenu, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + + /// + /// Creates an instance of with a synchronous builder. + /// + /// Function that builds and returns the submenu. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// + public SubmenuMenuOption( Func submenuBuilder, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; PlaySound = true; this.submenuBuilderAsync = () => Task.FromResult(submenuBuilder()); - Click += OnSubmenuClick; } /// - /// Creates an instance of with an asynchronous builder. + /// Creates an instance of with a synchronous builder. /// /// The text content to display. - /// Async function that builds and returns the submenu. + /// Function that builds and returns the submenu. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. public SubmenuMenuOption( string text, + Func submenuBuilder, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(submenuBuilder, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + + /// + /// Creates an instance of with an asynchronous builder. + /// + /// Async function that builds and returns the submenu. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// + public SubmenuMenuOption( Func> submenuBuilderAsync, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; PlaySound = true; this.submenuBuilderAsync = submenuBuilderAsync; - Click += OnSubmenuClick; } - public override string GetDisplayText( IPlayer player, int displayLine = 0 ) + /// + /// Creates an instance of with an asynchronous builder. + /// + /// The text content to display. + /// Async function that builds and returns the submenu. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public SubmenuMenuOption( + string text, + Func> submenuBuilderAsync, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(submenuBuilderAsync, updateIntervalMs, pauseIntervalMs) { - return isLoading.TryGetValue(player, out var loading) && loading - ? "Waiting..." - : base.GetDisplayText(player, displayLine); + Text = text; } private async ValueTask OnSubmenuClick( object? sender, MenuOptionClickEventArgs args ) { - var menu = await GetSubmenuAsync(args.Player); - if (menu is not MenuAPI submenu || Menu == null) + if (submenuBuilderAsync == null || Menu == null) + { + return; + } + + var menu = await submenuBuilderAsync.Invoke(); + if (menu is not MenuAPI submenu) { return; } - if (Menu != Menu.MenuManager.GetCurrentMenu(args.Player) || Menu.MenuManager.GetCurrentMenu(args.Player) == null) + var currentMenu = Menu.MenuManager.GetCurrentMenu(args.Player); + if (Menu != currentMenu || currentMenu == null) { return; } @@ -101,26 +144,7 @@ private async ValueTask OnSubmenuClick( object? sender, MenuOptionClickEventArgs // Player = args.Player, // Menu = menu // }); - submenu.Parent = Menu; + submenu.Parent = (Menu, this); Menu.MenuManager.OpenMenuForPlayer(args.Player, submenu); } - - private async Task GetSubmenuAsync( IPlayer player ) - { - if (submenuBuilderAsync != null) - { - _ = isLoading.AddOrUpdate(player, true, ( _, _ ) => true); - - try - { - return await submenuBuilderAsync.Invoke(); - } - finally - { - _ = isLoading.AddOrUpdate(player, false, ( _, _ ) => false); - } - } - - return null; - } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/TextMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/TextMenuOption.cs index 557e063d0..4ad43a3fd 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/TextMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/TextMenuOption.cs @@ -5,6 +5,21 @@ namespace SwiftlyS2.Core.Menus.OptionsBase; /// public sealed class TextMenuOption : MenuOptionBase { + /// + /// Creates an instance of with dynamic text updating capabilities. + /// + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// + public TextMenuOption( + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) + { + PlaySound = false; + } + /// /// Creates an instance of with dynamic text updating capabilities. /// @@ -14,9 +29,8 @@ public sealed class TextMenuOption : MenuOptionBase public TextMenuOption( string text, int updateIntervalMs = 120, - int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) + int pauseIntervalMs = 1000 ) : this(updateIntervalMs, pauseIntervalMs) { Text = text; - PlaySound = false; } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ToggleMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ToggleMenuOption.cs index 94a816db5..773ebd651 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ToggleMenuOption.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/ToggleMenuOption.cs @@ -10,19 +10,36 @@ namespace SwiftlyS2.Core.Menus.OptionsBase; public sealed class ToggleMenuOption : MenuOptionBase { private readonly ConcurrentDictionary toggled = new(); + private readonly bool defaultToggleState; + private readonly string toggleOnSymbol; + private readonly string toggleOffSymbol; + + /// + /// Event triggered when the toggle value changes for a player. + /// + public event EventHandler>? ValueChanged; /// /// Creates an instance of with dynamic text updating capabilities. /// - /// The text content to display. + /// The default toggle state for new players. Defaults to true. + /// The HTML symbol to display when toggle is on. Defaults to green checkmark. + /// The HTML symbol to display when toggle is off. Defaults to red X mark. /// The interval in milliseconds between text updates. Defaults to 120ms. /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + /// + /// When using this constructor, the property must be manually set to specify the initial text. + /// public ToggleMenuOption( - string text, + bool defaultToggleState = true, + string? toggleOnSymbol = null, + string? toggleOffSymbol = null, int updateIntervalMs = 120, int pauseIntervalMs = 1000 ) : base(updateIntervalMs, pauseIntervalMs) { - Text = text; + this.defaultToggleState = defaultToggleState; + this.toggleOnSymbol = toggleOnSymbol ?? "✔"; + this.toggleOffSymbol = toggleOffSymbol ?? "✘"; PlaySound = true; toggled.Clear(); @@ -30,6 +47,26 @@ public ToggleMenuOption( Click += OnToggleClick; } + /// + /// Creates an instance of with dynamic text updating capabilities. + /// + /// The text content to display. + /// The default toggle state for new players. Defaults to true. + /// The HTML symbol to display when toggle is on. Defaults to green checkmark. + /// The HTML symbol to display when toggle is off. Defaults to red X mark. + /// The interval in milliseconds between text updates. Defaults to 120ms. + /// The pause duration in milliseconds before starting the next text update cycle. Defaults to 1000ms. + public ToggleMenuOption( + string text, + bool defaultToggleState = true, + string? toggleOnSymbol = null, + string? toggleOffSymbol = null, + int updateIntervalMs = 120, + int pauseIntervalMs = 1000 ) : this(defaultToggleState, toggleOnSymbol, toggleOffSymbol, updateIntervalMs, pauseIntervalMs) + { + Text = text; + } + /// /// Gets the display text for this option, including the toggle state indicator. /// @@ -39,13 +76,50 @@ public ToggleMenuOption( public override string GetDisplayText( IPlayer player, int displayLine = 0 ) { var text = base.GetDisplayText(player, displayLine); - var isToggled = toggled.GetOrAdd(player, true); - return $"{text} {(isToggled ? "" : "")}"; + var isToggled = toggled.GetOrAdd(player, defaultToggleState); + return $"{text} {(isToggled ? $"{toggleOnSymbol}" : $"{toggleOffSymbol}")}"; + } + + /// + /// Gets the toggle state for the specified player. + /// + /// The player whose toggle state to retrieve. + /// True if toggled on, false if toggled off. Uses the configured default value for new players. + public bool GetToggleState( IPlayer player ) + { + return toggled.GetOrAdd(player, defaultToggleState); + } + + /// + /// Sets the toggle state for the specified player and triggers the value changed event. + /// + /// The player whose toggle state to set. + /// The toggle state to set. + /// True if the value was changed, false if it was already the same value. + public bool SetToggleState( IPlayer player, bool value ) + { + var oldValue = toggled.GetOrAdd(player, defaultToggleState); + + if (oldValue == value) + { + return false; + } + + _ = toggled.AddOrUpdate(player, value, ( _, _ ) => value); + + ValueChanged?.Invoke(this, new MenuOptionValueChangedEventArgs { + Player = player, + Option = this, + OldValue = oldValue, + NewValue = value + }); + + return true; } private ValueTask OnToggleClick( object? sender, MenuOptionClickEventArgs args ) { - _ = toggled.AddOrUpdate(args.Player, true, ( _, current ) => !current); + _ = SetToggleState(args.Player, !GetToggleState(args.Player)); return ValueTask.CompletedTask; } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Helper.cs b/managed/src/SwiftlyS2.Shared/Helper.cs index 220586d60..fd68ebc39 100644 --- a/managed/src/SwiftlyS2.Shared/Helper.cs +++ b/managed/src/SwiftlyS2.Shared/Helper.cs @@ -1,3 +1,4 @@ +using System.Globalization; using SwiftlyS2.Shared.Schemas; namespace SwiftlyS2.Shared; @@ -120,4 +121,50 @@ public static T AsSchema( nint ptr ) where T : ISchemaClass /// The text string to measure. /// The estimated display width in relative units. public static float EstimateTextWidth( string text ) => text.Sum(GetCharWidth); + + /// + /// Parses a hex color string and returns the RGBA color components. + /// + /// The hex color string to parse. + /// Whether alpha component comes first (ARGB format). Default is false (RGBA format). + /// Color components (R, G, B, A) or nulls if invalid. Alpha defaults to 255 for RGB format. + public static (int? r, int? g, int? b, int? a) ParseHexColor( string hexColor, bool alphaFirst = false ) + { + if (string.IsNullOrWhiteSpace(hexColor) || !hexColor.StartsWith('#')) + { + return (null, null, null, null); + } + + var hexPart = hexColor[1..]; + if (hexPart.Length == 0 || !hexPart.All(char.IsAsciiHexDigit)) + { + return (null, null, null, null); + } + + foreach (var groupCount in new[] { 3, 4 }) + { + if (hexPart.Length % groupCount != 0) + { + continue; + } + + var groupSize = hexPart.Length / groupCount; + var groups = Enumerable.Range(0, groupCount) + .Select(i => hexPart.Substring(i * groupSize, groupSize)) + .Select(g => int.TryParse(g, NumberStyles.HexNumber, null, out var v) && v is >= 0 and <= 255 ? v : (int?)null) + .ToArray(); + + if (groups.All(g => g.HasValue)) + { + return groupCount switch { + 3 => (groups[0], groups[1], groups[2], 255), // RGB + 4 when alphaFirst => (groups[1], groups[2], groups[3], groups[0]), // ARGB → RGBA + 4 => (groups[0], groups[1], groups[2], groups[3]), // RGBA + _ => (null, null, null, null) + }; + } + } + + return (null, null, null, null); + } } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/HtmlGradient.cs b/managed/src/SwiftlyS2.Shared/HtmlGradient.cs index cb6c6310a..fed2f4aa3 100644 --- a/managed/src/SwiftlyS2.Shared/HtmlGradient.cs +++ b/managed/src/SwiftlyS2.Shared/HtmlGradient.cs @@ -12,9 +12,9 @@ public static class HtmlGradient /// The starting color in hex format (e.g., "#FF0000"). /// The ending color in hex format (e.g., "#0000FF"). /// HTML string with each character wrapped in a colored font tag. - public static string GenerateGradientText(string text, string startColor, string endColor) + public static string GenerateGradientText( string text, string startColor, string endColor ) { - if (string.IsNullOrEmpty(text)) + if (string.IsNullOrWhiteSpace(text)) { return text; } @@ -23,12 +23,12 @@ public static string GenerateGradientText(string text, string startColor, string var (endR, endG, endB) = ParseHexColor(endColor); var length = text.Length; - return string.Concat(text.Select((ch, i) => + return string.Concat(text.Select(( ch, i ) => { var ratio = length > 1 ? (float)i / (length - 1) : 0f; - var r = (int)(startR + (endR - startR) * ratio); - var g = (int)(startG + (endG - startG) * ratio); - var b = (int)(startB + (endB - startB) * ratio); + var r = (int)(startR + ((endR - startR) * ratio)); + var g = (int)(startG + ((endG - startG) * ratio)); + var b = (int)(startB + ((endB - startB) * ratio)); return $"{ch}"; })); } @@ -39,20 +39,65 @@ public static string GenerateGradientText(string text, string startColor, string /// The text to apply gradient to. /// Array of color stops in hex format (e.g., "#FF0000", "#00FF00", "#0000FF"). /// HTML string with each character wrapped in a colored font tag. - public static string GenerateGradientText(string text, params string[] colors) => (text, colors) switch - { + public static string GenerateGradientText( string text, params string[] colors ) => (text, colors) switch { (null or "", _) => string.Empty, (_, []) => text, (_, [var single]) => $"{text}", _ => GenerateMultiColorGradient(text, colors) }; - private static string GenerateMultiColorGradient(string text, string[] colors) + /// + /// Generates gradient colored text by interpolating between two SwiftlyS2 native colors. + /// + /// The text to apply gradient to. + /// The starting SwiftlyS2 native color. + /// The ending SwiftlyS2 native color. + /// HTML string with each character wrapped in a colored font tag. + public static string GenerateGradientText( string text, Natives.Color startColor, Natives.Color endColor ) + { + return GenerateGradientText(text, startColor.ToHex(), endColor.ToHex()); + } + + /// + /// Generates gradient colored text by interpolating between two System.Drawing colors. + /// + /// The text to apply gradient to. + /// The starting System.Drawing color. + /// The ending System.Drawing color. + /// HTML string with each character wrapped in a colored font tag. + public static string GenerateGradientText( string text, System.Drawing.Color startColor, System.Drawing.Color endColor ) + { + return GenerateGradientText(text, $"#{startColor.R:X2}{startColor.G:X2}{startColor.B:X2}", $"#{endColor.R:X2}{endColor.G:X2}{endColor.B:X2}"); + } + + /// + /// Generates gradient colored text by interpolating across multiple SwiftlyS2 native color stops. + /// + /// The text to apply gradient to. + /// Array of SwiftlyS2 native color stops. + /// HTML string with each character wrapped in a colored font tag. + public static string GenerateGradientText( string text, params Natives.Color[] colors ) + { + return GenerateGradientText(text, colors.Select(c => c.ToHex()).ToArray()); + } + + /// + /// Generates gradient colored text by interpolating across multiple System.Drawing color stops. + /// + /// The text to apply gradient to. + /// Array of System.Drawing color stops. + /// HTML string with each character wrapped in a colored font tag. + public static string GenerateGradientText( string text, params System.Drawing.Color[] colors ) + { + return GenerateGradientText(text, colors.Select(c => $"#{c.R:X2}{c.G:X2}{c.B:X2}").ToArray()); + } + + private static string GenerateMultiColorGradient( string text, string[] colors ) { var parsedColors = colors.Select(ParseHexColor).ToArray(); var length = text.Length; - return string.Concat(text.Select((ch, i) => + return string.Concat(text.Select(( ch, i ) => { var position = length > 1 ? (float)i / (length - 1) : 0f; var segmentIndex = position * (parsedColors.Length - 1); @@ -71,8 +116,6 @@ private static string GenerateMultiColorGradient(string text, string[] colors) })); } - private static (int R, int G, int B) ParseHexColor(string hex) => - hex.TrimStart('#') is { Length: 6 } h - ? (Convert.ToInt32(h[..2], 16), Convert.ToInt32(h[2..4], 16), Convert.ToInt32(h[4..6], 16)) - : (255, 255, 255); + private static (int R, int G, int B) ParseHexColor( string hex ) => + Helper.ParseHexColor(hex) is var (r, g, b, _) ? (r ?? 255, g ?? 255, b ?? 255) : (255, 255, 255); } \ No newline at end of file diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuAPI.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuAPI.cs index 9f73c08fb..1b142987d 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuAPI.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuAPI.cs @@ -1,4 +1,3 @@ -using SwiftlyS2.Core.Menus; using SwiftlyS2.Shared.Players; namespace SwiftlyS2.Shared.Menus; @@ -12,6 +11,12 @@ namespace SwiftlyS2.Shared.Menus; /// public record class MenuConfiguration { + private int maxVisibleItems = -1; + private string? navigationMarkerColor = null; + private string? footerColor = null; + private string? visualGuideLineColor = null; + private string? disabledColor = null; + /// /// The title of the menu. /// @@ -76,13 +81,98 @@ public int MaxVisibleItems { /// public float AutoCloseAfter { get; set; } = 0f; - private int maxVisibleItems = -1; + /// + /// The color of navigation markers (selection indicators, page indicators, etc.) in hex format. + /// + /// + /// Supports "#RGB", "#RGBA", "#RRGGBB", and "#RRGGBBAA" formats. + /// + public string? NavigationMarkerColor { + get => navigationMarkerColor; + set { + if (string.IsNullOrWhiteSpace(value) || Helper.ParseHexColor(value) is not (not null, not null, not null, _)) + { + Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"NavigationMarkerColor: '{value}' is not a valid hex color format. Expected '#RRGGBB'.", nameof(value))); + navigationMarkerColor = null; + } + else + { + navigationMarkerColor = value; + } + } + } + + /// + /// The color of the menu footer in hex format. + /// + /// + /// Supports "#RGB", "#RGBA", "#RRGGBB", and "#RRGGBBAA" formats. + /// + public string? FooterColor { + get => footerColor; + set { + if (string.IsNullOrWhiteSpace(value) || Helper.ParseHexColor(value) is not (not null, not null, not null, _)) + { + Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"FooterColor: '{value}' is not a valid hex color format. Expected '#RRGGBB'.", nameof(value))); + footerColor = null; + } + else + { + footerColor = value; + } + } + } + + /// + /// The color of visual guide lines in hex format. + /// + /// + /// Supports "#RGB", "#RGBA", "#RRGGBB", and "#RRGGBBAA" formats. + /// + public string? VisualGuideLineColor { + get => visualGuideLineColor; + set { + if (string.IsNullOrWhiteSpace(value) || Helper.ParseHexColor(value) is not (not null, not null, not null, _)) + { + Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"VisualGuideLineColor: '{value}' is not a valid hex color format. Expected '#RRGGBB'.", nameof(value))); + visualGuideLineColor = null; + } + else + { + visualGuideLineColor = value; + } + } + } + + /// + /// The color of disabled menu options in hex format. + /// + /// + /// Supports "#RGB", "#RGBA", "#RRGGBB", and "#RRGGBBAA" formats. + /// + public string? DisabledColor { + get => disabledColor; + set { + if (string.IsNullOrWhiteSpace(value) || Helper.ParseHexColor(value) is not (not null, not null, not null, _)) + { + Spectre.Console.AnsiConsole.WriteException(new ArgumentException($"DisabledColor: '{value}' is not a valid hex color format. Expected '#RRGGBB'.", nameof(value))); + disabledColor = null; + } + else + { + disabledColor = value; + } + } + } } /// /// Custom key bindings for menu actions. /// Each property can be set to override the default bindings, or left null to use defaults. /// +/// +/// NOTE: For WASD input mode, any key binding overrides will not take effect. +/// public readonly record struct MenuKeybindOverrides { /// @@ -158,9 +248,13 @@ public interface IMenuAPI : IDisposable public IMenuBuilderAPI? Builder { get; } /// - /// The parent menu in a hierarchical menu structure, or null if this is a top-level menu. + /// The parent hierarchy information in a hierarchical menu structure. /// - public IMenuAPI? Parent { get; } + /// + /// ParentMenu is the parent menu instance, null for top-level menus. + /// TriggerOption is the menu option that triggered this submenu, null for top-level or directly created menus. + /// + public (IMenuAPI? ParentMenu, IMenuOption? TriggerOption) Parent { get; } /// /// Read-only collection of all options in this menu. diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesignAPI.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesignAPI.cs index 57fc66d31..463ec6ac3 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesignAPI.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesignAPI.cs @@ -60,6 +60,90 @@ public interface IMenuDesignAPI /// public IMenuBuilderAPI SetGlobalScrollStyle( MenuOptionScrollStyle style ); + /// + /// Sets the color of the navigation markers using hex color format. + /// + /// The color in hex format (e.g., "#FFFFFF" for white). Pass null to reset to default. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetNavigationMarkerColor( string? hexColor = null ); + + /// + /// Sets the color of the navigation markers using SwiftlyS2 native color. + /// + /// The SwiftlyS2 native color to apply to the navigation markers. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetNavigationMarkerColor( Natives.Color color ); + + /// + /// Sets the color of the navigation markers using System.Drawing color. + /// + /// The System.Drawing color to apply to the navigation markers. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetNavigationMarkerColor( System.Drawing.Color color ); + + /// + /// Sets the color of the menu footer using hex color format. + /// + /// The color in hex format (e.g., "#FF0000" for red). Pass null to reset to default. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetMenuFooterColor( string? hexColor = null ); + + /// + /// Sets the color of the menu footer using SwiftlyS2 native color. + /// + /// The SwiftlyS2 native color to apply to the footer. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetMenuFooterColor( Natives.Color color ); + + /// + /// Sets the color of the menu footer using System.Drawing color. + /// + /// The System.Drawing color to apply to the footer. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetMenuFooterColor( System.Drawing.Color color ); + + /// + /// Sets the color of the visual guide lines using hex color format. + /// + /// The color in hex format (e.g., "#FFFFFF" for white). Pass null to reset to default. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetVisualGuideLineColor( string? hexColor = null ); + + /// + /// Sets the color of the visual guide lines using SwiftlyS2 native color. + /// + /// The SwiftlyS2 native color to apply to the guide lines. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetVisualGuideLineColor( Natives.Color color ); + + /// + /// Sets the color of the visual guide lines using System.Drawing color. + /// + /// The System.Drawing color to apply to the guide lines. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetVisualGuideLineColor( System.Drawing.Color color ); + + /// + /// Sets the color of disabled menu options using hex color format. + /// + /// The color in hex format (e.g., "#808080" for gray). Pass null to reset to default. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetDisabledColor( string? hexColor = null ); + + /// + /// Sets the color of disabled menu options using SwiftlyS2 native color. + /// + /// The SwiftlyS2 native color to apply to disabled options. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetDisabledColor( Natives.Color color ); + + /// + /// Sets the color of disabled menu options using System.Drawing color. + /// + /// The System.Drawing color to apply to disabled options. + /// The menu builder for method chaining. + public IMenuBuilderAPI SetDisabledColor( System.Drawing.Color color ); + // /// // /// Sets the global option text style for the menu. // /// diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuOption.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuOption.cs index 57b64962e..71841a2ff 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuOption.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuOption.cs @@ -80,6 +80,9 @@ public sealed class MenuOptionValidatingEventArgs : EventArgs /// /// Provides event data for menu option click events. /// +/// +/// NOTE: When handling click events, the sender parameter must be passed as IMenuOption. +/// public sealed class MenuOptionClickEventArgs : EventArgs { /// @@ -87,15 +90,15 @@ public sealed class MenuOptionClickEventArgs : EventArgs /// public required IPlayer Player { get; init; } - /// - /// The menu option that was clicked. - /// - public required IMenuOption Option { get; init; } + // /// + // /// The menu option that was clicked. + // /// + // public required IMenuOption Option { get; init; } /// - /// Gets or sets a value indicating whether the menu should be closed after handling the click. + /// Gets a value indicating whether the menu should be closed after handling the click. /// - public bool CloseMenu { get; set; } + public bool CloseMenu { get; internal set; } } /// @@ -238,6 +241,13 @@ public interface IMenuOption : IDisposable /// public event EventHandler? AfterFormat; + /// + /// Determines whether the click task for the specified player is completed. + /// + /// The player to check. + /// True if the click task is completed; otherwise, false. + public bool IsClickTaskCompleted( IPlayer player ); + /// /// Determines whether this option is visible to the specified player. /// @@ -245,6 +255,16 @@ public interface IMenuOption : IDisposable /// True if the option is visible to the player, otherwise, false. public bool GetVisible( IPlayer player ); + /// + /// Sets the visibility of this option for a specific player. + /// + /// The player to set visibility for. + /// True to make the option visible to the player; false to hide it. + /// + /// The per-player visibility has lower priority than the global property. + /// + public void SetVisible( IPlayer player, bool visible ); + /// /// Determines whether this option is enabled for the specified player. /// @@ -252,6 +272,16 @@ public interface IMenuOption : IDisposable /// True if the option is enabled for the player, otherwise, false. public bool GetEnabled( IPlayer player ); + /// + /// Sets the enabled state of this option for a specific player. + /// + /// The player to set enabled state for. + /// True to enable the option for the player; false to disable it. + /// + /// The per-player enabled state has lower priority than the global property. + /// + public void SetEnabled( IPlayer player, bool enabled ); + // /// // /// Gets the text to display for this option for the specified player. // /// diff --git a/managed/src/SwiftlyS2.Shared/Natives/Structs/Color.cs b/managed/src/SwiftlyS2.Shared/Natives/Structs/Color.cs index fbc10ff6a..95f82d67f 100644 --- a/managed/src/SwiftlyS2.Shared/Natives/Structs/Color.cs +++ b/managed/src/SwiftlyS2.Shared/Natives/Structs/Color.cs @@ -1,81 +1,83 @@ -using System.Drawing; using System.Runtime.InteropServices; namespace SwiftlyS2.Shared.Natives; [StructLayout(LayoutKind.Sequential, Size = 4)] -public struct Color : IEquatable { - public byte R; - public byte G; - public byte B; - public byte A; - - public Color(byte r, byte g, byte b, byte a) { - R = r; - G = g; - B = b; - A = a; - } - - public Color(byte r, byte g, byte b) : this(r, g, b, byte.MaxValue) { - } - - public Color(int r, int g, int b) : this((byte)r, (byte)g, (byte)b, byte.MaxValue) { - } - public Color(int r, int g, int b, int a) : this((byte)r, (byte)g, (byte)b, (byte)a) - { - } - public Color(char r, char g, char b, char a) : this((byte)r, (byte)g, (byte)b, (byte)a) { - } - - public Color(char r, char g, char b) : this((byte)r, (byte)g, (byte)b, byte.MaxValue) { - } +public struct Color : IEquatable +{ + public byte R; + public byte G; + public byte B; + public byte A; + + public Color( byte r, byte g, byte b, byte a ) + { + R = r; + G = g; + B = b; + A = a; + } - public static Color FromBuiltin(System.Drawing.Color color) { - return new Color(color.R, color.G, color.B, color.A); - } + public Color( byte r, byte g, byte b ) : this(r, g, b, byte.MaxValue) + { + } - public System.Drawing.Color ToBuiltin() { - return System.Drawing.Color.FromArgb(A, R, G, B); - } + public Color( int r, int g, int b ) : this((byte)r, (byte)g, (byte)b, byte.MaxValue) + { + } + public Color( int r, int g, int b, int a ) : this((byte)r, (byte)g, (byte)b, (byte)a) + { + } + public Color( char r, char g, char b, char a ) : this((byte)r, (byte)g, (byte)b, (byte)a) + { + } - public static Color FromHex(string hex) { - if (hex.StartsWith("#")) { - hex = hex.Substring(1); + public Color( char r, char g, char b ) : this((byte)r, (byte)g, (byte)b, byte.MaxValue) + { } - if (hex.Length == 6) { - return new Color((byte)int.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber), (byte)int.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber), (byte)int.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber), byte.MaxValue); + + public static Color FromInt32( int color ) + { + return new Color((byte)(color >> 24), (byte)(color >> 16), (byte)(color >> 8), (byte)color); } - if (hex.Length == 8) { - return new Color((byte)int.Parse(hex.Substring(0, 2), System.Globalization.NumberStyles.HexNumber), (byte)int.Parse(hex.Substring(2, 2), System.Globalization.NumberStyles.HexNumber), (byte)int.Parse(hex.Substring(4, 2), System.Globalization.NumberStyles.HexNumber), (byte)int.Parse(hex.Substring(6, 2), System.Globalization.NumberStyles.HexNumber)); + + public static Color FromBuiltin( System.Drawing.Color color ) + { + return new Color(color.R, color.G, color.B, color.A); } - throw new ArgumentException("Hex color must be 6 or 8 characters long."); - } - public string ToHex(bool includeAlpha = false) { - return $"#{R:X2}{G:X2}{B:X2}{(includeAlpha ? $"{A:X2}" : "")}"; - } + public static Color FromHex( string hex ) + { + var (r, g, b, a) = Helper.ParseHexColor(hex); + return r == null || g == null || b == null + ? throw new ArgumentException($"Invalid hex color format: '{hex}'. Expected #RGB, #RGBA, #RRGGBB, or #RRGGBBAA.") + : new Color(r.Value, g.Value, b.Value, a ?? 255); + } - public override string ToString() { - return $"Color({R}, {G}, {B}, {A})"; - } + public readonly System.Drawing.Color ToBuiltin() + { + return System.Drawing.Color.FromArgb(A, R, G, B); + } - public int ToInt32() { - return (R << 24) | (G << 16) | (B << 8) | A; - } + public readonly int ToInt32() + { + return (R << 24) | (G << 16) | (B << 8) | A; + } - public static Color FromInt32(int color) { - return new Color((byte)(color >> 24), (byte)(color >> 16), (byte)(color >> 8), (byte)color); - } + public readonly string ToHex( bool includeAlpha = false ) + { + return $"#{R:X2}{G:X2}{B:X2}{(includeAlpha ? $"{A:X2}" : "")}"; + } - public override bool Equals(object? obj) => obj is Color color && this.Equals(color); - public override int GetHashCode() => HashCode.Combine(R, G, B, A); + public readonly bool Equals( Color other ) + { + return R == other.R && G == other.G && B == other.B && A == other.A; + } - public bool Equals(Color other) - { - return R == other.R && G == other.G && B == other.B && A == other.A; - } + public override string ToString() => $"Color({R}, {G}, {B}, {A})"; + public override bool Equals( object? obj ) => obj is Color color && Equals(color); + public override int GetHashCode() => HashCode.Combine(R, G, B, A); - public static bool operator ==(Color left, Color right) => left.Equals(right); - public static bool operator !=(Color left, Color right) => !left.Equals(right); + public static bool operator ==( Color left, Color right ) => left.Equals(right); + public static bool operator !=( Color left, Color right ) => !left.Equals(right); } \ No newline at end of file diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index b81901342..a0b6ce996 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -1,831 +1,878 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Tomlyn.Extensions.Configuration; -using System.Text.RegularExpressions; -using SwiftlyS2.Shared; -using SwiftlyS2.Shared.Commands; -using SwiftlyS2.Shared.GameEventDefinitions; -using SwiftlyS2.Shared.GameEvents; -using SwiftlyS2.Shared.NetMessages; -using SwiftlyS2.Shared.Misc; -using SwiftlyS2.Shared.Natives; -using SwiftlyS2.Shared.Plugins; -using SwiftlyS2.Shared.SchemaDefinitions; -using SwiftlyS2.Shared.ProtobufDefinitions; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; -using SwiftlyS2.Shared.Events; -using SwiftlyS2.Shared.Memory; -using YamlDotNet.Core.Tokens; -using Dapper; -using SwiftlyS2.Shared.Sounds; -using SwiftlyS2.Shared.EntitySystem; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Hosting; -using SwiftlyS2.Shared.Players; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Toolchains.InProcess.NoEmit; -using BenchmarkDotNet.Toolchains.InProcess.Emit; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Loggers; -using SwiftlyS2.Shared.Menus; -using SwiftlyS2.Shared.SteamAPI; -using SwiftlyS2.Core.Menus.OptionsBase; -using System.Collections.Concurrent; -using Dia2Lib; - -namespace TestPlugin; - -public class TestConfig -{ - public string Name { get; set; } - public int Age { get; set; } -} - -public class InProcessConfig : ManualConfig -{ - public InProcessConfig() - { - AddLogger(ConsoleLogger.Default); - AddJob(Job.Default - .WithToolchain(new InProcessNoEmitToolchain(true)) - .WithId("InProcess")); - } -} - -[PluginMetadata(Id = "testplugin", Version = "1.0.0")] -public class TestPlugin : BasePlugin -{ - public TestPlugin( ISwiftlyCore core ) : base(core) - { - Console.WriteLine("[TestPlugin] TestPlugin constructed successfully!"); - // Console.WriteLine($"sizeof(bool): {sizeof(bool)}"); - // Console.WriteLine($"Marshal.SizeOf: {Marshal.SizeOf()}"); - Core.Event.OnWeaponServicesCanUseHook += ( @event ) => - { - // Console.WriteLine($"WeaponServicesCanUse: {@event.Weapon.WeaponBaseVData.AttackMovespeedFactor} {@event.OriginalResult}"); - - }; - } - - [Command("be")] - public void Test2Command( ICommandContext context ) - { - BenchContext.Controller = context.Sender!.RequiredController; - BenchmarkRunner.Run(new InProcessConfig()); - } - - [GameEventHandler(HookMode.Pre)] - public HookResult OnPlayerSpawn( EventPlayerSpawn @event ) - { - if (!@event.UserIdPlayer.IsValid) - { - return HookResult.Continue; - } - var player = @event.UserIdPlayer.RequiredController; - if (player.InGameMoneyServices?.IsValid == true) - { - player.InGameMoneyServices.Account = Core.ConVar.Find("mp_maxmoney")?.Value ?? 16000; - player.InGameMoneyServices.AccountUpdated(); - } - return HookResult.Continue; - } - - public override void Load( bool hotReload ) - { - // Core.Command.HookClientCommand((playerId, commandLine) => - // { - // Console.WriteLine("TestPlugin HookClientCommand " + playerId + " " + commandLine); - // return HookResult.Continue; - // }); - - // Core.Event.OnConsoleOutput += (@event) => - // { - // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); - // }; - - // Core.Event.OnCommandExecuteHook += (@event) => - // { - // if (@event.HookMode == HookMode.Pre) return; - // Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.Command[0], @event.Command.ArgS); - // }; - - // Core.Event.OnEntityStartTouch += (@event) => - // { - // Console.WriteLine($"[New] EntityStartTouch: {@event.Entity.Entity?.DesignerName} -> {@event.OtherEntity.Entity?.DesignerName}"); - // }; - - // Core.Event.OnEntityTouchHook += (@event) => - // { - // switch (@event.TouchType) - // { - // case EntityTouchType.StartTouch: - // Console.WriteLine($"EntityStartTouch: {@event.Entity.Entity?.DesignerName} -> {@event.OtherEntity.Entity?.DesignerName}"); - // break; - // case EntityTouchType.Touch: - // break; - // case EntityTouchType.EndTouch: - // if (@event.Entity.Entity?.DesignerName != "player" || @event.OtherEntity.Entity?.DesignerName != "player") - // { - // return; - // } - // var player = @event.Entity.As(); - // var otherPlayer = @event.OtherEntity.As(); - // Console.WriteLine($"EntityEndTouch: {(player.Controller.Value?.PlayerName ?? string.Empty)} -> {(otherPlayer.Controller.Value?.PlayerName ?? string.Empty)}"); - // break; - // } - // }; - - Core.Engine.ExecuteCommandWithBuffer("@ping", ( buffer ) => - { - Console.WriteLine($"pong: {buffer}"); - }); - - Core.GameEvent.HookPre(@event => - { - @event.LocToken = "test"; - return HookResult.Continue; - }); - - Core.Configuration - .InitializeJsonWithModel("test.jsonc", "Main") - .Configure(( builder ) => - { - builder.AddJsonFile("test.jsonc", optional: false, reloadOnChange: true); - builder.AddTomlFile("test.toml", optional: true, reloadOnChange: true); - }); - - ServiceCollection services = new(); - - services - .AddSwiftly(Core); - - Core.Event.OnPrecacheResource += ( @event ) => - { - @event.AddItem("soundevents/mvp_anthem.vsndevts"); - }; - - Core.Event.OnConVarValueChanged += ( @event ) => - { - Console.WriteLine($"ConVar {@event.ConVarName} changed from {@event.OldValue} to {@event.NewValue} by player {@event.PlayerId}"); - }; - - - // var provider = services.BuildServiceProvider(); - - // provider.GetRequiredService(); - - - // Host.CreateDefaultBuilder() - // .ConfigureLogging((context, logging) => { - // logging.AddConsole(); - // }) - // .ConfigureAppConfiguration((context, config) => { - // config.SetBasePath(Core.Configuration.GetBasePath()); - // config.AddJsonFile("test.jsonc", optional: false, reloadOnChange: true); - // }) - // .ConfigureServices((context, services) => { - // services.AddOptionsWithValidateOnStart>() - // .Bind(context.Configuration.GetSection("Main")); - // }) - // .Build(); - - // This can be used everywhere and the value will be updated when the config is changed - // Console.WriteLine(config.CurrentValue.Age); - - - // var config = new TestConfig(); - - // throw new Exception("TestPlugin loaded"); - - // Core. - - int i = 0; - - // var token2 = Core.Scheduler.Repeat(10, () => { - // Console.WriteLine(Core.Engine.TickCount); - // Console.WriteLine("TestPlugin Timer"); - // }); - Core.Logger.LogInformation("TestPlugin loaded"); - - using var se = new SoundEvent(); - - // var func = Core.Memory.GetUnmanagedFunctionByAddress(Core.Memory.GetAddressBySignature(Library.Server, "AAAAA")!.Value); - - // func.CallOriginal(1, 2); - - // func.Call(1, 2); - - // func.AddHook((next) => { - // return (a, b) => { - // Console.WriteLine("TestPlugin Hook " + a + " " + b); - // next()(a, b); - // }; - // }); - - - // Entrypoint - - // Core.Event.OnTick += () => { - // Console.WriteLine("TestPlugin OnTick "); - // }; - - // Core.Event.OnClientConnected += (@event) => { - // Console.WriteLine("TestPlugin OnClientConnected " + @event.PlayerId); - // }; - - // Core.Event.OnClientPutInServer += (@event) => { - // Console.WriteLine("TestPlugin OnClientPutInServer " + @event.PlayerId); - // }; - - Core.Event.OnClientDisconnected += ( @event ) => - { - Console.WriteLine("TestPlugin OnClientDisconnected " + @event.PlayerId); - }; - Core.Event.OnTick += () => - { - int i = 0; - }; - - // Core.Event.OnClientProcessUsercmds += (@event) => { - // foreach(var usercmd in @event.Usercmds) { - // usercmd.Base.ButtonsPb.Buttonstate1 &= 1UL << (int)GameButtons.Ctrl; - // usercmd.Base.ButtonsPb.Buttonstate2 &= 1UL << (int)GameButtons.Ctrl; - // usercmd.Base.ButtonsPb.Buttonstate3 &= 1UL << (int)GameButtons.Ctrl; - // } - // }; - - // Core.NetMessage.HookClientMessage((msg, id) => { - // Console.WriteLine("TestPlugin OnClientMove "); - // Console.WriteLine(BitConverter.ToString(msg.Data)); - // return HookResult.Continue; - // }); - - // Core.Event.OnEntityTakeDamage += (@event) => { - // Console.WriteLine("TestPlugin OnEntityTakeDamage " + @event.Entity.Entity?.DesignerName + " " + @event.Info.HitGroupId); - // }; - - // Core.Event.OnTick += () => { - - // Console.WriteLine("TestPlugin OnTick"); - // }; - - // Core.Event.OnEntityCreated += (ev) => { - // var entity = ev.Entity; - // entity.Entity.DesignerName = "a"; - // Console.WriteLine("TestPlugin OnEntityCreated " + ev.Entity.Entity?.DesignerName); - // }; - - using CEntityKeyValues kv = new(); - - kv.SetBool("test", true); - - Console.WriteLine(kv.Get("test2")); - - CUtlStringToken token = new("hello"); - Console.WriteLine($"2"); - } - - CEntityKeyValues kv { get; set; } - CEntityInstance entity { get; set; } - - [Command("tt")] - public void TestCommand( ICommandContext context ) - { - // token2?.Cancel(); - // kv = new(); - // kv.SetString("test", "SAFE"); - - // _Core.Logger.LogInformation("!@#"); - - // _Core.Logger.LogInformation(_Core.GameData.GetSignature("CEntityInstance::AcceptInput").ToString()); - - // entity = _Core.EntitySystem.CreateEntityByDesignerName("point_worldtext"); - // entity.DispatchSpawn(kv); - // Console.WriteLine("Spawned entity with keyvalues"); - - int j = 0; - - var cvar = Core.ConVar.Find("sv_cheats")!; - Console.WriteLine(cvar); - Console.WriteLine(cvar.Value); - var cvar2 = Core.ConVar.Find("sv_autobunnyhopping")!; - Console.WriteLine(cvar2); - Console.WriteLine(cvar2.Value); - - var cvar3 = Core.ConVar.Create("sw_test_cvar", "Test cvar", "ABCDEFG"); - Console.WriteLine(cvar3); - Console.WriteLine(cvar3.Value); - - var cvar4 = Core.ConVar.Find("r_drawworld")!; - - cvar2.ReplicateToClient(0, true); - - cvar4.QueryClient(0, ( value ) => - { - Console.WriteLine("QueryCallback " + value); - }); - } - - [Command("w")] - public void TestCommand1( ICommandContext context ) - { - var ret = SteamGameServerUGC.DownloadItem(new PublishedFileId_t(3596198331), true); - Console.WriteLine(SteamGameServer.GetPublicIP().ToIPAddress()); - - - } - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - delegate nint DispatchSpawnDelegate( nint pEntity, nint pKV ); - int order = 0; - - IUnmanagedFunction? _dispatchspawn; - - [Command("h1")] - public void TestCommand2( ICommandContext context ) - { - // var token = Core.Scheduler.DelayAndRepeat(500, 1000, () => - // { - - // }); - - var addres = Core.GameData.GetSignature("CBaseEntity::DispatchSpawn"); - var func = Core.Memory.GetUnmanagedFunctionByAddress(addres); - - var guid = func.AddHook(( next ) => - { - return ( pEntity, pKV ) => - { - Console.WriteLine("TestPlugin DispatchSpawn " + order++); - return next()(pEntity, pKV); - }; - }); - - _dispatchspawn.AddHook(( next ) => - { - return ( pEntity, pKV ) => - { - Console.WriteLine("TestPlugin DispatchSpawn2 " + order++); - return next()(pEntity, pKV); - }; - }); - - } - - [EventListener] - public void OnEntityCreated( IOnEntityCreatedEvent @event ) - { - // @event.Entity.Entity.DesignerName = "abc"; - Console.WriteLine("TestPlugin OnEntityCreated222 " + @event.Entity.Entity?.DesignerName); - } - - Guid _hookId = Guid.Empty; - - [Command("bad")] - public void TestCommandBad( ICommandContext context ) - { - try - { - var isValveDS = Core.EntitySystem.GetGameRules()!.IsValveDS; - } - catch (Exception e) - { - Core.Logger.LogWarning("{Exception}", e.Message); - } - - try - { - Core.EntitySystem.GetGameRules()!.IsValveDS = true; - } - catch (Exception e) - { - Core.Logger.LogWarning("{Exception}", e.Message); - } - - try - { - Core.EntitySystem.GetGameRules()!.IsValveDSUpdated(); - } - catch (Exception e) - { - Core.Logger.LogWarning("{Exception}", e.Message); - } - } - - [Command("h2")] - public void TestCommand3( ICommandContext context ) - { - var ent = Core.EntitySystem.CreateEntity(); - ent.DispatchSpawn(); - ent.Collision.MaxsUpdated(); - ent.Collision.CollisionAttribute.OwnerIdUpdated(); - } - - [Command("tt3")] - public void TestCommand33( ICommandContext context ) - { - var ent = Core.EntitySystem.CreateEntity(); - using CEntityKeyValues kv = new(); - kv.Set("m_spawnflags", 256); - ent.DispatchSpawn(kv); - ent.SetModel("weapons/models/grenade/incendiary/weapon_incendiarygrenade.vmdl"); - ent.Teleport(new Vector(context.Sender!.PlayerPawn!.AbsOrigin!.Value.X + 50, context.Sender!.PlayerPawn!.AbsOrigin!.Value.Y + 50, context.Sender!.PlayerPawn!.AbsOrigin!.Value.Z + 30), QAngle.Zero, Vector.Zero); - } - - [Command("tt4")] - public void TestCommand4( ICommandContext context ) - { - Console.WriteLine(Core.Permission.PlayerHasPermission(7656, context.Args[0])); - } - - [Command("tt5")] - public void TestCommand5( ICommandContext context ) - { - Console.WriteLine("TestPlugin TestCommand5"); - } - - [Command("tt6", permission: "tt6")] - public void TestCommand6( ICommandContext context ) - { - Console.WriteLine("TestPlugin TestCommand6"); - } - - [Command("tt99")] - public void TestCommand99( ICommandContext context ) - { - Console.WriteLine(Core.Engine.ServerIP); - } - - [Command("tt7")] - public void TestCommand7( ICommandContext context ) - { - Core.Engine.ExecuteCommandWithBuffer("@ping", ( buffer ) => - { - Console.WriteLine($"pong: {buffer}"); - }); - } - - [Command("tt8")] - public unsafe void TestCommand8( ICommandContext context ) - { - Core.EntitySystem.GetAllEntitiesByDesignerName("func_buyzone").ToList().ForEach(zone => - { - if ((zone?.IsValid ?? false)) - { - zone.Despawn(); - } - }); - - var sender = context.Sender!; - var target = Core.PlayerManager.GetAllPlayers().FirstOrDefault(p => p.PlayerID != sender.PlayerID)!; - - var origin = sender.RequiredPlayerPawn.AbsOrigin ?? Vector.Zero; - var targetOrigin = target.RequiredPlayerPawn.AbsOrigin ?? Vector.Zero; - - Console.WriteLine("\n"); - Console.WriteLine($"Origin: {origin}"); - Console.WriteLine($"Target Origin: {targetOrigin}"); - - // Ray_t* ray = stackalloc Ray_t[1]; - // ray->Init(Vector.Zero, Vector.Zero); - Ray_t ray = new(); - ray.Init(Vector.Zero, Vector.Zero); - - var filter = new CTraceFilter { - // unk01 = 1, - IterateEntities = true, - QueryShapeAttributes = new RnQueryShapeAttr_t { - InteractsWith = MaskTrace.Player | MaskTrace.Solid | MaskTrace.Hitbox | MaskTrace.Npc, - InteractsExclude = MaskTrace.Empty, - InteractsAs = MaskTrace.Player, - CollisionGroup = CollisionGroup.PlayerMovement, - ObjectSetMask = RnQueryObjectSet.AllGameEntities, - HitSolid = true, - // HitTrigger = false, - // HitSolidRequiresGenerateContacts = false, - // ShouldIgnoreDisabledPairs = true, - // IgnoreIfBothInteractWithHitboxes = true, - // ForceHitEverything = true - } - }; - - // filter.QueryShapeAttributes.EntityIdsToIgnore[0] = unchecked((uint)-1); - // filter.QueryShapeAttributes.EntityIdsToIgnore[1] = unchecked((uint)-1); - // filter.QueryShapeAttributes.OwnerIdsToIgnore[0] = unchecked((uint)-1); - // filter.QueryShapeAttributes.OwnerIdsToIgnore[1] = unchecked((uint)-1); - // filter.QueryShapeAttributes.HierarchyIds[0] = 0; - // filter.QueryShapeAttributes.HierarchyIds[1] = 0; - - var trace = new CGameTrace(); - Core.Trace.TraceShape(origin, targetOrigin, ray, filter, ref trace); - - Console.WriteLine(trace.pEntity != null ? $"! Hit Entity: {trace.Entity.DesignerName}" : "! No entity hit"); - Console.WriteLine($"! SurfaceProperties: {(nint)trace.SurfaceProperties}, pEntity: {(nint)trace.pEntity}, HitBox: {(nint)trace.HitBox}({trace.HitBox->m_name.Value}), Body: {(nint)trace.Body}, Shape: {(nint)trace.Shape}, Contents: {trace.Contents}"); - Console.WriteLine($"! StartPos: {trace.StartPos}, EndPos: {trace.EndPos}, HitNormal: {trace.HitNormal}, HitPoint: {trace.HitPoint}"); - Console.WriteLine($"! HitOffset: {trace.HitOffset}, Fraction: {trace.Fraction}, Triangle: {trace.Triangle}, HitboxBoneIndex: {trace.HitboxBoneIndex}"); - Console.WriteLine($"! RayType: {trace.RayType}, StartInSolid: {trace.StartInSolid}, ExactHitPoint: {trace.ExactHitPoint}"); - Console.WriteLine("\n"); - } - - [GameEventHandler(HookMode.Pre)] - public HookResult TestGameEventHandler( EventPlayerJump @e ) - { - Console.WriteLine(@e.UserIdController.PlayerName); - return HookResult.Continue; - } - - [ServerNetMessageHandler] - public HookResult TestServerNetMessageHandler( CCSUsrMsg_SendPlayerItemDrops msg ) - { - Console.WriteLine("FIRED"); - return HookResult.Continue; - } - - private Callback _authTicketResponse; - - [EventListener] - public void OnSteamAPIActivated() - { - Console.WriteLine("TestPlugin OnSteamAPIActivated"); - _authTicketResponse = Callback.Create(AuthResponse); - } - - public void AuthResponse( GCMessageAvailable_t param ) - { - Console.WriteLine($"AuthResponse {param.m_nMessageSize}"); - } - - [Command("getip")] - public void GetIpCommand( ICommandContext context ) - { - context.Reply(SteamGameServer.GetPublicIP().ToString()); - } - - // [Command("i76")] - // public void TestIssue76Command( ICommandContext context ) - // { - // var player = context.Sender!; - // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); - // // Add the following code to render text properly - // //settingsMenu.Builder.AddText("123", overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(25f)); - // settingsMenu.Builder.AddText("123"); - // settingsMenu.Builder.AddText("1234"); - // settingsMenu.Builder.AddText("12345"); - - // Core.Menus.OpenMenu(player, settingsMenu); - // } - - // [Command("i77")] - // public void TestIssue77Command( ICommandContext context ) - // { - // var player = context.Sender!; - // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); - - // settingsMenu.Builder.AddText("123"); - // settingsMenu.Builder.AddSubmenu("Submenu", () => - // { - // var menu = Core.Menus.CreateMenu("Submenu"); - // menu.Builder.AddText("1234"); - // return menu; - // }); - - // settingsMenu.Builder.AddSubmenu("Async Submenu", async () => - // { - // await Task.Delay(5000); - // var menu = Core.Menus.CreateMenu("Async Submenu"); - // menu.Builder.AddText("12345"); - // return menu; - // }); - - // Core.Menus.OpenMenu(player, settingsMenu); - // } - - // [Command("i78")] - // public void TestIssue78Command( ICommandContext context ) - // { - // var player = context.Sender!; - // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); - // settingsMenu.Builder.AddButton("123", ( p ) => - // { - // player.SendMessage(MessageType.Chat, "Button"); - // }); - - // settingsMenu.Builder.Design.OverrideExitButton("shift"); - // settingsMenu.Builder.Design.OverrideSelectButton("e"); - - // Core.Menus.OpenMenu(player, settingsMenu); - // } - - [Command("rmt")] - public void RefactoredMenuTestCommand( ICommandContext context ) - { - var button = new ButtonMenuOption($"{HtmlGradient.GenerateGradientText("Swiftlys2 向这广袤世界致以温柔问候", "#FFE4E1", "#FFC0CB", "#FF69B4")}") { TextStyle = MenuOptionTextStyle.ScrollLeftLoop }; - button.Click += ( sender, args ) => - { - args.Player.SendMessage(MessageType.Chat, "Swiftlys2 向这广袤世界致以温柔问候"); - button.Enabled = false; - _ = Task.Run(async () => - { - await Task.Delay(1000); - button.Enabled = true; - }); - return ValueTask.CompletedTask; - }; - - var player = context.Sender!; - var menu = Core.MenusAPI - .CreateBuilder() - .SetPlayerFrozen(false) - .Design.SetMaxVisibleItems(5) - .Design.SetMenuTitle($"{HtmlGradient.GenerateGradientText("Redesigned Menu", "#00FA9A", "#F5FFFA")}") - .Design.SetMenuTitleVisible(true) - .Design.SetMenuFooterVisible(true) - .Design.EnableAutoAdjustVisibleItems() - .Design.SetGlobalScrollStyle(MenuOptionScrollStyle.WaitingCenter) - .AddOption(new TextMenuOption("1") { Visible = false }) - .AddOption(new ToggleMenuOption("12")) - .AddOption(new ChoiceMenuOption("123", ["Option 1", "Option 2", "Option 3"])) - .AddOption(new SliderMenuOption("1234")) - .AddOption(new ProgressBarMenuOption("12345", () => (float)new Random().NextDouble(), multiLine: false)) - .AddOption(new SubmenuMenuOption("123456", async () => - { - await Task.Delay(1000); - var menu = Core.MenusAPI.CreateBuilder() - .SetPlayerFrozen(true) - .Design.SetMenuTitle("Async Submenu") - .AddOption(new TextMenuOption("123456")) - .Build(); - return menu; - })) - .AddOption(new InputMenuOption("1234567")) - .AddOption(new TextMenuOption("12345678") { TextStyle = MenuOptionTextStyle.ScrollLeftLoop }) - .AddOption(new TextMenuOption("123456789")) - .AddOption(new TextMenuOption("1234567890") { Visible = false }) - .AddOption(button) - .AddOption(new TextMenuOption($"{HtmlGradient.GenerateGradientText("Swiftlys2 からこの広大なる世界へ温かい挨拶を", "#FFE5CC", "#FFAB91", "#FF7043")}") { TextStyle = MenuOptionTextStyle.ScrollRightLoop }) - .AddOption(new TextMenuOption($"{HtmlGradient.GenerateGradientText("Swiftlys2 가 이 넓은 세상에 따뜻한 인사를 전합니다", "#E6E6FA", "#00FFFF", "#FF1493")}") { TextStyle = MenuOptionTextStyle.ScrollLeftFade }) - .AddOption(new TextMenuOption($"{HtmlGradient.GenerateGradientText("Swiftlys2 приветствует этот прекрасный мир", "#AFEEEE", "#7FFFD4", "#40E0D0")}") { TextStyle = MenuOptionTextStyle.ScrollRightFade }) - .AddOption(new TextMenuOption("Swiftlys2 salută această lume minunată") { TextStyle = MenuOptionTextStyle.TruncateEnd }) - .AddOption(new TextMenuOption("Swiftlys2 extends warmest greetings to this wondrous world") { TextStyle = MenuOptionTextStyle.TruncateBothEnds }) - // .AddOption(new TextMenuOption("Swiftlys2 sendas korajn salutojn al ĉi tiu mirinda mondo")) - .AddOption(new TextMenuOption("1234567890") { Visible = false }) - .AddOption(new TextMenuOption("123456789")) - .AddOption(new TextMenuOption("12345678") { TextStyle = MenuOptionTextStyle.ScrollRightLoop }) - .AddOption(new InputMenuOption("1234567")) - .AddOption(new SubmenuMenuOption("123456", () => - { - var menu = Core.MenusAPI.CreateBuilder() - .Design.SetMenuTitle("Async Submenu") - .AddOption(new TextMenuOption("123456")) - .Build(); - return menu; - })) - .AddOption(new ProgressBarMenuOption("12345", () => (float)new Random().NextDouble(), multiLine: false)) - .AddOption(new SliderMenuOption("1234")) - .AddOption(new ChoiceMenuOption("123", ["Option 1", "Option 2", "Option 3"])) - .AddOption(new ToggleMenuOption("12")) - .AddOption(new TextMenuOption("1") { Visible = false }) - .Build(); - - Core.MenusAPI.OpenMenu(menu); - // Core.MenusAPI.OpenMenuForPlayer(player, menu); - } - - // [Command("mt")] - // public void MenuTestCommand( ICommandContext context ) - // { - // var player = context.Sender!; - - // IMenu settingsMenu = Core.Menus.CreateMenu("MenuTest"); - - // settingsMenu.Builder.Design.MaxVisibleItems(5); - - // // settingsMenu.Builder.Design.MaxVisibleItems(Random.Shared.Next(-2, 8)); - // if (context.Args.Length < 1 || !int.TryParse(context.Args[0], out int vtype)) vtype = 0; - // settingsMenu.Builder.Design.SetVerticalScrollStyle(vtype switch { - // 1 => MenuVerticalScrollStyle.LinearScroll, - // 2 => MenuVerticalScrollStyle.WaitingCenter, - // _ => MenuVerticalScrollStyle.CenterFixed - // }); - - // if (context.Args.Length < 2 || !int.TryParse(context.Args[1], out int htype)) htype = 0; - // settingsMenu.Builder.Design.SetGlobalHorizontalStyle(htype switch { - // 0 => MenuHorizontalStyle.Default, - // 1 => MenuHorizontalStyle.TruncateBothEnds(26f), - // 2 => MenuHorizontalStyle.ScrollLeftFade(26f, 8, 128), - // 3 => MenuHorizontalStyle.ScrollLeftLoop(26f, 8, 128), - // 1337 => MenuHorizontalStyle.TruncateEnd(0f), - // _ => MenuHorizontalStyle.TruncateEnd(26f) - // }); - - // settingsMenu.Builder.AddButton("1. AButton", ( p ) => - // { - // player.SendMessage(MessageType.Chat, "Button"); - // }); - - // settingsMenu.Builder.AddToggle("2. Toggle", defaultValue: true, ( p, value ) => - // { - // player.SendMessage(MessageType.Chat, $"AddToggle {value}"); - // }); - - // settingsMenu.Builder.AddSlider("3. Slider", min: 0, max: 100, defaultValue: 10, step: 10, ( p, value ) => - // { - // player.SendMessage(MessageType.Chat, $"AddSlider {value}"); - // }); - - // settingsMenu.Builder.AddAsyncButton("4. AsyncButton", async ( p ) => - // { - // await Task.Delay(2000); - // }); - - // settingsMenu.Builder.AddText("5. Text"); - // settingsMenu.Builder.AddText("6. Text"); - // settingsMenu.Builder.AddText("7. Text"); - // settingsMenu.Builder.AddText("8. Text"); - // settingsMenu.Builder.AddText("9. Text"); - // settingsMenu.Builder.AddSeparator(); - // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#FFE4E1", "#FFC0CB", "#FF69B4")}", overflowStyle: MenuHorizontalStyle.TruncateEnd(26f)); - // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#FFE5CC", "#FFAB91", "#FF7043")}", overflowStyle: MenuHorizontalStyle.TruncateBothEnds(26f)); - // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#E6E6FA", "#00FFFF", "#FF1493")}", overflowStyle: MenuHorizontalStyle.ScrollRightFade(26f, 8)); - // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#AFEEEE", "#7FFFD4", "#40E0D0")}", overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(26f, 8)); - // settingsMenu.Builder.AddText("12345678901234567890split12345678901234567890", overflowStyle: MenuHorizontalStyle.ScrollLeftFade(26f, 8, 128)); - // settingsMenu.Builder.AddText("一二三四五六七八九十分割一二三四五六七八九十", overflowStyle: MenuHorizontalStyle.ScrollRightLoop(26f, 8, 64)); - // settingsMenu.Builder.AddSeparator(); - // settingsMenu.Builder.AddText("Swiftlys2 向这广袤世界致以温柔问候", overflowStyle: MenuHorizontalStyle.ScrollRightLoop(26f, 8)); - // settingsMenu.Builder.AddText("Swiftlys2 からこの広大なる世界へ温かい挨拶を"); - // settingsMenu.Builder.AddText("Swiftlys2 가 이 넓은 세상에 따뜻한 인사를 전합니다"); - // settingsMenu.Builder.AddText("Swiftlys2 приветствует этот прекрасный мир"); - // settingsMenu.Builder.AddText("Swiftlys2 salută această lume minunată"); - // settingsMenu.Builder.AddText("Swiftlys2 extends warmest greetings to this wondrous world"); - // settingsMenu.Builder.AddText("Swiftlys2 sendas korajn salutojn al ĉi tiu mirinda mondo"); - // settingsMenu.Builder.AddSeparator(); - // settingsMenu.Builder.AddAsyncButton("AsyncButton|AsyncButton|AsyncButton", async ( p ) => await Task.Delay(2000)); - // settingsMenu.Builder.AddButton("Button|Button|Button|Button", ( p ) => { }); - // settingsMenu.Builder.AddChoice("Choice|Choice|Choice|Choice", ["Option 1", "Option 2", "Option 3"], "Option 1", ( p, value ) => { }, overflowStyle: MenuHorizontalStyle.TruncateEnd(8f)); - // settingsMenu.Builder.AddProgressBar("ProgressBar|ProgressBar|ProgressBar", () => (float)Random.Shared.NextDouble(), overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(26f, 12)); - // settingsMenu.Builder.AddSlider("Slider|Slider|Slider|Slider", 0f, 100f, 0f, 1f, ( p, value ) => { }, overflowStyle: MenuHorizontalStyle.ScrollRightLoop(8f, 12)); - // // settingsMenu.Builder.AddSubmenu("Submenu"); - // settingsMenu.Builder.AddToggle("Toggle|Toggle|Toggle|Toggle", true, ( p, value ) => { }); - // settingsMenu.Builder.AddSeparator(); - - // Core.Menus.OpenMenu(player, settingsMenu); - // } - - // [Command("menu")] - // public void MenuCommand( ICommandContext context ) - // { - // var player = context.Sender!; - // var menu = Core.Menus.CreateMenu("Test Menu"); - - // menu.Builder - // .AddButton("Button 1", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 1"); - // }) - // .AddButton("Button 2", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 2"); - // }) - // .AddButton("Button 3", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 3"); - // }) - // .AddButton("Button 4", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 4"); - // }) - // .AddButton("Button 5", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 5"); - // }) - // .AddButton("Button 6", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 6"); - // }) - // .AddButton("Button 7", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 7"); - // }) - // .AddButton("Button 8", ( ctx ) => - // { - // player.SendMessage(MessageType.Chat, "You clicked Button 8"); - // }) - // .AddSeparator() - // .AddText("hello!", size: IMenuTextSize.ExtraLarge) - // .AutoClose(15f) - // .HasSound(true) - // .ForceFreeze(); - - // menu.Builder.Design.SetColor(new(0, 186, 105, 255)); - - // Core.Menus.OpenMenu(player, menu); - // } - - public override void Unload() - { - Console.WriteLine("TestPlugin unloaded"); - } +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Tomlyn.Extensions.Configuration; +using System.Text.RegularExpressions; +using SwiftlyS2.Shared; +using SwiftlyS2.Shared.Commands; +using SwiftlyS2.Shared.GameEventDefinitions; +using SwiftlyS2.Shared.GameEvents; +using SwiftlyS2.Shared.NetMessages; +using SwiftlyS2.Shared.Misc; +using SwiftlyS2.Shared.Natives; +using SwiftlyS2.Shared.Plugins; +using SwiftlyS2.Shared.SchemaDefinitions; +using SwiftlyS2.Shared.ProtobufDefinitions; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using SwiftlyS2.Shared.Events; +using SwiftlyS2.Shared.Memory; +using YamlDotNet.Core.Tokens; +using Dapper; +using SwiftlyS2.Shared.Sounds; +using SwiftlyS2.Shared.EntitySystem; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Hosting; +using SwiftlyS2.Shared.Players; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Toolchains.InProcess.NoEmit; +using BenchmarkDotNet.Toolchains.InProcess.Emit; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using SwiftlyS2.Shared.Menus; +using SwiftlyS2.Shared.SteamAPI; +using SwiftlyS2.Core.Menus.OptionsBase; +using System.Collections.Concurrent; +using Dia2Lib; +using System.Reflection.Metadata; + +namespace TestPlugin; + +public class TestConfig +{ + public string Name { get; set; } + public int Age { get; set; } +} + +public class InProcessConfig : ManualConfig +{ + public InProcessConfig() + { + AddLogger(ConsoleLogger.Default); + AddJob(Job.Default + .WithToolchain(new InProcessNoEmitToolchain(true)) + .WithId("InProcess")); + } +} + +[PluginMetadata(Id = "testplugin", Version = "1.0.0")] +public class TestPlugin : BasePlugin +{ + public TestPlugin( ISwiftlyCore core ) : base(core) + { + Console.WriteLine("[TestPlugin] TestPlugin constructed successfully!"); + // Console.WriteLine($"sizeof(bool): {sizeof(bool)}"); + // Console.WriteLine($"Marshal.SizeOf: {Marshal.SizeOf()}"); + Core.Event.OnWeaponServicesCanUseHook += ( @event ) => + { + // Console.WriteLine($"WeaponServicesCanUse: {@event.Weapon.WeaponBaseVData.AttackMovespeedFactor} {@event.OriginalResult}"); + + }; + } + + [Command("be")] + public void Test2Command( ICommandContext context ) + { + BenchContext.Controller = context.Sender!.RequiredController; + BenchmarkRunner.Run(new InProcessConfig()); + } + + [GameEventHandler(HookMode.Pre)] + public HookResult OnPlayerSpawn( EventPlayerSpawn @event ) + { + if (!@event.UserIdPlayer.IsValid) + { + return HookResult.Continue; + } + var player = @event.UserIdPlayer.RequiredController; + if (player.InGameMoneyServices?.IsValid == true) + { + player.InGameMoneyServices.Account = Core.ConVar.Find("mp_maxmoney")?.Value ?? 16000; + player.InGameMoneyServices.AccountUpdated(); + } + return HookResult.Continue; + } + + public override void Load( bool hotReload ) + { + // Core.Command.HookClientCommand((playerId, commandLine) => + // { + // Console.WriteLine("TestPlugin HookClientCommand " + playerId + " " + commandLine); + // return HookResult.Continue; + // }); + + // Core.Event.OnConsoleOutput += (@event) => + // { + // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); + // }; + + // Core.Event.OnCommandExecuteHook += (@event) => + // { + // if (@event.HookMode == HookMode.Pre) return; + // Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.Command[0], @event.Command.ArgS); + // }; + + // Core.Event.OnEntityStartTouch += (@event) => + // { + // Console.WriteLine($"[New] EntityStartTouch: {@event.Entity.Entity?.DesignerName} -> {@event.OtherEntity.Entity?.DesignerName}"); + // }; + + // Core.Event.OnEntityTouchHook += (@event) => + // { + // switch (@event.TouchType) + // { + // case EntityTouchType.StartTouch: + // Console.WriteLine($"EntityStartTouch: {@event.Entity.Entity?.DesignerName} -> {@event.OtherEntity.Entity?.DesignerName}"); + // break; + // case EntityTouchType.Touch: + // break; + // case EntityTouchType.EndTouch: + // if (@event.Entity.Entity?.DesignerName != "player" || @event.OtherEntity.Entity?.DesignerName != "player") + // { + // return; + // } + // var player = @event.Entity.As(); + // var otherPlayer = @event.OtherEntity.As(); + // Console.WriteLine($"EntityEndTouch: {(player.Controller.Value?.PlayerName ?? string.Empty)} -> {(otherPlayer.Controller.Value?.PlayerName ?? string.Empty)}"); + // break; + // } + // }; + + Core.Engine.ExecuteCommandWithBuffer("@ping", ( buffer ) => + { + Console.WriteLine($"pong: {buffer}"); + }); + + Core.GameEvent.HookPre(@event => + { + @event.LocToken = "test"; + return HookResult.Continue; + }); + + Core.Configuration + .InitializeJsonWithModel("test.jsonc", "Main") + .Configure(( builder ) => + { + builder.AddJsonFile("test.jsonc", optional: false, reloadOnChange: true); + builder.AddTomlFile("test.toml", optional: true, reloadOnChange: true); + }); + + ServiceCollection services = new(); + + services + .AddSwiftly(Core); + + Core.Event.OnPrecacheResource += ( @event ) => + { + @event.AddItem("soundevents/mvp_anthem.vsndevts"); + }; + + Core.Event.OnConVarValueChanged += ( @event ) => + { + Console.WriteLine($"ConVar {@event.ConVarName} changed from {@event.OldValue} to {@event.NewValue} by player {@event.PlayerId}"); + }; + + + // var provider = services.BuildServiceProvider(); + + // provider.GetRequiredService(); + + + // Host.CreateDefaultBuilder() + // .ConfigureLogging((context, logging) => { + // logging.AddConsole(); + // }) + // .ConfigureAppConfiguration((context, config) => { + // config.SetBasePath(Core.Configuration.GetBasePath()); + // config.AddJsonFile("test.jsonc", optional: false, reloadOnChange: true); + // }) + // .ConfigureServices((context, services) => { + // services.AddOptionsWithValidateOnStart>() + // .Bind(context.Configuration.GetSection("Main")); + // }) + // .Build(); + + // This can be used everywhere and the value will be updated when the config is changed + // Console.WriteLine(config.CurrentValue.Age); + + + // var config = new TestConfig(); + + // throw new Exception("TestPlugin loaded"); + + // Core. + + int i = 0; + + // var token2 = Core.Scheduler.Repeat(10, () => { + // Console.WriteLine(Core.Engine.TickCount); + // Console.WriteLine("TestPlugin Timer"); + // }); + Core.Logger.LogInformation("TestPlugin loaded"); + + using var se = new SoundEvent(); + + // var func = Core.Memory.GetUnmanagedFunctionByAddress(Core.Memory.GetAddressBySignature(Library.Server, "AAAAA")!.Value); + + // func.CallOriginal(1, 2); + + // func.Call(1, 2); + + // func.AddHook((next) => { + // return (a, b) => { + // Console.WriteLine("TestPlugin Hook " + a + " " + b); + // next()(a, b); + // }; + // }); + + + // Entrypoint + + // Core.Event.OnTick += () => { + // Console.WriteLine("TestPlugin OnTick "); + // }; + + // Core.Event.OnClientConnected += (@event) => { + // Console.WriteLine("TestPlugin OnClientConnected " + @event.PlayerId); + // }; + + // Core.Event.OnClientPutInServer += (@event) => { + // Console.WriteLine("TestPlugin OnClientPutInServer " + @event.PlayerId); + // }; + + Core.Event.OnClientDisconnected += ( @event ) => + { + Console.WriteLine("TestPlugin OnClientDisconnected " + @event.PlayerId); + }; + Core.Event.OnTick += () => + { + int i = 0; + }; + + // Core.Event.OnClientProcessUsercmds += (@event) => { + // foreach(var usercmd in @event.Usercmds) { + // usercmd.Base.ButtonsPb.Buttonstate1 &= 1UL << (int)GameButtons.Ctrl; + // usercmd.Base.ButtonsPb.Buttonstate2 &= 1UL << (int)GameButtons.Ctrl; + // usercmd.Base.ButtonsPb.Buttonstate3 &= 1UL << (int)GameButtons.Ctrl; + // } + // }; + + // Core.NetMessage.HookClientMessage((msg, id) => { + // Console.WriteLine("TestPlugin OnClientMove "); + // Console.WriteLine(BitConverter.ToString(msg.Data)); + // return HookResult.Continue; + // }); + + // Core.Event.OnEntityTakeDamage += (@event) => { + // Console.WriteLine("TestPlugin OnEntityTakeDamage " + @event.Entity.Entity?.DesignerName + " " + @event.Info.HitGroupId); + // }; + + // Core.Event.OnTick += () => { + + // Console.WriteLine("TestPlugin OnTick"); + // }; + + // Core.Event.OnEntityCreated += (ev) => { + // var entity = ev.Entity; + // entity.Entity.DesignerName = "a"; + // Console.WriteLine("TestPlugin OnEntityCreated " + ev.Entity.Entity?.DesignerName); + // }; + + using CEntityKeyValues kv = new(); + + kv.SetBool("test", true); + + Console.WriteLine(kv.Get("test2")); + + CUtlStringToken token = new("hello"); + Console.WriteLine($"2"); + } + + CEntityKeyValues kv { get; set; } + CEntityInstance entity { get; set; } + + [Command("tt")] + public void TestCommand( ICommandContext context ) + { + // token2?.Cancel(); + // kv = new(); + // kv.SetString("test", "SAFE"); + + // _Core.Logger.LogInformation("!@#"); + + // _Core.Logger.LogInformation(_Core.GameData.GetSignature("CEntityInstance::AcceptInput").ToString()); + + // entity = _Core.EntitySystem.CreateEntityByDesignerName("point_worldtext"); + // entity.DispatchSpawn(kv); + // Console.WriteLine("Spawned entity with keyvalues"); + + int j = 0; + + var cvar = Core.ConVar.Find("sv_cheats")!; + Console.WriteLine(cvar); + Console.WriteLine(cvar.Value); + var cvar2 = Core.ConVar.Find("sv_autobunnyhopping")!; + Console.WriteLine(cvar2); + Console.WriteLine(cvar2.Value); + + var cvar3 = Core.ConVar.Create("sw_test_cvar", "Test cvar", "ABCDEFG"); + Console.WriteLine(cvar3); + Console.WriteLine(cvar3.Value); + + var cvar4 = Core.ConVar.Find("r_drawworld")!; + + cvar2.ReplicateToClient(0, true); + + cvar4.QueryClient(0, ( value ) => + { + Console.WriteLine("QueryCallback " + value); + }); + } + + [Command("w")] + public void TestCommand1( ICommandContext context ) + { + var ret = SteamGameServerUGC.DownloadItem(new PublishedFileId_t(3596198331), true); + Console.WriteLine(SteamGameServer.GetPublicIP().ToIPAddress()); + + + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + delegate nint DispatchSpawnDelegate( nint pEntity, nint pKV ); + int order = 0; + + IUnmanagedFunction? _dispatchspawn; + + [Command("h1")] + public void TestCommand2( ICommandContext context ) + { + // var token = Core.Scheduler.DelayAndRepeat(500, 1000, () => + // { + + // }); + + var addres = Core.GameData.GetSignature("CBaseEntity::DispatchSpawn"); + var func = Core.Memory.GetUnmanagedFunctionByAddress(addres); + + var guid = func.AddHook(( next ) => + { + return ( pEntity, pKV ) => + { + Console.WriteLine("TestPlugin DispatchSpawn " + order++); + return next()(pEntity, pKV); + }; + }); + + _dispatchspawn.AddHook(( next ) => + { + return ( pEntity, pKV ) => + { + Console.WriteLine("TestPlugin DispatchSpawn2 " + order++); + return next()(pEntity, pKV); + }; + }); + + } + + [EventListener] + public void OnEntityCreated( IOnEntityCreatedEvent @event ) + { + // @event.Entity.Entity.DesignerName = "abc"; + Console.WriteLine("TestPlugin OnEntityCreated222 " + @event.Entity.Entity?.DesignerName); + } + + Guid _hookId = Guid.Empty; + + [Command("bad")] + public void TestCommandBad( ICommandContext context ) + { + try + { + var isValveDS = Core.EntitySystem.GetGameRules()!.IsValveDS; + } + catch (Exception e) + { + Core.Logger.LogWarning("{Exception}", e.Message); + } + + try + { + Core.EntitySystem.GetGameRules()!.IsValveDS = true; + } + catch (Exception e) + { + Core.Logger.LogWarning("{Exception}", e.Message); + } + + try + { + Core.EntitySystem.GetGameRules()!.IsValveDSUpdated(); + } + catch (Exception e) + { + Core.Logger.LogWarning("{Exception}", e.Message); + } + } + + [Command("h2")] + public void TestCommand3( ICommandContext context ) + { + var ent = Core.EntitySystem.CreateEntity(); + ent.DispatchSpawn(); + ent.Collision.MaxsUpdated(); + ent.Collision.CollisionAttribute.OwnerIdUpdated(); + } + + [Command("tt3")] + public void TestCommand33( ICommandContext context ) + { + var ent = Core.EntitySystem.CreateEntity(); + using CEntityKeyValues kv = new(); + kv.Set("m_spawnflags", 256); + ent.DispatchSpawn(kv); + ent.SetModel("weapons/models/grenade/incendiary/weapon_incendiarygrenade.vmdl"); + ent.Teleport(new Vector(context.Sender!.PlayerPawn!.AbsOrigin!.Value.X + 50, context.Sender!.PlayerPawn!.AbsOrigin!.Value.Y + 50, context.Sender!.PlayerPawn!.AbsOrigin!.Value.Z + 30), QAngle.Zero, Vector.Zero); + } + + [Command("tt4")] + public void TestCommand4( ICommandContext context ) + { + Console.WriteLine(Core.Permission.PlayerHasPermission(7656, context.Args[0])); + } + + [Command("tt5")] + public void TestCommand5( ICommandContext context ) + { + Console.WriteLine("TestPlugin TestCommand5"); + } + + [Command("tt6", permission: "tt6")] + public void TestCommand6( ICommandContext context ) + { + Console.WriteLine("TestPlugin TestCommand6"); + } + + [Command("tt99")] + public void TestCommand99( ICommandContext context ) + { + Console.WriteLine(context.Sender!.SteamID); + Console.WriteLine(context.Sender!.UnauthorizedSteamID); + } + + [Command("tt7")] + public void TestCommand7( ICommandContext context ) + { + Core.Engine.ExecuteCommandWithBuffer("@ping", ( buffer ) => + { + Console.WriteLine($"pong: {buffer}"); + }); + } + + [Command("tt8")] + public unsafe void TestCommand8( ICommandContext context ) + { + Core.EntitySystem.GetAllEntitiesByDesignerName("func_buyzone").ToList().ForEach(zone => + { + if ((zone?.IsValid ?? false)) + { + zone.Despawn(); + } + }); + + var sender = context.Sender!; + var target = Core.PlayerManager.GetAllPlayers().FirstOrDefault(p => p.PlayerID != sender.PlayerID)!; + + var origin = sender.RequiredPlayerPawn.AbsOrigin ?? Vector.Zero; + var targetOrigin = target.RequiredPlayerPawn.AbsOrigin ?? Vector.Zero; + + Console.WriteLine("\n"); + Console.WriteLine($"Origin: {origin}"); + Console.WriteLine($"Target Origin: {targetOrigin}"); + + // Ray_t* ray = stackalloc Ray_t[1]; + // ray->Init(Vector.Zero, Vector.Zero); + Ray_t ray = new(); + ray.Init(Vector.Zero, Vector.Zero); + + var filter = new CTraceFilter { + // unk01 = 1, + IterateEntities = true, + QueryShapeAttributes = new RnQueryShapeAttr_t { + InteractsWith = MaskTrace.Player | MaskTrace.Solid | MaskTrace.Hitbox | MaskTrace.Npc, + InteractsExclude = MaskTrace.Empty, + InteractsAs = MaskTrace.Player, + CollisionGroup = CollisionGroup.PlayerMovement, + ObjectSetMask = RnQueryObjectSet.AllGameEntities, + HitSolid = true, + // HitTrigger = false, + // HitSolidRequiresGenerateContacts = false, + // ShouldIgnoreDisabledPairs = true, + // IgnoreIfBothInteractWithHitboxes = true, + // ForceHitEverything = true + } + }; + + // filter.QueryShapeAttributes.EntityIdsToIgnore[0] = unchecked((uint)-1); + // filter.QueryShapeAttributes.EntityIdsToIgnore[1] = unchecked((uint)-1); + // filter.QueryShapeAttributes.OwnerIdsToIgnore[0] = unchecked((uint)-1); + // filter.QueryShapeAttributes.OwnerIdsToIgnore[1] = unchecked((uint)-1); + // filter.QueryShapeAttributes.HierarchyIds[0] = 0; + // filter.QueryShapeAttributes.HierarchyIds[1] = 0; + + var trace = new CGameTrace(); + Core.Trace.TraceShape(origin, targetOrigin, ray, filter, ref trace); + + Console.WriteLine(trace.pEntity != null ? $"! Hit Entity: {trace.Entity.DesignerName}" : "! No entity hit"); + Console.WriteLine($"! SurfaceProperties: {(nint)trace.SurfaceProperties}, pEntity: {(nint)trace.pEntity}, HitBox: {(nint)trace.HitBox}({trace.HitBox->m_name.Value}), Body: {(nint)trace.Body}, Shape: {(nint)trace.Shape}, Contents: {trace.Contents}"); + Console.WriteLine($"! StartPos: {trace.StartPos}, EndPos: {trace.EndPos}, HitNormal: {trace.HitNormal}, HitPoint: {trace.HitPoint}"); + Console.WriteLine($"! HitOffset: {trace.HitOffset}, Fraction: {trace.Fraction}, Triangle: {trace.Triangle}, HitboxBoneIndex: {trace.HitboxBoneIndex}"); + Console.WriteLine($"! RayType: {trace.RayType}, StartInSolid: {trace.StartInSolid}, ExactHitPoint: {trace.ExactHitPoint}"); + Console.WriteLine("\n"); + } + + [GameEventHandler(HookMode.Pre)] + public HookResult TestGameEventHandler( EventPlayerJump @e ) + { + Console.WriteLine(@e.UserIdController.PlayerName); + return HookResult.Continue; + } + + [ServerNetMessageHandler] + public HookResult TestServerNetMessageHandler( CCSUsrMsg_SendPlayerItemDrops msg ) + { + Console.WriteLine("FIRED"); + return HookResult.Continue; + } + + private Callback _authTicketResponse; + + [EventListener] + public void OnSteamAPIActivated() + { + Console.WriteLine("TestPlugin OnSteamAPIActivated"); + _authTicketResponse = Callback.Create(AuthResponse); + } + + public void AuthResponse( GCMessageAvailable_t param ) + { + Console.WriteLine($"AuthResponse {param.m_nMessageSize}"); + } + + [Command("getip")] + public void GetIpCommand( ICommandContext context ) + { + context.Reply(SteamGameServer.GetPublicIP().ToString()); + } + + // [Command("i76")] + // public void TestIssue76Command( ICommandContext context ) + // { + // var player = context.Sender!; + // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); + // // Add the following code to render text properly + // //settingsMenu.Builder.AddText("123", overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(25f)); + // settingsMenu.Builder.AddText("123"); + // settingsMenu.Builder.AddText("1234"); + // settingsMenu.Builder.AddText("12345"); + + // Core.Menus.OpenMenu(player, settingsMenu); + // } + + // [Command("i77")] + // public void TestIssue77Command( ICommandContext context ) + // { + // var player = context.Sender!; + // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); + + // settingsMenu.Builder.AddText("123"); + // settingsMenu.Builder.AddSubmenu("Submenu", () => + // { + // var menu = Core.Menus.CreateMenu("Submenu"); + // menu.Builder.AddText("1234"); + // return menu; + // }); + + // settingsMenu.Builder.AddSubmenu("Async Submenu", async () => + // { + // await Task.Delay(5000); + // var menu = Core.Menus.CreateMenu("Async Submenu"); + // menu.Builder.AddText("12345"); + // return menu; + // }); + + // Core.Menus.OpenMenu(player, settingsMenu); + // } + + // [Command("i78")] + // public void TestIssue78Command( ICommandContext context ) + // { + // var player = context.Sender!; + // IMenu settingsMenu = Core.Menus.CreateMenu("Settings"); + // settingsMenu.Builder.AddButton("123", ( p ) => + // { + // player.SendMessage(MessageType.Chat, "Button"); + // }); + + // settingsMenu.Builder.Design.OverrideExitButton("shift"); + // settingsMenu.Builder.Design.OverrideSelectButton("e"); + + // Core.Menus.OpenMenu(player, settingsMenu); + // } + + [Command("tm")] + public void TestMenuCommand( ICommandContext context ) + { + var buyButton = new ButtonMenuOption("Purchase") { CloseAfterClick = true }; + buyButton.Click += async ( sender, args ) => + { + await Task.Delay(1000); + + if (sender is MenuOptionBase option) + { + var triggerOption = option!.Menu!.Parent.TriggerOption; + triggerOption!.Enabled = false; + args.Player.SendChat($"Purchase completed -> {triggerOption!.Text}"); + } + }; + + var confirmMenu = Core.MenusAPI + .CreateBuilder() + .Design.SetMenuTitle("Confirmation Menu") + .AddOption(buyButton) + .AddOption(new ButtonMenuOption("Cancel") { CloseAfterClick = true }) + .Build(); + + var menu = Core.MenusAPI + .CreateBuilder() + .Design.SetMenuTitle("Shop Menu") + .AddOption(new SubmenuMenuOption("Item 1", confirmMenu)) + .AddOption(new SubmenuMenuOption("Item 2", confirmMenu)) + .AddOption(new SubmenuMenuOption("Item 3", confirmMenu)) + .AddOption(new SubmenuMenuOption("Item 4", confirmMenu)) + .Build(); + + Core.MenusAPI.OpenMenuForPlayer(context.Sender!, menu); + } + + [Command("rmt")] + public void RefactoredMenuTestCommand( ICommandContext context ) + { + var button = new ButtonMenuOption(HtmlGradient.GenerateGradientText("Swiftlys2 向这广袤世界致以温柔问候", "#FFE4E1", "#FFC0CB", "#FF69B4")) { TextStyle = MenuOptionTextStyle.ScrollLeftLoop/*, CloseAfterClick = true*/ }; + button.Click += ( sender, args ) => + { + args.Player.SendMessage(MessageType.Chat, "Swiftlys2 向这广袤世界致以温柔问候"); + button.Enabled = false; + _ = Task.Run(async () => + { + await Task.Delay(1000); + button.Enabled = true; + }); + return ValueTask.CompletedTask; + }; + + var toggle = new ToggleMenuOption("12"); + toggle.ValueChanged += ( sender, args ) => + { + args.Player.SendChat($"OldValue: {args.OldValue}({args.OldValue.GetType().Name}), NewValue: {args.NewValue}({args.NewValue.GetType().Name})"); + }; + + var player = context.Sender!; + var menu = Core.MenusAPI + .CreateBuilder() + .SetPlayerFrozen(false) + .Design.SetMaxVisibleItems(5) + .Design.SetMenuTitle($"{HtmlGradient.GenerateGradientText("Redesigned Menu", "#00FA9A", "#F5FFFA")}") + .Design.SetMenuTitleVisible(true) + .Design.SetMenuFooterVisible(true) + .Design.SetMenuFooterColor("#0F0") + .Design.SetNavigationMarkerColor("#F0F8FFFF") + .Design.SetVisualGuideLineColor("#FFFFFF") + .Design.SetDisabledColor("#808080") + .Design.EnableAutoAdjustVisibleItems() + .Design.SetGlobalScrollStyle(MenuOptionScrollStyle.WaitingCenter) + .AddOption(new TextMenuOption("1") { Visible = false }) + .AddOption(toggle) + .AddOption(new ChoiceMenuOption("123", ["Option 1", "Option 2", "Option 3"])) + .AddOption(new SliderMenuOption("1234")) + .AddOption(new ProgressBarMenuOption("12345", () => (float)new Random().NextDouble(), multiLine: false)) + .AddOption(new SubmenuMenuOption("123456", async () => + { + await Task.Delay(1000); + var menu = Core.MenusAPI.CreateBuilder() + .SetPlayerFrozen(true) + .Design.SetMenuTitle("Async Submenu") + .AddOption(new TextMenuOption("123456")) + .Build(); + return menu; + })) + .AddOption(new InputMenuOption("1234567")) + .AddOption(new TextMenuOption() { Text = "12345678", TextStyle = MenuOptionTextStyle.ScrollLeftLoop }) + .AddOption(new TextMenuOption("123456789")) + .AddOption(new TextMenuOption("1234567890") { Visible = false }) + .AddOption(button) + .AddOption(new TextMenuOption(HtmlGradient.GenerateGradientText("Swiftlys2 からこの広大なる世界へ温かい挨拶を", "#FFE5CC", "#FFAB91", "#FF7043")) { TextStyle = MenuOptionTextStyle.ScrollRightLoop }) + .AddOption(new TextMenuOption(HtmlGradient.GenerateGradientText("Swiftlys2 가 이 넓은 세상에 따뜻한 인사를 전합니다", "#E6E6FA", "#00FFFF", "#FF1493")) { TextStyle = MenuOptionTextStyle.ScrollLeftFade }) + .AddOption(new TextMenuOption(HtmlGradient.GenerateGradientText("Swiftlys2 приветствует этот прекрасный мир", "#AFEEEE", "#7FFFD4", "#40E0D0")) { TextStyle = MenuOptionTextStyle.ScrollRightFade }) + .AddOption(new TextMenuOption("Swiftlys2 salută această lume minunată") { TextStyle = MenuOptionTextStyle.TruncateEnd }) + .AddOption(new TextMenuOption("Swiftlys2 extends warmest greetings to this wondrous world") { TextStyle = MenuOptionTextStyle.TruncateBothEnds }) + // .AddOption(new TextMenuOption("Swiftlys2 sendas korajn salutojn al ĉi tiu mirinda mondo")) + .AddOption(new TextMenuOption("1234567890") { Visible = false }) + .AddOption(new TextMenuOption("123456789")) + .AddOption(new TextMenuOption("12345678") { TextStyle = MenuOptionTextStyle.ScrollRightLoop }) + .AddOption(new InputMenuOption("1234567")) + .AddOption(new SubmenuMenuOption("123456", () => + { + var menu = Core.MenusAPI.CreateBuilder() + .Design.SetMenuTitle("Async Submenu") + .AddOption(new TextMenuOption("123456")) + .Build(); + return menu; + })) + .AddOption(new ProgressBarMenuOption("12345", () => (float)new Random().NextDouble(), multiLine: false)) + .AddOption(new SliderMenuOption("1234")) + .AddOption(new ChoiceMenuOption("123", ["Option 1", "Option 2", "Option 3"])) + .AddOption(new ToggleMenuOption("12", false, "O", "X")) + .AddOption(new TextMenuOption("1") { Visible = false }) + .Build(); + + Core.MenusAPI.OpenMenu(menu); + // Core.MenusAPI.OpenMenuForPlayer(player, menu); + } + + // [Command("mt")] + // public void MenuTestCommand( ICommandContext context ) + // { + // var player = context.Sender!; + + // IMenu settingsMenu = Core.Menus.CreateMenu("MenuTest"); + + // settingsMenu.Builder.Design.MaxVisibleItems(5); + + // // settingsMenu.Builder.Design.MaxVisibleItems(Random.Shared.Next(-2, 8)); + // if (context.Args.Length < 1 || !int.TryParse(context.Args[0], out int vtype)) vtype = 0; + // settingsMenu.Builder.Design.SetVerticalScrollStyle(vtype switch { + // 1 => MenuVerticalScrollStyle.LinearScroll, + // 2 => MenuVerticalScrollStyle.WaitingCenter, + // _ => MenuVerticalScrollStyle.CenterFixed + // }); + + // if (context.Args.Length < 2 || !int.TryParse(context.Args[1], out int htype)) htype = 0; + // settingsMenu.Builder.Design.SetGlobalHorizontalStyle(htype switch { + // 0 => MenuHorizontalStyle.Default, + // 1 => MenuHorizontalStyle.TruncateBothEnds(26f), + // 2 => MenuHorizontalStyle.ScrollLeftFade(26f, 8, 128), + // 3 => MenuHorizontalStyle.ScrollLeftLoop(26f, 8, 128), + // 1337 => MenuHorizontalStyle.TruncateEnd(0f), + // _ => MenuHorizontalStyle.TruncateEnd(26f) + // }); + + // settingsMenu.Builder.AddButton("1. AButton", ( p ) => + // { + // player.SendMessage(MessageType.Chat, "Button"); + // }); + + // settingsMenu.Builder.AddToggle("2. Toggle", defaultValue: true, ( p, value ) => + // { + // player.SendMessage(MessageType.Chat, $"AddToggle {value}"); + // }); + + // settingsMenu.Builder.AddSlider("3. Slider", min: 0, max: 100, defaultValue: 10, step: 10, ( p, value ) => + // { + // player.SendMessage(MessageType.Chat, $"AddSlider {value}"); + // }); + + // settingsMenu.Builder.AddAsyncButton("4. AsyncButton", async ( p ) => + // { + // await Task.Delay(2000); + // }); + + // settingsMenu.Builder.AddText("5. Text"); + // settingsMenu.Builder.AddText("6. Text"); + // settingsMenu.Builder.AddText("7. Text"); + // settingsMenu.Builder.AddText("8. Text"); + // settingsMenu.Builder.AddText("9. Text"); + // settingsMenu.Builder.AddSeparator(); + // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#FFE4E1", "#FFC0CB", "#FF69B4")}", overflowStyle: MenuHorizontalStyle.TruncateEnd(26f)); + // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#FFE5CC", "#FFAB91", "#FF7043")}", overflowStyle: MenuHorizontalStyle.TruncateBothEnds(26f)); + // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#E6E6FA", "#00FFFF", "#FF1493")}", overflowStyle: MenuHorizontalStyle.ScrollRightFade(26f, 8)); + // settingsMenu.Builder.AddText($"{HtmlGradient.GenerateGradientText("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "#AFEEEE", "#7FFFD4", "#40E0D0")}", overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(26f, 8)); + // settingsMenu.Builder.AddText("12345678901234567890split12345678901234567890", overflowStyle: MenuHorizontalStyle.ScrollLeftFade(26f, 8, 128)); + // settingsMenu.Builder.AddText("一二三四五六七八九十分割一二三四五六七八九十", overflowStyle: MenuHorizontalStyle.ScrollRightLoop(26f, 8, 64)); + // settingsMenu.Builder.AddSeparator(); + // settingsMenu.Builder.AddText("Swiftlys2 向这广袤世界致以温柔问候", overflowStyle: MenuHorizontalStyle.ScrollRightLoop(26f, 8)); + // settingsMenu.Builder.AddText("Swiftlys2 からこの広大なる世界へ温かい挨拶を"); + // settingsMenu.Builder.AddText("Swiftlys2 가 이 넓은 세상에 따뜻한 인사를 전합니다"); + // settingsMenu.Builder.AddText("Swiftlys2 приветствует этот прекрасный мир"); + // settingsMenu.Builder.AddText("Swiftlys2 salută această lume minunată"); + // settingsMenu.Builder.AddText("Swiftlys2 extends warmest greetings to this wondrous world"); + // settingsMenu.Builder.AddText("Swiftlys2 sendas korajn salutojn al ĉi tiu mirinda mondo"); + // settingsMenu.Builder.AddSeparator(); + // settingsMenu.Builder.AddAsyncButton("AsyncButton|AsyncButton|AsyncButton", async ( p ) => await Task.Delay(2000)); + // settingsMenu.Builder.AddButton("Button|Button|Button|Button", ( p ) => { }); + // settingsMenu.Builder.AddChoice("Choice|Choice|Choice|Choice", ["Option 1", "Option 2", "Option 3"], "Option 1", ( p, value ) => { }, overflowStyle: MenuHorizontalStyle.TruncateEnd(8f)); + // settingsMenu.Builder.AddProgressBar("ProgressBar|ProgressBar|ProgressBar", () => (float)Random.Shared.NextDouble(), overflowStyle: MenuHorizontalStyle.ScrollLeftLoop(26f, 12)); + // settingsMenu.Builder.AddSlider("Slider|Slider|Slider|Slider", 0f, 100f, 0f, 1f, ( p, value ) => { }, overflowStyle: MenuHorizontalStyle.ScrollRightLoop(8f, 12)); + // // settingsMenu.Builder.AddSubmenu("Submenu"); + // settingsMenu.Builder.AddToggle("Toggle|Toggle|Toggle|Toggle", true, ( p, value ) => { }); + // settingsMenu.Builder.AddSeparator(); + + // Core.Menus.OpenMenu(player, settingsMenu); + // } + + // [Command("menu")] + // public void MenuCommand( ICommandContext context ) + // { + // var player = context.Sender!; + // var menu = Core.Menus.CreateMenu("Test Menu"); + + // menu.Builder + // .AddButton("Button 1", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 1"); + // }) + // .AddButton("Button 2", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 2"); + // }) + // .AddButton("Button 3", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 3"); + // }) + // .AddButton("Button 4", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 4"); + // }) + // .AddButton("Button 5", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 5"); + // }) + // .AddButton("Button 6", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 6"); + // }) + // .AddButton("Button 7", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 7"); + // }) + // .AddButton("Button 8", ( ctx ) => + // { + // player.SendMessage(MessageType.Chat, "You clicked Button 8"); + // }) + // .AddSeparator() + // .AddText("hello!", size: IMenuTextSize.ExtraLarge) + // .AutoClose(15f) + // .HasSound(true) + // .ForceFreeze(); + + // menu.Builder.Design.SetColor(new(0, 186, 105, 255)); + + // Core.Menus.OpenMenu(player, menu); + // } + + public override void Unload() + { + Console.WriteLine("TestPlugin unloaded"); + } } \ No newline at end of file