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