diff --git a/.editorconfig b/.editorconfig
index 07149cbae..63c57481f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -73,3 +73,104 @@ cpp_wrap_preserve_blocks = one_liners
cpp_include_cleanup_add_missing_error_tag_type = suggestion
cpp_include_cleanup_remove_unused_error_tag_type = dimmed
+
+[*.cs]
+dotnet_style_qualification_for_field = true
+dotnet_style_qualification_for_property = true
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_event = true
+dotnet_diagnostic.IDE0003.severity = suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+dotnet_diagnostic.IDE0049.severity = warning
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+dotnet_diagnostic.IDE0036.severity = suggestion
+dotnet_style_require_accessibility_modifiers = always
+dotnet_diagnostic.IDE0040.severity = warning
+dotnet_style_readonly_field = true
+dotnet_diagnostic.IDE0044.severity = warning
+csharp_prefer_static_local_function = true
+dotnet_diagnostic.IDE0062.severity = warning
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_diagnostic.IDE0047.severity = suggestion
+dotnet_diagnostic.IDE0048.severity = suggestion
+dotnet_diagnostic.IDE0010.severity = warning
+dotnet_style_object_initializer = true
+dotnet_diagnostic.IDE0017.severity = warning
+csharp_style_inlined_variable_declaration = true
+dotnet_diagnostic.IDE0018.severity = warning
+dotnet_style_collection_initializer = true
+dotnet_diagnostic.IDE0028.severity = warning
+dotnet_style_prefer_auto_properties = true
+dotnet_diagnostic.IDE0032.severity = warning
+dotnet_style_explicit_tuple_names = true
+dotnet_diagnostic.IDE0033.severity = warning
+csharp_prefer_simple_default_expression = true
+dotnet_diagnostic.IDE0034.severity = warning
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_diagnostic.IDE0037.severity = warning
+csharp_style_prefer_local_over_anonymous_function = true
+dotnet_diagnostic.IDE0039.severity = warning
+csharp_style_deconstructed_variable_declaration = false
+dotnet_diagnostic.IDE0042.severity = warning
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_diagnostic.IDE0045.severity = warning
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_diagnostic.IDE0046.severity = warning
+dotnet_style_prefer_compound_assignment = true
+dotnet_diagnostic.IDE0054.severity = warning
+dotnet_diagnostic.IDE0074.severity = warning
+csharp_style_prefer_index_operator = true
+dotnet_diagnostic.IDE0056.severity = warning
+csharp_style_prefer_range_operator = false
+dotnet_diagnostic.IDE0057.severity = warning
+dotnet_diagnostic.IDE0070.severity = warning
+dotnet_style_prefer_simplified_interpolation = true
+dotnet_diagnostic.IDE0071.severity = warning
+dotnet_diagnostic.IDE0072.severity = warning
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_diagnostic.IDE0075.severity = warning
+dotnet_diagnostic.IDE0082.severity = warning
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+dotnet_diagnostic.IDE0090.severity = warning
+dotnet_diagnostic.IDE0180.severity = warning
+csharp_style_namespace_declarations = file_scoped
+dotnet_diagnostic.IDE0160.severity = warning
+dotnet_diagnostic.IDE0161.severity = warning
+csharp_style_throw_expression = true
+dotnet_diagnostic.IDE0016.severity = warning
+dotnet_style_coalesce_expression = true
+dotnet_diagnostic.IDE0029.severity = warning
+dotnet_diagnostic.IDE0030.severity = warning
+dotnet_style_null_propagation = true
+dotnet_diagnostic.IDE0031.severity = warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_diagnostic.IDE0041.severity = warning
+csharp_style_prefer_null_check_over_type_check = true
+dotnet_diagnostic.IDE0150.severity = warning
+csharp_style_conditional_delegate_call = true
+dotnet_diagnostic.IDE1005.severity = warning
+csharp_style_var_for_built_in_types = true
+csharp_style_var_when_type_is_apparent = true
+csharp_style_var_elsewhere = true
+dotnet_diagnostic.IDE0007.severity = warning
+dotnet_diagnostic.IDE0008.severity = warning
+dotnet_diagnostic.IDE0001.severity = warning
+dotnet_diagnostic.IDE0002.severity = warning
+dotnet_diagnostic.IDE0004.severity = warning
+dotnet_diagnostic.IDE0005.severity = warning
+dotnet_diagnostic.IDE0035.severity = warning
+dotnet_diagnostic.IDE0051.severity = warning
+dotnet_diagnostic.IDE0052.severity = warning
+csharp_style_unused_value_expression_statement_preference = discard_variable
+dotnet_diagnostic.IDE0058.severity = warning
+csharp_style_unused_value_assignment_preference = discard_variable
+dotnet_diagnostic.IDE0059.severity = warning
+
+indent_style = space
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_new_line_before_open_brace = types, object_collection, methods, control_blocks, lambdas
+dotnet_sort_system_directives_first = true
+csharp_space_between_method_declaration_parameter_list_parentheses = true
\ No newline at end of file
diff --git a/managed/managed.csproj b/managed/managed.csproj
index ff5a11582..ee691cbee 100644
--- a/managed/managed.csproj
+++ b/managed/managed.csproj
@@ -49,11 +49,14 @@
+
+
+
@@ -63,5 +66,5 @@
-
+
diff --git a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs
index 8f4521603..48f5f04f3 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Engine/EngineService.cs
@@ -3,6 +3,7 @@
using SwiftlyS2.Shared.Services;
using SwiftlyS2.Core.Extensions;
using SwiftlyS2.Shared.Natives;
+using SwiftlyS2.Shared.SchemaDefinitions;
namespace SwiftlyS2.Core.Services;
@@ -10,7 +11,7 @@ internal class EngineService : IEngineService
{
private readonly CommandTrackerManager _commandTrackedManager;
- public EngineService(CommandTrackerManager commandTrackedManager)
+ public EngineService( CommandTrackerManager commandTrackedManager )
{
this._commandTrackedManager = commandTrackedManager;
}
@@ -27,25 +28,39 @@ public EngineService(CommandTrackerManager commandTrackedManager)
public int TickCount => GlobalVars.TickCount;
- public void ExecuteCommand(string command)
+ public void ExecuteCommand( string command )
{
NativeEngineHelpers.ExecuteCommand(command);
}
- public void ExecuteCommandWithBuffer(string command, Action bufferCallback)
+ public void ExecuteCommandWithBuffer( string command, Action bufferCallback )
{
_commandTrackedManager.EnqueueCommand(bufferCallback);
NativeEngineHelpers.ExecuteCommand($"^wb^{command}");
}
- public bool IsMapValid(string map)
+ public bool IsMapValid( string map )
{
return NativeEngineHelpers.IsMapValid(map);
}
- public nint? FindGameSystemByName(string name)
+ public nint? FindGameSystemByName( string name )
{
var handle = NativeEngineHelpers.FindGameSystemByName(name);
return handle == nint.Zero ? null : handle;
}
+
+ public void DispatchParticleEffect( string particleName, ParticleAttachment_t attachmentType, byte attachmentPoint, CUtlSymbolLarge attachmentName, CRecipientFilter filter, bool resetAllParticlesOnEntity = false, int splitScreenSlot = 0, CBaseEntity? entity = null )
+ {
+ GameFunctions.DispatchParticleEffect(
+ particleName,
+ (uint)attachmentType,
+ entity?.Address ?? 0,
+ attachmentPoint,
+ attachmentName,
+ resetAllParticlesOnEntity,
+ splitScreenSlot,
+ filter
+ );
+ }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
index e1676d826..b20a4eb9e 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnEntityTouchHookEvent.cs
@@ -3,6 +3,7 @@
namespace SwiftlyS2.Core.Events;
+[Obsolete("OnEntityTouchHookEvent is deprecated. Use OnEntityStartTouchEvent, OnEntityTouchEvent, or OnEntityEndTouchEvent instead.")]
internal class OnEntityTouchHookEvent : IOnEntityTouchHookEvent
{
public required CBaseEntity Entity { get; init; }
@@ -10,4 +11,25 @@ internal class OnEntityTouchHookEvent : IOnEntityTouchHookEvent
public required CBaseEntity OtherEntity { get; init; }
public required EntityTouchType TouchType { get; init; }
+}
+
+internal class OnEntityStartTouchEvent : IOnEntityStartTouchEvent
+{
+ public required CBaseEntity Entity { get; init; }
+
+ public required CBaseEntity OtherEntity { get; init; }
+}
+
+internal class OnEntityTouchEvent : IOnEntityTouchEvent
+{
+ public required CBaseEntity Entity { get; init; }
+
+ public required CBaseEntity OtherEntity { get; init; }
+}
+
+internal class OnEntityEndTouchEvent : IOnEntityEndTouchEvent
+{
+ public required CBaseEntity Entity { get; init; }
+
+ public required CBaseEntity OtherEntity { get; init; }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
index 97b08ec7a..087e50ae4 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventPublisher.cs
@@ -513,6 +513,7 @@ public static void OnPrecacheResource(nint pResourceManifest)
}
}
+ [Obsolete("InvokeOnEntityTouchHook is deprecated. Use InvokeOnEntityStartTouch, InvokeOnEntityTouch, or InvokeOnEntityEndTouch instead.")]
public static void InvokeOnEntityTouchHook(OnEntityTouchHookEvent @event)
{
if (_subscribers.Count == 0) return;
@@ -531,6 +532,60 @@ public static void InvokeOnEntityTouchHook(OnEntityTouchHookEvent @event)
}
}
+ public static void InvokeOnEntityStartTouch(OnEntityStartTouchEvent @event)
+ {
+ if (_subscribers.Count == 0) return;
+ try
+ {
+ foreach (var subscriber in _subscribers)
+ {
+ subscriber.InvokeOnEntityStartTouch(@event);
+ }
+ return;
+ }
+ catch (Exception e)
+ {
+ AnsiConsole.WriteException(e);
+ return;
+ }
+ }
+
+ public static void InvokeOnEntityTouch(OnEntityTouchEvent @event)
+ {
+ if (_subscribers.Count == 0) return;
+ try
+ {
+ foreach (var subscriber in _subscribers)
+ {
+ subscriber.InvokeOnEntityTouch(@event);
+ }
+ return;
+ }
+ catch (Exception e)
+ {
+ AnsiConsole.WriteException(e);
+ return;
+ }
+ }
+
+ public static void InvokeOnEntityEndTouch(OnEntityEndTouchEvent @event)
+ {
+ if (_subscribers.Count == 0) return;
+ try
+ {
+ foreach (var subscriber in _subscribers)
+ {
+ subscriber.InvokeOnEntityEndTouch(@event);
+ }
+ return;
+ }
+ catch (Exception e)
+ {
+ AnsiConsole.WriteException(e);
+ return;
+ }
+ }
+
public static void InvokeOnCanAcquireHook(OnItemServicesCanAcquireHookEvent @event)
{
if (_subscribers.Count == 0) return;
diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
index ddb97a679..de97afe09 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventSubscriber.cs
@@ -47,6 +47,9 @@ public EventSubscriber(CoreContext id, IContextedProfilerService profiler, ILogg
public event EventDelegates.OnEntityTakeDamage? OnEntityTakeDamage;
public event EventDelegates.OnPrecacheResource? OnPrecacheResource;
public event EventDelegates.OnEntityTouchHook? OnEntityTouchHook;
+ public event EventDelegates.OnEntityStartTouch? OnEntityStartTouch;
+ public event EventDelegates.OnEntityTouch? OnEntityTouch;
+ public event EventDelegates.OnEntityEndTouch? OnEntityEndTouch;
public event EventDelegates.OnItemServicesCanAcquireHook? OnItemServicesCanAcquireHook;
public event EventDelegates.OnWeaponServicesCanUseHook? OnWeaponServicesCanUseHook;
public event EventDelegates.OnConsoleOutput? OnConsoleOutput;
@@ -344,6 +347,7 @@ public void InvokeOnPrecacheResource(OnPrecacheResourceEvent @event)
}
}
+ [Obsolete("InvokeOnEntityTouchHook is deprecated. Use InvokeOnEntityStartTouch, InvokeOnEntityTouch, or InvokeOnEntityEndTouch instead.")]
public void InvokeOnEntityTouchHook(OnEntityTouchHookEvent @event)
{
try
@@ -362,6 +366,60 @@ public void InvokeOnEntityTouchHook(OnEntityTouchHookEvent @event)
}
}
+ public void InvokeOnEntityStartTouch(OnEntityStartTouchEvent @event)
+ {
+ try
+ {
+ if (OnEntityStartTouch == null) return;
+ _Profiler.StartRecording("Event::OnEntityStartTouch");
+ OnEntityStartTouch?.Invoke(@event);
+ }
+ catch (Exception e)
+ {
+ _Logger.LogError(e, "Error invoking OnEntityStartTouch.");
+ }
+ finally
+ {
+ _Profiler.StopRecording("Event::OnEntityStartTouch");
+ }
+ }
+
+ public void InvokeOnEntityTouch(OnEntityTouchEvent @event)
+ {
+ try
+ {
+ if (OnEntityTouch == null) return;
+ _Profiler.StartRecording("Event::OnEntityTouch");
+ OnEntityTouch?.Invoke(@event);
+ }
+ catch (Exception e)
+ {
+ _Logger.LogError(e, "Error invoking OnEntityTouch.");
+ }
+ finally
+ {
+ _Profiler.StopRecording("Event::OnEntityTouch");
+ }
+ }
+
+ public void InvokeOnEntityEndTouch(OnEntityEndTouchEvent @event)
+ {
+ try
+ {
+ if (OnEntityEndTouch == null) return;
+ _Profiler.StartRecording("Event::OnEntityEndTouch");
+ OnEntityEndTouch?.Invoke(@event);
+ }
+ catch (Exception e)
+ {
+ _Logger.LogError(e, "Error invoking OnEntityEndTouch.");
+ }
+ finally
+ {
+ _Profiler.StopRecording("Event::OnEntityEndTouch");
+ }
+ }
+
public void InvokeOnItemServicesCanAcquireHook(OnItemServicesCanAcquireHookEvent @event)
{
try
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Menu.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Menu.cs
index 9028ab068..c86b40401 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Menu.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Menu.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Text;
+using System.Text.RegularExpressions;
using SwiftlyS2.Core.Menu.Options;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared;
@@ -10,9 +11,9 @@
namespace SwiftlyS2.Core.Menus;
-internal class Menu : IMenu
+internal partial class Menu : IMenu
{
- public string Title { get; set; } = "";
+ public string Title { get; set; } = "Menu";
public List Options { get; set; } = new();
@@ -40,10 +41,25 @@ internal class Menu : IMenu
private ConcurrentDictionary RenderedText { get; set; } = new();
private ConcurrentDictionary SelectedIndex { get; set; } = new();
private List PlayersWithMenuOpen { get; set; } = new();
+ private ConcurrentDictionary ScrollOffsets { get; set; } = new();
+ private ConcurrentDictionary ScrollCallCounts { get; set; } = new();
+ private ConcurrentDictionary ScrollPauseCounts { get; set; } = new();
internal ISwiftlyCore _Core { get; set; }
public bool HasSound { get; set; } = true;
public bool RenderOntick { get; set; } = false;
private bool Initialized { get; set; } = false;
+ public MenuVerticalScrollStyle VerticalScrollStyle { get; set; } = MenuVerticalScrollStyle.CenterFixed;
+ public MenuHorizontalStyle? HorizontalStyle { get; set; } = null;
+ public bool RequiresTickRendering => RenderOntick ||
+ HorizontalStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollLeftFade ||
+ HorizontalStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollRightFade ||
+ HorizontalStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollLeftLoop ||
+ HorizontalStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollRightLoop ||
+ Options.Any(opt =>
+ opt.OverflowStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollLeftFade ||
+ opt.OverflowStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollRightFade ||
+ opt.OverflowStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollLeftLoop ||
+ opt.OverflowStyle?.OverflowStyle == MenuHorizontalOverflowStyle.ScrollRightLoop);
public void Close(IPlayer player)
{
@@ -53,11 +69,15 @@ public void Close(IPlayer player)
PlayersWithMenuOpen.Remove(player);
- if (Initialized && PlayersWithMenuOpen.Count == 0 && RenderOntick)
+ if (Initialized && PlayersWithMenuOpen.Count == 0 && RequiresTickRendering)
{
Initialized = false;
_Core.Event.OnTick -= OnTickRender;
}
+
+ ScrollOffsets.Clear();
+ ScrollCallCounts.Clear();
+ ScrollPauseCounts.Clear();
}
public void MoveSelection(IPlayer player, int offset)
@@ -87,7 +107,7 @@ public void MoveSelection(IPlayer player, int offset)
Rerender(player);
}
- public void Rerender(IPlayer player)
+ public void Rerender(IPlayer player, bool updateDisplayText = false)
{
BeforeRender?.Invoke(player);
@@ -126,11 +146,36 @@ public void Rerender(IPlayer player)
var selectedIdx = SelectedIndex[player];
var halfVisible = maxVisibleOptions / 2;
- for (int offset = -halfVisible; offset <= halfVisible && offset < maxVisibleOptions - halfVisible; offset++)
+ var (startIndex, arrowPosition) = VerticalScrollStyle switch
+ {
+ MenuVerticalScrollStyle.WaitingCenter when selectedIdx < halfVisible
+ => (0, selectedIdx), // WaitingCenter: (Near top) start from 0, arrow at selected
+ MenuVerticalScrollStyle.WaitingCenter when selectedIdx >= totalOptions - halfVisible
+ => (totalOptions - maxVisibleOptions, maxVisibleOptions - (totalOptions - selectedIdx)), // WaitingCenter: (Near bottom) start from end-visible, arrow at bottom area
+ MenuVerticalScrollStyle.WaitingCenter
+ => (selectedIdx - halfVisible, halfVisible), // WaitingCenter: (Middle) start from selected-half, arrow at center
+
+ MenuVerticalScrollStyle.LinearScroll when maxVisibleOptions == 1
+ => (selectedIdx, 0), // LinearScroll: single visible, start from selected, arrow at top
+
+ MenuVerticalScrollStyle.LinearScroll when selectedIdx < maxVisibleOptions - 1
+ => (0, selectedIdx), // LinearScroll: (Near top) start from 0, arrow at selected
+ MenuVerticalScrollStyle.LinearScroll when selectedIdx >= totalOptions - (maxVisibleOptions - 1)
+ => (totalOptions - maxVisibleOptions, maxVisibleOptions - (totalOptions - selectedIdx)), // LinearScroll: (Near bottom) start from end-visible, arrow at bottom area
+ MenuVerticalScrollStyle.LinearScroll
+ => (selectedIdx - (maxVisibleOptions - 1), maxVisibleOptions - 1), // LinearScroll: (Middle) start from selected-visible+1, arrow at bottom
+
+ _
+ => (-1, halfVisible) // CenterFixed: no scroll, arrow at middle
+ };
+
+ for (int i = 0; i < maxVisibleOptions; i++)
{
- var actualIndex = (selectedIdx + offset + totalOptions) % totalOptions;
+ var (actualIndex, isSelected) = VerticalScrollStyle == MenuVerticalScrollStyle.CenterFixed
+ ? ((selectedIdx + i - halfVisible + totalOptions) % totalOptions, i == halfVisible) // CenterFixed: circular wrap, arrow at center
+ : (startIndex + i, i == arrowPosition); // WaitingCenter / LinearScroll: linear offset, arrow at position
+
var option = visibleOptions[actualIndex];
- var isSelected = offset == 0;
var arrowSizeClass = MenuSizeHelper.GetSizeClass(option.GetTextSize());
if (isSelected)
@@ -142,7 +187,10 @@ public void Rerender(IPlayer player)
html.Append("\u00A0\u00A0\u00A0 ");
}
- html.Append(option.GetDisplayText(player));
+ if (updateDisplayText)
+ {
+ html.Append(option.GetDisplayText(player));
+ }
html.Append("
");
}
@@ -164,7 +212,10 @@ public void Rerender(IPlayer player)
html.Append("\u00A0\u00A0\u00A0 ");
}
- html.Append(option.GetDisplayText(player));
+ if (updateDisplayText)
+ {
+ html.Append(option.GetDisplayText(player));
+ }
html.Append("
");
}
@@ -205,9 +256,9 @@ private string BuildFooter()
private void OnTickRender()
{
- foreach (var p in PlayersWithMenuOpen)
+ foreach (var player in PlayersWithMenuOpen)
{
- Rerender(p);
+ Rerender(player, true);
}
}
@@ -223,7 +274,7 @@ public void Show(IPlayer player)
PlayersWithMenuOpen.Add(player);
}
- if (!Initialized && RenderOntick)
+ if (!Initialized && RequiresTickRendering)
{
Initialized = true;
_Core.Event.OnTick += OnTickRender;
@@ -240,6 +291,10 @@ public void Show(IPlayer player)
Close(player);
});
}
+
+ ScrollOffsets.Clear();
+ ScrollCallCounts.Clear();
+ ScrollPauseCounts.Clear();
}
public void UseSelection(IPlayer player)
@@ -271,7 +326,7 @@ public void UseSelection(IPlayer player)
}
asyncButton.IsLoading = true;
asyncButton.SetLoadingText("Processing...");
- Rerender(player);
+ Rerender(player, true);
var closeAfter = asyncButton.CloseOnSelect;
Task.Run(async () =>
{
@@ -282,7 +337,7 @@ public void UseSelection(IPlayer player)
finally
{
asyncButton.IsLoading = false;
- Rerender(player);
+ Rerender(player, true);
if (closeAfter && player.IsValid)
{
@@ -301,7 +356,7 @@ public void UseSelection(IPlayer player)
}
else
{
- Rerender(player);
+ Rerender(player, true);
}
break;
}
@@ -371,4 +426,532 @@ public void SetFreezeState(IPlayer player, bool freeze)
pawn.ActualMoveType = moveType;
pawn.MoveTypeUpdated();
}
+
+ internal string ApplyHorizontalStyle(string text, MenuHorizontalStyle? overflowStyle = null)
+ {
+ var activeStyle = overflowStyle ?? HorizontalStyle;
+
+ if (activeStyle == null || string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ var plainText = StripHtmlTags(text);
+ if (Helper.EstimateTextWidth(plainText) <= activeStyle.Value.MaxWidth)
+ {
+ return text;
+ }
+
+ return activeStyle.Value.OverflowStyle switch
+ {
+ MenuHorizontalOverflowStyle.TruncateEnd => TruncateTextEnd(text, activeStyle.Value.MaxWidth),
+ MenuHorizontalOverflowStyle.TruncateBothEnds => TruncateTextBothEnds(text, activeStyle.Value.MaxWidth),
+ MenuHorizontalOverflowStyle.ScrollLeftFade => ScrollTextWithFade(text, activeStyle.Value.MaxWidth, true, activeStyle),
+ MenuHorizontalOverflowStyle.ScrollRightFade => ScrollTextWithFade(text, activeStyle.Value.MaxWidth, false, activeStyle),
+ MenuHorizontalOverflowStyle.ScrollLeftLoop => ScrollTextWithLoop($"{text.TrimEnd()} ", activeStyle.Value.MaxWidth, true, activeStyle),
+ MenuHorizontalOverflowStyle.ScrollRightLoop => ScrollTextWithLoop($" {text.TrimStart()}", activeStyle.Value.MaxWidth, false, activeStyle),
+ _ => text
+ };
+ }
+
+ private string ScrollTextWithFade(string text, float maxWidth, bool scrollLeft, MenuHorizontalStyle? style = null)
+ {
+ // Prepare scroll data and validate
+ var (plainChars, segments, targetCharCount) = PrepareScrollData(text, maxWidth);
+ if (plainChars is null)
+ {
+ return text;
+ }
+ if (targetCharCount == 0)
+ {
+ return string.Empty;
+ }
+
+ // Update scroll offset (allow scrolling beyond end for complete fade-out)
+ var offset = UpdateScrollOffset(StripHtmlTags(text), scrollLeft, plainChars.Length + 1, style);
+
+ // Calculate visible character range
+ var (skipStart, skipEnd) = scrollLeft
+ ? (offset, Math.Max(0, plainChars.Length - offset - targetCharCount))
+ : (Math.Max(0, plainChars.Length - targetCharCount - offset), offset);
+
+ // Build output with proper HTML tag tracking
+ StringBuilder result = new();
+ List outputTags = [], activeTags = [];
+ var (charIdx, started) = (0, false);
+
+ foreach (var (content, isTag) in segments)
+ {
+ if (isTag)
+ {
+ // Track active opening and closing tags
+ UpdateTagState(content, activeTags);
+
+ // Output tags within visible window
+ if (started)
+ {
+ result.Append(content);
+ ProcessOpenTag(content, outputTags);
+ }
+ }
+ else
+ {
+ // Process characters within scroll window
+ foreach (var ch in content)
+ {
+ if (charIdx >= skipStart && charIdx < plainChars.Length - skipEnd)
+ {
+ // Apply active tags at start of output
+ if (!started)
+ {
+ started = true;
+ activeTags.ForEach(tag => { result.Append(tag); ProcessOpenTag(tag, outputTags); });
+ }
+ result.Append(ch);
+ }
+ charIdx++;
+ }
+ }
+ }
+
+ CloseOpenTags(result, outputTags);
+ return result.ToString();
+ }
+
+ private string ScrollTextWithLoop(string text, float maxWidth, bool scrollLeft, MenuHorizontalStyle? style = null)
+ {
+ // Prepare scroll data and validate
+ var (plainChars, segments, targetCharCount) = PrepareScrollData(text, maxWidth);
+ if (plainChars is null)
+ {
+ return text;
+ }
+ if (targetCharCount == 0)
+ {
+ return string.Empty;
+ }
+
+ // Update scroll offset for circular wrapping
+ var offset = UpdateScrollOffset(StripHtmlTags(text), scrollLeft, plainChars.Length, style);
+
+ // Build character-to-tags mapping for circular access
+ Dictionary> charToActiveTags = [];
+ List currentActiveTags = [];
+ var currentCharIdx = 0;
+
+ foreach (var (content, isTag) in segments)
+ {
+ if (isTag)
+ {
+ // Track active opening and closing tags
+ UpdateTagState(content, currentActiveTags);
+ }
+ else
+ {
+ // Map each character to its active tags
+ foreach (var ch in content)
+ {
+ charToActiveTags[currentCharIdx] = [.. currentActiveTags];
+ currentCharIdx++;
+ }
+ }
+ }
+
+ // Build output in circular order with dynamic tag management
+ StringBuilder result = new();
+ List outputTags = [];
+ List? previousTags = null;
+
+ for (int i = 0; i < targetCharCount; i++)
+ {
+ // Calculate circular character index
+ var charIndex = scrollLeft
+ ? (offset + i) % plainChars.Length
+ : (plainChars.Length - offset + i) % plainChars.Length;
+ var currentTags = charToActiveTags.GetValueOrDefault(charIndex, []);
+
+ // Close tags that are no longer active
+ if (previousTags is not null)
+ {
+ for (int j = previousTags.Count - 1; j >= 0; j--)
+ {
+ if (!currentTags.Contains(previousTags[j]))
+ {
+ var prevTagName = previousTags[j][1..^1].Split(' ')[0];
+ result.Append($"{prevTagName}>");
+ var idx = outputTags.FindLastIndex(t => t.Equals(prevTagName, StringComparison.OrdinalIgnoreCase));
+ if (idx >= 0)
+ {
+ outputTags.RemoveAt(idx);
+ }
+ }
+ }
+ }
+
+ // Open new tags that are now active
+ foreach (var tag in currentTags)
+ {
+ if (previousTags is null || !previousTags.Contains(tag))
+ {
+ result.Append(tag);
+ var tagName = tag[1..^1].Split(' ')[0];
+ outputTags.Add(tagName);
+ }
+ }
+
+ result.Append(plainChars[charIndex]);
+ previousTags = currentTags;
+ }
+
+ CloseOpenTags(result, outputTags);
+ return result.ToString();
+ }
+
+ private static string TruncateTextEnd(string text, float maxWidth, string suffix = "...")
+ {
+ // Reserve space for suffix
+ var targetWidth = maxWidth - Helper.EstimateTextWidth(suffix);
+ if (targetWidth <= 0)
+ {
+ return suffix;
+ }
+
+ var segments = ParseHtmlSegments(text);
+ StringBuilder result = new();
+ List openTags = [];
+ var (currentWidth, reachedLimit) = (0f, false);
+
+ foreach (var (content, isTag) in segments)
+ {
+ switch (isTag, reachedLimit)
+ {
+ // Preserve HTML tags before reaching limit
+ case (true, false):
+ result.Append(content);
+ ProcessOpenTag(content, openTags);
+ break;
+
+ // Process plain text characters until width limit
+ case (false, false):
+ foreach (var ch in content)
+ {
+ var charWidth = Helper.GetCharWidth(ch);
+ if (currentWidth + charWidth > targetWidth)
+ {
+ reachedLimit = true;
+ break;
+ }
+ result.Append(ch);
+ currentWidth += charWidth;
+ }
+ break;
+ }
+ }
+
+ if (reachedLimit)
+ {
+ result.Append(suffix);
+ }
+
+ CloseOpenTags(result, openTags);
+ return result.ToString();
+ }
+
+ private static string TruncateTextBothEnds(string text, float maxWidth)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ // Check if text fits without truncation
+ var plainText = StripHtmlTags(text);
+ if (Helper.EstimateTextWidth(plainText) <= maxWidth)
+ {
+ return text;
+ }
+
+ // Extract all plain text characters from segments
+ var segments = ParseHtmlSegments(text);
+ var plainChars = segments
+ .Where(s => !s.IsTag)
+ .SelectMany(s => s.Content)
+ .ToArray();
+
+ if (plainChars.Length == 0)
+ {
+ return text;
+ }
+
+ // Calculate how many characters can fit
+ var targetCharCount = CalculateTargetCharCount(plainChars, maxWidth);
+ if (targetCharCount == 0)
+ {
+ return string.Empty;
+ }
+
+ // Calculate range to keep from middle
+ var skipFromStart = Math.Max(0, (plainChars.Length - targetCharCount) / 2);
+ var skipFromEnd = plainChars.Length - skipFromStart - targetCharCount;
+
+ StringBuilder result = new();
+ List outputOpenTags = [];
+ List pendingOpenTags = [];
+ var (plainCharIndex, hasStartedOutput) = (0, false);
+
+ foreach (var (content, isTag) in segments)
+ {
+ switch (isTag, hasStartedOutput)
+ {
+ // Process tags after output has started
+ case (true, true):
+ result.Append(content);
+ ProcessOpenTag(content, outputOpenTags);
+ break;
+
+ // Queue opening tags before output starts
+ case (true, false) when !content.StartsWith("") && !content.StartsWith(""):
+ pendingOpenTags.Add(content);
+ break;
+
+ // Process plain text, keeping only middle portion
+ case (false, _):
+ foreach (var ch in content)
+ {
+ if (plainCharIndex >= skipFromStart && plainCharIndex < plainChars.Length - skipFromEnd)
+ {
+ // Start output and apply pending tags
+ if (!hasStartedOutput)
+ {
+ hasStartedOutput = true;
+ pendingOpenTags.ForEach(tag =>
+ {
+ result.Append(tag);
+ ProcessOpenTag(tag, outputOpenTags);
+ });
+ }
+ result.Append(ch);
+ }
+ plainCharIndex++;
+ }
+ break;
+ }
+ }
+
+ CloseOpenTags(result, outputOpenTags);
+ return result.ToString();
+ }
+}
+
+internal partial class Menu
+{
+ [GeneratedRegex("<.*?>")]
+ private static partial Regex HtmlTagRegex();
+
+ ///
+ /// Removes all HTML tags from the given text.
+ ///
+ /// The text containing HTML tags.
+ /// The text with all HTML tags removed.
+ private static string StripHtmlTags(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ return HtmlTagRegex().Replace(text, string.Empty);
+ }
+
+ ///
+ /// Parses text into segments, separating HTML tags from plain text content.
+ ///
+ /// The text to parse.
+ /// A list of segments where each segment is either a tag or plain text content.
+ private static List<(string Content, bool IsTag)> ParseHtmlSegments(string text)
+ {
+ var tagMatches = HtmlTagRegex().Matches(text);
+ if (tagMatches.Count == 0)
+ {
+ return [(text, false)];
+ }
+
+ List<(string Content, bool IsTag)> segments = [];
+ var currentIndex = 0;
+
+ foreach (Match match in tagMatches)
+ {
+ if (match.Index > currentIndex)
+ {
+ segments.Add((text[currentIndex..match.Index], false));
+ }
+ segments.Add((match.Value, true));
+ currentIndex = match.Index + match.Length;
+ }
+
+ if (currentIndex < text.Length)
+ {
+ segments.Add((text[currentIndex..], false));
+ }
+
+ return segments;
+ }
+
+ ///
+ /// Processes an HTML tag and updates the list of currently open tags.
+ /// Adds opening tags to the list and removes matching closing tags.
+ ///
+ /// The HTML tag to process.
+ /// The list of currently open tag names.
+ private static void ProcessOpenTag(string tag, List openTags)
+ {
+ var tagName = tag switch
+ {
+ ['<', '/', .. var rest] => new string(rest).TrimEnd('>').Split(' ', 2)[0],
+ ['<', '!', ..] => null,
+ [.. var chars] when chars[^1] == '/' && chars[^2] == '>' => null,
+ ['<', .. var rest] => new string(rest).TrimEnd('>').Split(' ', 2)[0],
+ _ => null
+ };
+
+ if (tagName is null)
+ {
+ return;
+ }
+
+ if (tag.StartsWith(""))
+ {
+ var index = openTags.FindLastIndex(t => t.Equals(tagName, StringComparison.OrdinalIgnoreCase));
+ if (index >= 0) openTags.RemoveAt(index);
+ }
+ else
+ {
+ openTags.Add(tagName);
+ }
+ }
+
+ ///
+ /// Appends closing tags for all currently open tags in reverse order.
+ ///
+ /// The StringBuilder to append closing tags to.
+ /// The list of currently open tag names.
+ private static void CloseOpenTags(StringBuilder result, List openTags)
+ {
+ openTags.AsEnumerable().Reverse().ToList().ForEach(tag => result.Append($"{tag}>"));
+ }
+
+ ///
+ /// Calculates how many characters can fit within the specified width.
+ ///
+ /// The characters to measure.
+ /// The maximum width allowed.
+ /// The number of characters that fit within the width.
+ private static int CalculateTargetCharCount(ReadOnlySpan plainChars, float maxWidth)
+ {
+ var currentWidth = 0f;
+ var count = 0;
+
+ foreach (var ch in plainChars)
+ {
+ var charWidth = Helper.GetCharWidth(ch);
+ if (currentWidth + charWidth > maxWidth) break;
+ currentWidth += charWidth;
+ count++;
+ }
+
+ return count;
+ }
+
+ ///
+ /// Updates and returns the scroll offset for the given text.
+ /// The offset increments based on tick count and wraps around at the specified length.
+ ///
+ /// The plain text being scrolled.
+ /// Whether scrolling left or right.
+ /// The length at which the offset wraps around.
+ /// Optional horizontal style settings.
+ /// The current scroll offset.
+ private int UpdateScrollOffset(string plainText, bool scrollLeft, int wrapLength, MenuHorizontalStyle? style)
+ {
+ var key = $"{plainText}_{scrollLeft}";
+ ScrollOffsets.TryAdd(key, 0);
+ ScrollCallCounts.TryAdd(key, 0);
+ ScrollPauseCounts.TryAdd(key, 0);
+
+ var ticksPerScroll = style?.TicksPerScroll ?? HorizontalStyle?.TicksPerScroll ?? 16;
+ var pauseTicks = style?.PauseTicks ?? HorizontalStyle?.PauseTicks ?? 0;
+
+ // If currently pausing, decrement pause counter and maintain current offset
+ if (ScrollPauseCounts[key] > 0)
+ {
+ ScrollPauseCounts[key]--;
+ return ScrollOffsets[key];
+ }
+
+ // Increment call count and scroll when threshold is reached
+ if (++ScrollCallCounts[key] >= ticksPerScroll)
+ {
+ ScrollCallCounts[key] = 0;
+ var newOffset = (ScrollOffsets[key] + 1) % wrapLength;
+
+ // When completing a full loop (offset returns to 0), start pause if configured
+ if (newOffset == 0 && pauseTicks > 0)
+ {
+ ScrollPauseCounts[key] = pauseTicks;
+ }
+
+ ScrollOffsets[key] = newOffset;
+ }
+
+ return ScrollOffsets[key];
+ }
+
+ ///
+ /// Updates the list of active tags based on the given HTML tag content.
+ /// Adds opening tags and removes matching closing tags.
+ ///
+ /// The HTML tag content to process.
+ /// The list of currently active tags.
+ private static void UpdateTagState(string content, List activeTags)
+ {
+ if (!content.StartsWith("") && !content.StartsWith(""))
+ {
+ activeTags.Add(content);
+ }
+ else if (content.StartsWith(""))
+ {
+ var tagName = content[2..^1].Split(' ')[0];
+ var index = activeTags.FindLastIndex(t => t[1..^1].Split(' ')[0].Equals(tagName, StringComparison.OrdinalIgnoreCase));
+ if (index >= 0)
+ {
+ activeTags.RemoveAt(index);
+ }
+ }
+ }
+
+ ///
+ /// Prepares data required for text scrolling by extracting plain characters and parsing segments.
+ ///
+ /// The text to prepare for scrolling.
+ /// The maximum width available for display.
+ /// A tuple containing plain characters array, HTML segments, and target character count.
+ private static (char[]? PlainChars, List<(string Content, bool IsTag)> Segments, int TargetCharCount) PrepareScrollData(string text, float maxWidth)
+ {
+ var plainText = StripHtmlTags(text);
+ if (Helper.EstimateTextWidth(plainText) <= maxWidth)
+ {
+ return (null, [], 0);
+ }
+
+ var segments = ParseHtmlSegments(text);
+ var plainChars = segments.Where(s => !s.IsTag).SelectMany(s => s.Content).ToArray();
+
+ if (plainChars.Length == 0)
+ {
+ return (null, segments, 0);
+ }
+
+ var targetCharCount = CalculateTargetCharCount(plainChars, maxWidth);
+ return (plainChars, segments, targetCharCount);
+ }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilder.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilder.cs
index 66494a99d..a27abe5c4 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilder.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuBuilder.cs
@@ -9,6 +9,9 @@ internal class MenuBuilder : IMenuBuilder
{
private IMenu? _menu;
private IMenu? _parent;
+ private MenuDesign? _design;
+
+ public IMenuDesign Design => _design ??= new MenuDesign(_menu!);
public IMenuBuilder SetMenu(IMenu menu)
{
@@ -16,85 +19,85 @@ public IMenuBuilder SetMenu(IMenu menu)
return this;
}
- public IMenuBuilder AddButton(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddButton(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ButtonMenuOption(text, onClick, size));
+ _menu!.Options.Add(new ButtonMenuOption(text, onClick, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddButton(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddButton(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ButtonMenuOption(text, onClick, size));
+ _menu!.Options.Add(new ButtonMenuOption(text, onClick, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddButton(string text, Action? onClick)
+ public IMenuBuilder AddButton(string text, Action? onClick, MenuHorizontalStyle? overflowStyle = null)
{
- return AddButton(text, onClick, IMenuTextSize.Medium);
+ return AddButton(text, onClick, IMenuTextSize.Medium, overflowStyle);
}
- public IMenuBuilder AddToggle(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddToggle(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ToggleMenuOption(text, defaultValue, onToggle, size));
+ _menu!.Options.Add(new ToggleMenuOption(text, defaultValue, onToggle, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ToggleMenuOption(text, defaultValue, onToggle, size));
+ _menu!.Options.Add(new ToggleMenuOption(text, defaultValue, onToggle, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle)
+ public IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle, MenuHorizontalStyle? overflowStyle = null)
{
- return AddToggle(text, defaultValue, onToggle, IMenuTextSize.Medium);
+ return AddToggle(text, defaultValue, onToggle, IMenuTextSize.Medium, overflowStyle);
}
- public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new SliderMenuButton(text, min, max, defaultValue, step, onChange, size));
+ _menu!.Options.Add(new SliderMenuButton(text, min, max, defaultValue, step, onChange, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new SliderMenuButton(text, min, max, defaultValue, step, onChange, size));
+ _menu!.Options.Add(new SliderMenuButton(text, min, max, defaultValue, step, onChange, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange)
+ public IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange, MenuHorizontalStyle? overflowStyle = null)
{
- return AddSlider(text, min, max, defaultValue, step, onChange, IMenuTextSize.Medium);
+ return AddSlider(text, min, max, defaultValue, step, onChange, IMenuTextSize.Medium, overflowStyle);
}
- public IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new AsyncButtonMenuOption(text, onClickAsync, size));
+ _menu!.Options.Add(new AsyncButtonMenuOption(text, onClickAsync, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new AsyncButtonMenuOption(text, onClickAsync, size));
+ _menu!.Options.Add(new AsyncButtonMenuOption(text, onClickAsync, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddAsyncButton(string text, Func onClickAsync)
+ public IMenuBuilder AddAsyncButton(string text, Func onClickAsync, MenuHorizontalStyle? overflowStyle = null)
{
- return AddAsyncButton(text, onClickAsync, IMenuTextSize.Medium);
+ return AddAsyncButton(text, onClickAsync, IMenuTextSize.Medium, overflowStyle);
}
- public IMenuBuilder AddText(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddText(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new TextMenuOption(text, alignment, size));
+ _menu!.Options.Add(new TextMenuOption(text, alignment, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
@@ -128,41 +131,45 @@ public IMenuBuilder AddSubmenu(string text, Func submenuBuilder)
{
return AddSubmenu(text, submenuBuilder, IMenuTextSize.Medium);
}
- public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium)
+
+ public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ChoiceMenuOption(text, choices, defaultChoice, onChange, size));
+ _menu!.Options.Add(new ChoiceMenuOption(text, choices, defaultChoice, onChange, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium)
+ public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ChoiceMenuOption(text, choices, defaultChoice, onChange, size));
+ _menu!.Options.Add(new ChoiceMenuOption(text, choices, defaultChoice, onChange, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange)
- {
- return AddChoice(text, choices, defaultChoice, onChange, IMenuTextSize.Medium);
- }
+ // [Obsolete("This overload causes ambiguity. Use AddChoice(text, choices, defaultChoice, onChange, IMenuTextSize.Medium, overflowStyle) instead.")]
+ // public IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, MenuHorizontalStyle? overflowStyle = null)
+ // {
+ // return AddChoice(text, choices, defaultChoice, onChange, IMenuTextSize.Medium, overflowStyle);
+ // }
+
public IMenuBuilder AddSeparator()
{
_menu!.Options.Add(new SeparatorMenuOption());
_menu!.Options[^1].Menu = _menu;
return this;
}
- public IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium)
+
+ public IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- _menu!.Options.Add(new ProgressBarMenuOption(text, progressProvider, barWidth, size));
+ _menu!.Options.Add(new ProgressBarMenuOption(text, progressProvider, barWidth, size, overflowStyle));
_menu!.Options[^1].Menu = _menu;
_menu!.RenderOntick = true;
return this;
}
- public IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth)
+ public IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth, MenuHorizontalStyle? overflowStyle = null)
{
- return AddProgressBar(text, progressProvider, barWidth, IMenuTextSize.Medium);
+ return AddProgressBar(text, progressProvider, barWidth, IMenuTextSize.Medium, overflowStyle);
}
public IMenuBuilder WithParent(IMenu parent)
{
@@ -286,28 +293,35 @@ public IMenuBuilder OverrideButtons(Action configureOverri
return this;
}
+ [Obsolete("Use Design.OverrideSelectButton instead")]
public IMenuBuilder OverrideSelectButton(params string[] buttonNames)
{
- _menu!.ButtonOverrides.Select = MenuButtonOverrides.ParseButtons(buttonNames);
+ _menu!.ButtonOverrides!.Select = MenuButtonOverrides.ParseButtons(buttonNames);
return this;
}
+ [Obsolete("Use Design.OverrideMoveButton instead")]
public IMenuBuilder OverrideMoveButton(params string[] buttonNames)
{
- _menu!.ButtonOverrides.Move = MenuButtonOverrides.ParseButtons(buttonNames);
+ _menu!.ButtonOverrides!.Move = MenuButtonOverrides.ParseButtons(buttonNames);
return this;
}
-
+ [Obsolete("Use Design.OverrideExitButton instead")]
public IMenuBuilder OverrideExitButton(params string[] buttonNames)
{
- _menu!.ButtonOverrides.Exit = MenuButtonOverrides.ParseButtons(buttonNames);
+ _menu!.ButtonOverrides!.Exit = MenuButtonOverrides.ParseButtons(buttonNames);
return this;
}
+ [Obsolete("Use Design.MaxVisibleItems instead")]
public IMenuBuilder MaxVisibleItems(int count)
{
- _menu!.MaxVisibleOptions = Math.Max(1, count);
+ if (count < 1 || count > 5)
+ {
+ Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(count), $"MaxVisibleItems: value {count} is out of range [1, 5]."));
+ }
+ _menu!.MaxVisibleOptions = Math.Clamp(count, 1, 5);
return this;
}
@@ -329,6 +343,7 @@ public IMenuBuilder HasSound(bool hasSound)
return this;
}
+ [Obsolete("Use Design.SetColor instead")]
public IMenuBuilder SetColor(Color color)
{
_menu!.RenderColor = color;
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesign.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesign.cs
new file mode 100644
index 000000000..4c5aafc9e
--- /dev/null
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuDesign.cs
@@ -0,0 +1,60 @@
+using SwiftlyS2.Shared.Menus;
+using SwiftlyS2.Shared.Natives;
+
+namespace SwiftlyS2.Core.Menus;
+
+internal sealed class MenuDesign : IMenuDesign
+{
+ private readonly IMenu _menu;
+
+ public MenuDesign(IMenu menu)
+ {
+ _menu = menu;
+ }
+
+ public IMenuDesign OverrideSelectButton(params string[] buttonNames)
+ {
+ _menu.ButtonOverrides!.Select = MenuButtonOverrides.ParseButtons(buttonNames);
+ return this;
+ }
+
+ public IMenuDesign OverrideMoveButton(params string[] buttonNames)
+ {
+ _menu.ButtonOverrides!.Move = MenuButtonOverrides.ParseButtons(buttonNames);
+ return this;
+ }
+
+ public IMenuDesign OverrideExitButton(params string[] buttonNames)
+ {
+ _menu.ButtonOverrides!.Exit = MenuButtonOverrides.ParseButtons(buttonNames);
+ return this;
+ }
+
+ public IMenuDesign MaxVisibleItems(int count)
+ {
+ if (count < 1 || count > 5)
+ {
+ Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(count), $"MaxVisibleItems: value {count} is out of range [1, 5]."));
+ }
+ _menu.MaxVisibleOptions = Math.Clamp(count, 1, 5);
+ return this;
+ }
+
+ public IMenuDesign SetColor(Color color)
+ {
+ _menu.RenderColor = color;
+ return this;
+ }
+
+ public IMenuDesign SetVerticalScrollStyle(MenuVerticalScrollStyle style)
+ {
+ _menu.VerticalScrollStyle = style;
+ return this;
+ }
+
+ public IMenuDesign SetGlobalHorizontalStyle(MenuHorizontalStyle style)
+ {
+ _menu.HorizontalStyle = style;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManager.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManager.cs
index 4237b3693..f352a3320 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuManager.cs
@@ -79,18 +79,17 @@ public MenuManager(ISwiftlyCore core)
_exitSound.Volume = Settings.SoundExitVolume;
_Core.Event.OnClientKeyStateChanged += KeyStateChange;
+ _Core.Event.OnClientDisconnected += OnClientDisconnected;
+ _Core.Event.OnMapUnload += OnMapUnload;
}
~MenuManager()
{
- foreach (var kvp in OpenMenus)
- {
- var player = kvp.Key;
- var menu = kvp.Value;
- menu.Close(player);
- }
+ CloseAllMenus();
_Core.Event.OnClientKeyStateChanged -= KeyStateChange;
+ _Core.Event.OnClientDisconnected -= OnClientDisconnected;
+ _Core.Event.OnMapUnload -= OnMapUnload;
}
void KeyStateChange(IOnClientKeyStateChangedEvent @event)
@@ -202,6 +201,47 @@ void KeyStateChange(IOnClientKeyStateChangedEvent @event)
}
}
+ public void OnClientDisconnected(IOnClientDisconnectedEvent @event)
+ {
+ var player = _Core.PlayerManager.GetPlayer(@event.PlayerId);
+ if (player == null)
+ {
+ return;
+ }
+
+ if (OpenMenus.TryRemove(player, out var menu))
+ {
+ var currentMenu = menu;
+ while (currentMenu != null)
+ {
+ currentMenu.Close(player);
+ OnMenuClosed?.Invoke(player, currentMenu);
+ currentMenu = currentMenu.Parent;
+ }
+ }
+ }
+
+ public void OnMapUnload(IOnMapUnloadEvent _)
+ {
+ CloseAllMenus();
+ }
+
+ public void CloseAllMenus()
+ {
+ foreach (var kvp in OpenMenus)
+ {
+ var player = kvp.Key;
+ var currentMenu = kvp.Value;
+ while (currentMenu != null)
+ {
+ currentMenu.Close(player);
+ OnMenuClosed?.Invoke(player, currentMenu);
+ currentMenu = currentMenu.Parent;
+ }
+ }
+ OpenMenus.Clear();
+ }
+
public void CloseMenu(IMenu menu)
{
foreach (var kvp in OpenMenus)
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/AsyncButtonMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/AsyncButtonMenuOption.cs
index 0400cc9e2..626169c69 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/AsyncButtonMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/AsyncButtonMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
@@ -16,6 +16,7 @@ internal class AsyncButtonMenuOption : IOption
public IMenuTextSize Size { get; set; }
public bool CloseOnSelect { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
@@ -23,18 +24,20 @@ internal class AsyncButtonMenuOption : IOption
private string? _loadingText;
- public AsyncButtonMenuOption(string text, Func? onClickAsync = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public AsyncButtonMenuOption(string text, Func? onClickAsync = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
OnClickAsync = onClickAsync;
Size = size;
+ OverflowStyle = overflowStyle;
}
- public AsyncButtonMenuOption(string text, Func? onClickAsync, IMenuTextSize size = IMenuTextSize.Medium)
+ public AsyncButtonMenuOption(string text, Func? onClickAsync, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
OnClickAsyncWithOption = onClickAsync;
Size = size;
+ OverflowStyle = overflowStyle;
}
public bool ShouldShow(IPlayer player)
@@ -56,12 +59,13 @@ public string GetDisplayText(IPlayer player)
return $"{_loadingText ?? "Loading..."}";
}
+ var text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text;
if (!CanInteract(player))
{
- return $"{Text}";
+ return $"{text}";
}
- return $"{Text}";
+ return $"{text}";
}
public IMenuTextSize GetTextSize()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ButtonMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ButtonMenuOption.cs
index d2a03abbf..86afbe890 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ButtonMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ButtonMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
@@ -16,42 +16,50 @@ internal class ButtonMenuOption : IOption
public IMenuTextSize Size { get; set; }
public bool CloseOnSelect { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
- public ButtonMenuOption(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public ButtonMenuOption(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
OnClick = onClick;
Size = size;
+ OverflowStyle = overflowStyle;
}
- public ButtonMenuOption(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium)
+ public ButtonMenuOption(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
OnClickWithOption = onClick;
Size = size;
+ OverflowStyle = overflowStyle;
}
+
public bool ShouldShow(IPlayer player)
{
return VisibilityCheck?.Invoke(player) ?? true;
}
+
public bool CanInteract(IPlayer player)
{
return EnabledCheck?.Invoke(player) ?? true;
}
+
public string GetDisplayText(IPlayer player)
{
var sizeClass = MenuSizeHelper.GetSizeClass(Size);
+ var text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text;
if (!CanInteract(player))
{
- return $"{Text}";
+ return $"{text}";
}
- return $"{Text}";
+ return $"{text}";
}
+
public IMenuTextSize GetTextSize()
{
return Size;
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ChoiceMenuButton.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ChoiceMenuButton.cs
index da0fb00a1..798fd42e5 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ChoiceMenuButton.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ChoiceMenuButton.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.SchemaDefinitions;
@@ -16,13 +16,14 @@ internal class ChoiceMenuOption : IOption
public Func? EnabledCheck { get; set; }
public IMenuTextSize Size { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
public string SelectedChoice => Choices.Count > 0 ? Choices[SelectedIndex] : "";
- public ChoiceMenuOption(string text, IEnumerable choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public ChoiceMenuOption(string text, IEnumerable choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Choices = [.. choices];
@@ -36,9 +37,10 @@ public ChoiceMenuOption(string text, IEnumerable choices, string? defaul
}
OnChange = onChange;
+ OverflowStyle = overflowStyle;
}
- public ChoiceMenuOption(string text, IEnumerable choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium)
+ public ChoiceMenuOption(string text, IEnumerable choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Choices = [.. choices];
@@ -52,6 +54,7 @@ public ChoiceMenuOption(string text, IEnumerable choices, string? defaul
}
OnChangeWithOption = onChange;
+ OverflowStyle = overflowStyle;
}
public bool ShouldShow(IPlayer player)
@@ -70,11 +73,12 @@ public string GetDisplayText(IPlayer player)
var choice = $"[{SelectedChoice}]";
+ var text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text;
if (!CanInteract(player))
{
- return $"{Text}: {choice}";
+ return $"{text}: {choice}";
}
- return $"{Text}: {choice}";
+ return $"{text}: {choice}";
}
public IMenuTextSize GetTextSize()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/DynamicMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/DynamicMenuOption.cs
index 737b5de8e..ca1d401b1 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/DynamicMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/DynamicMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.SchemaDefinitions;
@@ -19,6 +19,7 @@ internal class DynamicMenuOption : IOption
private IMenuTextSize _size;
private bool _closeOnSelect;
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public string Text
{
@@ -68,7 +69,7 @@ public string GetDisplayText(IPlayer player)
if (oldText != _cachedText && Menu != null)
{
- Menu.Rerender(player);
+ Menu.Rerender(player, true);
}
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ProgressBarMenuButton.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ProgressBarMenuButton.cs
index 3bfa16a25..7ca667407 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ProgressBarMenuButton.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ProgressBarMenuButton.cs
@@ -1,10 +1,10 @@
-using SwiftlyS2.Shared.Players;
+using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Core.Menus;
namespace SwiftlyS2.Core.Menu.Options;
-internal class ProgressBarMenuOption(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium) : IOption
+internal class ProgressBarMenuOption(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null) : IOption
{
public string Text { get; set; } = text;
public Func ProgressProvider { get; set; } = progressProvider;
@@ -14,6 +14,7 @@ internal class ProgressBarMenuOption(string text, Func progressProvider,
public bool ShowPercentage { get; set; } = true;
public IMenuTextSize Size { get; set; } = size;
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; } = overflowStyle;
public bool Visible => true;
public bool Enabled => false;
@@ -36,7 +37,7 @@ public string GetDisplayText(IPlayer player)
bar += $"{EmptyChar}";
var percentage = ShowPercentage ? $" {(int)(progress * 100)}%" : "";
- return $"{Text}: {bar}{percentage}";
+ return $"{((Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text)}: {bar}{percentage}";
}
public IMenuTextSize GetTextSize()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SeparatorMenuButton.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SeparatorMenuButton.cs
index 87a3632cd..d0936b2fe 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SeparatorMenuButton.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SeparatorMenuButton.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Shared.Players;
+using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.Menus;
namespace SwiftlyS2.Core.Menu.Options;
@@ -10,6 +10,7 @@ internal class SeparatorMenuOption : IOption
public bool Enabled => false;
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public SeparatorMenuOption()
{
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SliderMenuButton.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SliderMenuButton.cs
index 8952c2680..d5dbeb7d8 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SliderMenuButton.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SliderMenuButton.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.SchemaDefinitions;
@@ -18,11 +18,12 @@ internal class SliderMenuButton : IOption
public Func? EnabledCheck { get; set; }
public IMenuTextSize Size { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
- public SliderMenuButton(string text, float min = 0, float max = 10, float defaultValue = 5, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public SliderMenuButton(string text, float min = 0, float max = 10, float defaultValue = 5, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Min = min;
@@ -31,9 +32,10 @@ public SliderMenuButton(string text, float min = 0, float max = 10, float defaul
Step = step;
OnChange = onChange;
Size = size;
+ OverflowStyle = overflowStyle;
}
- public SliderMenuButton(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium)
+ public SliderMenuButton(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Min = min;
@@ -42,6 +44,7 @@ public SliderMenuButton(string text, float min, float max, float defaultValue, f
Step = step;
OnChangeWithOption = onChange;
Size = size;
+ OverflowStyle = overflowStyle;
}
public bool ShouldShow(IPlayer player)
@@ -72,12 +75,13 @@ public string GetDisplayText(IPlayer player)
}
slider += $") {Value:F1}";
+ var text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text;
if (!CanInteract(player))
{
- return $"{Text}: {slider}";
+ return $"{text}: {slider}";
}
- return $"{Text}: {slider}";
+ return $"{text}: {slider}";
}
public IMenuTextSize GetTextSize()
@@ -87,7 +91,7 @@ public IMenuTextSize GetTextSize()
private static float Wrap(float value, float min, float max)
{
- float range = max - min + 1;
+ float range = max - min;
return ((value - min) % range + range) % range + min;
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SubmenuMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SubmenuMenuOption.cs
index 9d337d034..6321bf997 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SubmenuMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/SubmenuMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
@@ -13,6 +13,7 @@ internal class SubmenuMenuOption : IOption
public Func? EnabledCheck { get; set; }
public IMenuTextSize Size { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/TextMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/TextMenuOption.cs
index bc7e0cf24..565f39cd1 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/TextMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/TextMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
@@ -12,23 +12,26 @@ internal class TextMenuOption : IOption
public Func? VisibilityCheck { get; set; }
public Func? DynamicText { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => false;
- public TextMenuOption(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium)
+ public TextMenuOption(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Alignment = alignment;
Size = size;
+ OverflowStyle = overflowStyle;
}
- public TextMenuOption(Func dynamicText, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium)
+ public TextMenuOption(Func dynamicText, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
- Text = "";
+ Text = string.Empty;
DynamicText = dynamicText;
Alignment = alignment;
Size = size;
+ OverflowStyle = overflowStyle;
}
public bool ShouldShow(IPlayer player)
@@ -45,6 +48,8 @@ public string GetDisplayText(IPlayer player)
{
var text = DynamicText?.Invoke() ?? Text;
+ text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(text, OverflowStyle) ?? text;
+
var sizeClass = MenuSizeHelper.GetSizeClass(Size);
text = $"{text}";
diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ToggleMenuOption.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ToggleMenuOption.cs
index b0ae040d0..d488af446 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ToggleMenuOption.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Menus/Options/ToggleMenuOption.cs
@@ -1,4 +1,4 @@
-using SwiftlyS2.Core.Menus;
+using SwiftlyS2.Core.Menus;
using SwiftlyS2.Shared.Menus;
using SwiftlyS2.Shared.Players;
@@ -17,24 +17,27 @@ internal class ToggleMenuOption : IOption
public IMenuTextSize Size { get; set; }
public bool CloseOnSelect { get; set; }
public IMenu? Menu { get; set; }
+ public MenuHorizontalStyle? OverflowStyle { get; init; }
public bool Visible => true;
public bool Enabled => true;
- public ToggleMenuOption(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium)
+ public ToggleMenuOption(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Value = defaultValue;
OnToggle = onToggle;
Size = size;
+ OverflowStyle = overflowStyle;
}
- public ToggleMenuOption(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium)
+ public ToggleMenuOption(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null)
{
Text = text;
Value = defaultValue;
OnToggleWithOption = onToggle;
Size = size;
+ OverflowStyle = overflowStyle;
}
public bool ShouldShow(IPlayer player)
@@ -53,12 +56,13 @@ public string GetDisplayText(IPlayer player)
var status = Value ? "✔" : "✘";
+ var text = (Menu as Menus.Menu)?.ApplyHorizontalStyle(Text, OverflowStyle) ?? Text;
if (!CanInteract(player))
{
- return $"{Text}: {status}/";
+ return $"{text}: {status}/";
}
- return $"{Text}: {status}";
+ return $"{text}: {status}";
}
public IMenuTextSize GetTextSize()
diff --git a/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs b/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
index dc96a22f7..c7d4dc906 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Players/Player.cs
@@ -11,28 +11,28 @@ namespace SwiftlyS2.Core.Players;
internal class Player : IPlayer
{
- private int _pid;
-
public Player(int pid)
{
- _pid = pid;
+ Slot = pid;
}
- public int PlayerID => _pid;
+ public int PlayerID => Slot;
+
+ public int Slot { get; }
- public bool IsFakeClient => NativePlayer.IsFakeClient(_pid);
+ public bool IsFakeClient => NativePlayer.IsFakeClient(Slot);
- public bool IsAuthorized => NativePlayer.IsAuthorized(_pid);
+ public bool IsAuthorized => NativePlayer.IsAuthorized(Slot);
- public uint ConnectedTime => NativePlayer.GetConnectedTime(_pid);
+ public uint ConnectedTime => NativePlayer.GetConnectedTime(Slot);
- public Language PlayerLanguage => new(NativePlayer.GetLanguage(_pid));
+ public Language PlayerLanguage => new(NativePlayer.GetLanguage(Slot));
- public ulong SteamID => NativePlayer.GetSteamID(_pid);
+ public ulong SteamID => NativePlayer.GetSteamID(Slot);
- public ulong UnauthorizedSteamID => NativePlayer.GetUnauthorizedSteamID(_pid);
+ public ulong UnauthorizedSteamID => NativePlayer.GetUnauthorizedSteamID(Slot);
- public CCSPlayerController Controller => new CCSPlayerControllerImpl(NativePlayer.GetController(_pid));
+ public CCSPlayerController Controller => new CCSPlayerControllerImpl(NativePlayer.GetController(Slot));
public CCSPlayerController RequiredController => Controller is { IsValid: true } controller ? controller : throw new InvalidOperationException("Controller is not valid");
@@ -44,11 +44,11 @@ public Player(int pid)
public CCSPlayerPawn RequiredPlayerPawn => PlayerPawn is { IsValid: true } pawn ? pawn : throw new InvalidOperationException("PlayerPawn is not valid");
- public GameButtonFlags PressedButtons => (GameButtonFlags)NativePlayer.GetPressedButtons(_pid);
+ public GameButtonFlags PressedButtons => (GameButtonFlags)NativePlayer.GetPressedButtons(Slot);
- public string IPAddress => NativePlayer.GetIPAddress(_pid);
+ public string IPAddress => NativePlayer.GetIPAddress(Slot);
- public VoiceFlagValue VoiceFlags { get => (VoiceFlagValue)NativeVoiceManager.GetClientVoiceFlags(_pid); set => NativeVoiceManager.SetClientVoiceFlags(_pid, (int)value); }
+ public VoiceFlagValue VoiceFlags { get => (VoiceFlagValue)NativeVoiceManager.GetClientVoiceFlags(Slot); set => NativeVoiceManager.SetClientVoiceFlags(Slot, (int)value); }
public bool IsValid =>
Controller is { IsValid: true, IsHLTV: false, Connected: PlayerConnectedState.PlayerConnected } &&
@@ -58,60 +58,60 @@ public Player(int pid)
public void ChangeTeam(Team team)
{
- NativePlayer.ChangeTeam(_pid, (byte)team);
+ NativePlayer.ChangeTeam(Slot, (byte)team);
}
public void ClearTransmitEntityBlocks()
{
- NativePlayer.ClearTransmitEntityBlocked(_pid);
+ NativePlayer.ClearTransmitEntityBlocked(Slot);
}
public ListenOverride GetListenOverride(int player)
{
- return (ListenOverride)NativeVoiceManager.GetClientListenOverride(_pid, player);
+ return (ListenOverride)NativeVoiceManager.GetClientListenOverride(Slot, player);
}
public bool IsTransmitEntityBlocked(int entityid)
{
- return NativePlayer.IsTransmitEntityBlocked(_pid, entityid);
+ return NativePlayer.IsTransmitEntityBlocked(Slot, entityid);
}
public void Kick(string reason, ENetworkDisconnectionReason gameReason)
{
- NativePlayer.Kick(_pid, reason, (int)gameReason);
+ NativePlayer.Kick(Slot, reason, (int)gameReason);
}
public void SendMessage(MessageType kind, string message)
{
- NativePlayer.SendMessage(_pid, (int)kind, message);
+ NativePlayer.SendMessage(Slot, (int)kind, message);
}
public void SetListenOverride(int player, ListenOverride listenOverride)
{
- NativeVoiceManager.SetClientListenOverride(_pid, player, (int)listenOverride);
+ NativeVoiceManager.SetClientListenOverride(Slot, player, (int)listenOverride);
}
public void ShouldBlockTransmitEntity(int entityid, bool shouldBlockTransmit)
{
- NativePlayer.ShouldBlockTransmitEntity(_pid, entityid, shouldBlockTransmit);
+ NativePlayer.ShouldBlockTransmitEntity(Slot, entityid, shouldBlockTransmit);
}
public void SwitchTeam(Team team)
{
- NativePlayer.SwitchTeam(_pid, (byte)team);
+ NativePlayer.SwitchTeam(Slot, (byte)team);
}
public void TakeDamage(CTakeDamageInfo damageInfo)
{
unsafe
{
- NativePlayer.TakeDamage(_pid, (nint)(&damageInfo));
+ NativePlayer.TakeDamage(Slot, (nint)(&damageInfo));
}
}
public void Teleport(Vector pos, QAngle angle, Vector velocity)
{
- NativePlayer.Teleport(_pid, pos, angle, velocity);
+ NativePlayer.Teleport(Slot, pos, angle, velocity);
}
public void Respawn()
@@ -121,13 +121,12 @@ public void Respawn()
public void ExecuteCommand(string command)
{
- NativePlayer.ExecuteCommand(_pid, command);
+ NativePlayer.ExecuteCommand(Slot, command);
}
public bool Equals(IPlayer? other)
{
- if (other is null) return false;
- return PlayerID == other.PlayerID;
+ return other is not null && PlayerID == other.PlayerID;
}
public override bool Equals(object? obj)
@@ -142,12 +141,8 @@ public override int GetHashCode()
public static bool operator ==(Player? left, Player? right)
{
- if (left is null) return right is null;
- return left.Equals(right);
+ return left is not null && right is not null && left.Equals(right);
}
- public static bool operator !=(Player? left, Player? right)
- {
- return !(left == right);
- }
+ public static bool operator !=(Player left, Player right) => !(left == right);
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
index 6a8d04191..0afd021fc 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginContext.cs
@@ -7,8 +7,8 @@
namespace SwiftlyS2.Core.Plugins;
-internal class PluginContext : IDisposable {
-
+internal class PluginContext : IDisposable
+{
public SwiftlyCore? Core { get; set; }
public PluginMetadata? Metadata { get; set; }
@@ -20,10 +20,11 @@ internal class PluginContext : IDisposable {
public PluginLoader? Loader { get; set; }
- public void Dispose() {
+ public void Dispose()
+ {
Plugin?.Unload();
Loader?.Dispose();
+ Core?.MenuManager?.CloseAllMenus();
Core?.Dispose();
}
-
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
index b69d2b2d0..bc19dbb98 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Plugins/PluginManager.cs
@@ -397,7 +397,7 @@ private void RebuildSharedServices()
return context;
}
- public void UnloadPlugin(string id)
+ public bool UnloadPlugin(string id)
{
var context = _Plugins
.Where(p => p.Status == PluginStatus.Loaded)
@@ -405,14 +405,15 @@ public void UnloadPlugin(string id)
if (context == null)
{
_Logger.LogWarning("Plugin not found or not loaded: " + id);
- return;
+ return false;
}
context.Dispose();
context.Status = PluginStatus.Unloaded;
+ return true;
}
- public void LoadPluginById(string id)
+ public bool LoadPluginById(string id)
{
var context = _Plugins
.Where(p => p.Status == PluginStatus.Unloaded)
@@ -433,7 +434,7 @@ public void LoadPluginById(string id)
if (context == null)
{
_Logger.LogWarning("Plugin not found: " + id);
- return;
+ return false;
}
}
else
@@ -444,15 +445,22 @@ public void LoadPluginById(string id)
}
RebuildSharedServices();
+ return true;
}
public void ReloadPlugin(string id)
{
_Logger.LogInformation("Reloading plugin " + id);
- UnloadPlugin(id);
- LoadPluginById(id);
- RebuildSharedServices();
+ if (!UnloadPlugin(id))
+ {
+ return;
+ }
+
+ if (!LoadPluginById(id))
+ {
+ RebuildSharedServices();
+ }
_Logger.LogInformation("Reloaded plugin " + id);
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseEntityImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseEntityImpl.cs
index f151233a2..6c3cdf4e0 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseEntityImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CBaseEntityImpl.cs
@@ -10,7 +10,7 @@ public CEntitySubclassVDataBase VData
{
get
{
- return new CEntitySubclassVDataBaseImpl((nint)NativeSchema.GetVData(_Handle));
+ return new CEntitySubclassVDataBaseImpl(NativeSchema.GetVData(_Handle));
}
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
index 34da97ca7..0df411f07 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstance.cs
@@ -35,7 +35,7 @@ public partial interface CEntityInstance {
/// Input value.
/// Activator entity. Nullable.
/// Caller entity. Nullable.
- /// Delay in seconds.
+ /// Delay in seconds.x
public void AddEntityIOEvent(string input, T value, CEntityInstance? activator = null, CEntityInstance? caller = null, float delay = 0f);
///
@@ -44,6 +44,25 @@ public partial interface CEntityInstance {
/// Entity key values. Nullable.
public void DispatchSpawn( CEntityKeyValues? entityKV = null );
+ ///
+ /// Set the transmit state of the entity for one player.
+ ///
+ /// Whether the entity should be transmitting.
+ /// The player ID to set the transmit state for.
+ public void SetTransmitState( bool transmitting , int playerId );
+
+ ///
+ /// Set the global transmit state of the entity.
+ ///
+ /// Whether the entity should be transmitting.
+ public void SetTransmitState( bool transmitting );
+
+ ///
+ /// Check if the entity is transmitting for one player.
+ ///
+ /// The player ID to check the transmit state for.
+ public bool IsTransmitting( int playerId );
+
///
/// Despawn the entity.
public void Despawn();
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
index eb4046c9c..097ff1615 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CEntityInstanceImpl.cs
@@ -75,6 +75,21 @@ public void AddEntityIOEvent(string input, T value, CEntityInstance? activato
}
}
+ public void SetTransmitState( bool transmitting , int playerId ) {
+ NativePlayer.ShouldBlockTransmitEntity(playerId, (int)Index, transmitting);
+
+ }
+
+ public void SetTransmitState( bool transmitting )
+ {
+ NativePlayerManager.ShouldBlockTransmitEntity((int)Index, transmitting);
+ }
+
+ public bool IsTransmitting( int playerId )
+ {
+ return NativePlayer.IsTransmitEntityBlocked(playerId, (int)Index);
+ }
+
public void DispatchSpawn(CEntityKeyValues? entityKV = null) {
NativeEntitySystem.Spawn(Address, entityKV?.Address ?? nint.Zero);
}
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServices.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServices.cs
index c0ff025f1..4f73b22f9 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServices.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServices.cs
@@ -19,6 +19,13 @@ public partial interface CPlayer_ItemServices {
/// The item that was given.
public T GiveItem(string itemDesignerName) where T : ISchemaClass;
+ ///
+ /// Give an item to the player.
+ ///
+ /// The designer name of the item to give.
+ public void GiveItem(string itemDesignerName);
+
+
///
/// Drop the item that player is holding.
///
diff --git a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServicesImpl.cs b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServicesImpl.cs
index 4769ef756..eaec2c16e 100644
--- a/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServicesImpl.cs
+++ b/managed/src/SwiftlyS2.Core/Modules/Schemas/Extensions/CPlayer_ItemServicesImpl.cs
@@ -18,6 +18,10 @@ public T GiveItem(string itemDesignerName) where T : ISchemaClass {
return T.From(GameFunctions.CCSPlayer_ItemServices_GiveNamedItem(Address, itemDesignerName));
}
+ public void GiveItem(string itemDesignerName) {
+ GameFunctions.CCSPlayer_ItemServices_GiveNamedItem(Address, itemDesignerName);
+ }
+
public void RemoveItems() {
GameFunctions.CCSPlayer_ItemServices_RemoveWeapons(Address);
}
diff --git a/managed/src/SwiftlyS2.Core/Natives/GameFunctions.cs b/managed/src/SwiftlyS2.Core/Natives/GameFunctions.cs
index 0962df5b2..6a2a6418c 100644
--- a/managed/src/SwiftlyS2.Core/Natives/GameFunctions.cs
+++ b/managed/src/SwiftlyS2.Core/Natives/GameFunctions.cs
@@ -2,18 +2,20 @@
using System.Text;
using Spectre.Console;
using SwiftlyS2.Shared.Natives;
+using SwiftlyS2.Shared.SchemaDefinitions;
namespace SwiftlyS2.Core.Natives;
internal static class GameFunctions
{
- public static unsafe delegate* unmanaged pCTakeDamageInfo_Constructor;
- public static unsafe delegate* unmanaged pTraceShape;
- public static unsafe delegate* unmanaged pTracePlayerBBox;
- public static unsafe delegate* unmanaged pSetModel;
- public static unsafe delegate* unmanaged pSetPlayerControllerPawn;
- public static unsafe delegate* unmanaged pSetOrAddAttribute;
- public static unsafe delegate* unmanaged pGetWeaponCSDataFromKey;
+ public static unsafe delegate* unmanaged< CTakeDamageInfo*, nint, nint, nint, Vector*, Vector*, float, int, int, void*, void > pCTakeDamageInfo_Constructor;
+ public static unsafe delegate* unmanaged< nint, Ray_t*, Vector, Vector, CTraceFilter*, CGameTrace*, void > pTraceShape;
+ public static unsafe delegate* unmanaged< Vector, Vector, BBox_t, CTraceFilter*, CGameTrace*, void > pTracePlayerBBox;
+ public static unsafe delegate* unmanaged< nint, IntPtr, void > pSetModel;
+ public static unsafe delegate* unmanaged< nint, nint, byte, byte, byte, byte, void > pSetPlayerControllerPawn;
+ public static unsafe delegate* unmanaged< nint, nint, float, void > pSetOrAddAttribute;
+ public static unsafe delegate* unmanaged< int, nint, nint > pGetWeaponCSDataFromKey;
+ public static unsafe delegate* unmanaged< nint, uint, nint, byte, CUtlSymbolLarge, byte, int, nint, nint, void > pDispatchParticleEffect;
public static int TeleportOffset => NativeOffsets.Fetch("CBaseEntity::Teleport");
public static int CommitSuicideOffset => NativeOffsets.Fetch("CBasePlayerPawn::CommitSuicide");
public static int GetSkeletonInstanceOffset => NativeOffsets.Fetch("CGameSceneNode::GetSkeletonInstance");
@@ -31,17 +33,42 @@ public static void Initialize()
{
unsafe
{
- pCTakeDamageInfo_Constructor = (delegate* unmanaged)NativeSignatures.Fetch("CTakeDamageInfo::Constructor");
- pTraceShape = (delegate* unmanaged)NativeSignatures.Fetch("TraceShape");
- pTracePlayerBBox = (delegate* unmanaged)NativeSignatures.Fetch("TracePlayerBBox");
- pSetModel = (delegate* unmanaged)NativeSignatures.Fetch("CBaseModelEntity::SetModel");
- pSetPlayerControllerPawn = (delegate* unmanaged)NativeSignatures.Fetch("CBasePlayerController::SetPawn");
- pSetOrAddAttribute = (delegate* unmanaged)NativeSignatures.Fetch("CAttributeList::SetOrAddAttributeValueByName");
- pGetWeaponCSDataFromKey = (delegate* unmanaged)NativeSignatures.Fetch("GetWeaponCSDataFromKey");
+ pCTakeDamageInfo_Constructor = (delegate* unmanaged< CTakeDamageInfo*, nint, nint, nint, Vector*, Vector*, float, int, int, void*, void >)NativeSignatures.Fetch("CTakeDamageInfo::Constructor");
+ pTraceShape = (delegate* unmanaged< nint, Ray_t*, Vector, Vector, CTraceFilter*, CGameTrace*, void >)NativeSignatures.Fetch("TraceShape");
+ pTracePlayerBBox = (delegate* unmanaged< Vector, Vector, BBox_t, CTraceFilter*, CGameTrace*, void >)NativeSignatures.Fetch("TracePlayerBBox");
+ pSetModel = (delegate* unmanaged< nint, IntPtr, void >)NativeSignatures.Fetch("CBaseModelEntity::SetModel");
+ pSetPlayerControllerPawn = (delegate* unmanaged< nint, nint, byte, byte, byte, byte, void >)NativeSignatures.Fetch("CBasePlayerController::SetPawn");
+ pSetOrAddAttribute = (delegate* unmanaged< nint, IntPtr, float, void >)NativeSignatures.Fetch("CAttributeList::SetOrAddAttributeValueByName");
+ pGetWeaponCSDataFromKey = (delegate* unmanaged< int, nint, nint >)NativeSignatures.Fetch("GetWeaponCSDataFromKey");
+ pDispatchParticleEffect = (delegate* unmanaged< nint, uint, nint, byte, CUtlSymbolLarge, byte, int, nint, nint, void >)NativeSignatures.Fetch("DispatchParticleEffect");
}
}
- public unsafe static nint GetWeaponCSDataFromKey(int unknown, string key)
+ public unsafe static void DispatchParticleEffect( string particleName, uint attachmentType, nint entity, byte attachmentPoint, CUtlSymbolLarge attachmentName, bool resetAllParticlesOnEntity, int splitScreenSlot, CRecipientFilter filter )
+ {
+ try
+ {
+ unsafe
+ {
+ var pool = ArrayPool.Shared;
+ var nameLength = Encoding.UTF8.GetByteCount(particleName);
+ var nameBuffer = pool.Rent(nameLength + 1);
+ _ = Encoding.UTF8.GetBytes(particleName, nameBuffer);
+ nameBuffer[nameLength] = 0;
+ fixed (byte* pParticleName = nameBuffer)
+ {
+ pDispatchParticleEffect((nint)pParticleName, attachmentType, entity, attachmentPoint, attachmentName, (byte)(resetAllParticlesOnEntity ? 1 : 0), splitScreenSlot, (nint)(&filter), IntPtr.Zero);
+ pool.Return(nameBuffer);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ AnsiConsole.WriteException(e);
+ }
+ }
+
+ public unsafe static nint GetWeaponCSDataFromKey( int unknown, string key )
{
try
{
@@ -65,14 +92,14 @@ public unsafe static nint GetWeaponCSDataFromKey(int unknown, string key)
}
}
- public unsafe static nint FindPickerEntity(nint handle, nint controller)
+ public unsafe static nint FindPickerEntity( nint handle, nint controller )
{
try
{
unsafe
{
void*** ppVTable = (void***)handle;
- var pFindPickerEntity = (delegate* unmanaged)ppVTable[0][FindPickerEntityOffset];
+ var pFindPickerEntity = (delegate* unmanaged< nint, nint, nint, nint >)ppVTable[0][FindPickerEntityOffset];
return pFindPickerEntity(handle, controller, IntPtr.Zero);
}
}
@@ -83,14 +110,14 @@ public unsafe static nint FindPickerEntity(nint handle, nint controller)
return 0;
}
- public unsafe static nint GetSkeletonInstance(nint handle)
+ public unsafe static nint GetSkeletonInstance( nint handle )
{
try
{
unsafe
{
void*** ppVTable = (void***)handle;
- var pSkeletonInstance = (delegate* unmanaged)ppVTable[0][GetSkeletonInstanceOffset];
+ var pSkeletonInstance = (delegate* unmanaged< nint, nint >)ppVTable[0][GetSkeletonInstanceOffset];
return pSkeletonInstance(handle);
}
}
@@ -101,14 +128,14 @@ public unsafe static nint GetSkeletonInstance(nint handle)
return 0;
}
- public unsafe static void PawnCommitSuicide(nint pPawn, bool bExplode, bool bForce)
+ public unsafe static void PawnCommitSuicide( nint pPawn, bool bExplode, bool bForce )
{
try
{
unsafe
{
void*** ppVTable = (void***)pPawn;
- var pCommitSuicide = (delegate* unmanaged)ppVTable[0][CommitSuicideOffset];
+ var pCommitSuicide = (delegate* unmanaged< nint, byte, byte, void >)ppVTable[0][CommitSuicideOffset];
pCommitSuicide(pPawn, (byte)(bExplode ? 1 : 0), (byte)(bForce ? 1 : 0));
}
}
@@ -118,7 +145,7 @@ public unsafe static void PawnCommitSuicide(nint pPawn, bool bExplode, bool bFor
}
}
- public unsafe static void SetPlayerControllerPawn(nint pController, nint pPawn, bool b1, bool b2, bool b3, bool b4)
+ public unsafe static void SetPlayerControllerPawn( nint pController, nint pPawn, bool b1, bool b2, bool b3, bool b4 )
{
try
{
@@ -133,7 +160,7 @@ public unsafe static void SetPlayerControllerPawn(nint pController, nint pPawn,
}
}
- public unsafe static void SetModel(nint pEntity, string model)
+ public unsafe static void SetModel( nint pEntity, string model )
{
try
{
@@ -169,7 +196,7 @@ public unsafe static void Teleport(
unsafe
{
void*** ppVTable = (void***)pEntity;
- var pTeleport = (delegate* unmanaged)ppVTable[0][TeleportOffset];
+ var pTeleport = (delegate* unmanaged< nint, Vector*, QAngle*, Vector*, void >)ppVTable[0][TeleportOffset];
pTeleport(pEntity, vecPosition, vecAngle, vecVelocity);
}
}
@@ -248,14 +275,14 @@ public unsafe static void CTakeDamageInfoConstructor(
}
}
- public unsafe static void CCSPlayer_ItemServices_RemoveWeapons(nint pThis)
+ public unsafe static void CCSPlayer_ItemServices_RemoveWeapons( nint pThis )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pRemoveWeapons = (delegate* unmanaged)ppVTable[0][RemoveWeaponsOffset];
+ var pRemoveWeapons = (delegate* unmanaged< nint, void >)ppVTable[0][RemoveWeaponsOffset];
pRemoveWeapons(pThis);
}
}
@@ -265,14 +292,14 @@ public unsafe static void CCSPlayer_ItemServices_RemoveWeapons(nint pThis)
}
}
- public unsafe static nint CCSPlayer_ItemServices_GiveNamedItem(nint pThis, string name)
+ public unsafe static nint CCSPlayer_ItemServices_GiveNamedItem( nint pThis, string name )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pGiveNamedItem = (delegate* unmanaged)ppVTable[0][GiveNamedItemOffset];
+ var pGiveNamedItem = (delegate* unmanaged< nint, nint, nint >)ppVTable[0][GiveNamedItemOffset];
var pool = ArrayPool.Shared;
var nameLength = Encoding.UTF8.GetByteCount(name);
var nameBuffer = pool.Rent(nameLength + 1);
@@ -291,14 +318,14 @@ public unsafe static nint CCSPlayer_ItemServices_GiveNamedItem(nint pThis, strin
}
}
- public unsafe static void CCSPlayer_ItemServices_DropActiveItem(nint pThis, Vector momentum)
+ public unsafe static void CCSPlayer_ItemServices_DropActiveItem( nint pThis, Vector momentum )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pDropActiveItem = (delegate* unmanaged)ppVTable[0][DropActiveItemOffset];
+ var pDropActiveItem = (delegate* unmanaged< nint, Vector*, void >)ppVTable[0][DropActiveItemOffset];
pDropActiveItem(pThis, &momentum);
}
}
@@ -308,14 +335,14 @@ public unsafe static void CCSPlayer_ItemServices_DropActiveItem(nint pThis, Vect
}
}
- public unsafe static void CCSPlayer_WeaponServices_DropWeapon(nint pThis, nint pWeapon)
+ public unsafe static void CCSPlayer_WeaponServices_DropWeapon( nint pThis, nint pWeapon )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pDropWeapon = (delegate* unmanaged)ppVTable[0][DropWeaponOffset];
+ var pDropWeapon = (delegate* unmanaged< nint, nint, void >)ppVTable[0][DropWeaponOffset];
pDropWeapon(pThis, pWeapon);
}
}
@@ -325,14 +352,14 @@ public unsafe static void CCSPlayer_WeaponServices_DropWeapon(nint pThis, nint p
}
}
- public unsafe static void CCSPlayer_WeaponServices_SelectWeapon(nint pThis, nint pWeapon)
+ public unsafe static void CCSPlayer_WeaponServices_SelectWeapon( nint pThis, nint pWeapon )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pSelectWeapon = (delegate* unmanaged)ppVTable[0][SelectWeaponOffset];
+ var pSelectWeapon = (delegate* unmanaged< nint, nint, void >)ppVTable[0][SelectWeaponOffset];
pSelectWeapon(pThis, pWeapon);
}
}
@@ -342,7 +369,7 @@ public unsafe static void CCSPlayer_WeaponServices_SelectWeapon(nint pThis, nint
}
}
- public unsafe static void CEntityResourceManifest_AddResource(nint pThis, string path)
+ public unsafe static void CEntityResourceManifest_AddResource( nint pThis, string path )
{
try
{
@@ -354,7 +381,7 @@ public unsafe static void CEntityResourceManifest_AddResource(nint pThis, string
Encoding.UTF8.GetBytes(path, pathBuffer);
pathBuffer[pathLength] = 0;
void*** ppVTable = (void***)pThis;
- var pAddResource = (delegate* unmanaged)ppVTable[0][AddResourceOffset];
+ var pAddResource = (delegate* unmanaged< nint, nint, void >)ppVTable[0][AddResourceOffset];
fixed (byte* pPath = pathBuffer)
{
pAddResource(pThis, (IntPtr)pPath);
@@ -368,7 +395,7 @@ public unsafe static void CEntityResourceManifest_AddResource(nint pThis, string
}
}
- public unsafe static void SetOrAddAttribute(nint handle, string name, float value)
+ public unsafe static void SetOrAddAttribute( nint handle, string name, float value )
{
try
{
@@ -392,14 +419,14 @@ public unsafe static void SetOrAddAttribute(nint handle, string name, float valu
}
}
- public unsafe static void CBaseEntity_CollisionRulesChanged(nint pThis)
+ public unsafe static void CBaseEntity_CollisionRulesChanged( nint pThis )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pCollisionRulesChanged = (delegate* unmanaged)ppVTable[0][CollisionRulesChangedOffset];
+ var pCollisionRulesChanged = (delegate* unmanaged< nint, void >)ppVTable[0][CollisionRulesChangedOffset];
pCollisionRulesChanged(pThis);
}
}
@@ -409,14 +436,14 @@ public unsafe static void CBaseEntity_CollisionRulesChanged(nint pThis)
}
}
- public unsafe static void CCSPlayerController_Respawn(nint pThis)
+ public unsafe static void CCSPlayerController_Respawn( nint pThis )
{
try
{
unsafe
{
void*** ppVTable = (void***)pThis;
- var pRespawn = (delegate* unmanaged)ppVTable[0][RespawnOffset];
+ var pRespawn = (delegate* unmanaged< nint, void >)ppVTable[0][RespawnOffset];
pRespawn(pThis);
}
}
diff --git a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs
index 703f153db..0786594bf 100644
--- a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs
+++ b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs
@@ -134,7 +134,10 @@ private void HookTouch()
{
return (pBaseEntity, pOtherEntity) =>
{
- EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = new CBaseEntityImpl(pBaseEntity), OtherEntity = new CBaseEntityImpl(pOtherEntity), TouchType = EntityTouchType.StartTouch });
+ var entity = new CBaseEntityImpl(pBaseEntity);
+ var otherEntity = new CBaseEntityImpl(pOtherEntity);
+ EventPublisher.InvokeOnEntityStartTouch(new OnEntityStartTouchEvent { Entity = entity, OtherEntity = otherEntity });
+ EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = entity, OtherEntity = otherEntity, TouchType = EntityTouchType.StartTouch });
return next()(pBaseEntity, pOtherEntity);
};
});
@@ -143,7 +146,10 @@ private void HookTouch()
{
return (pBaseEntity, pOtherEntity) =>
{
- EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = new CBaseEntityImpl(pBaseEntity), OtherEntity = new CBaseEntityImpl(pOtherEntity), TouchType = EntityTouchType.Touch });
+ var entity = new CBaseEntityImpl(pBaseEntity);
+ var otherEntity = new CBaseEntityImpl(pOtherEntity);
+ EventPublisher.InvokeOnEntityTouch(new OnEntityTouchEvent { Entity = entity, OtherEntity = otherEntity });
+ EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = entity, OtherEntity = otherEntity, TouchType = EntityTouchType.Touch });
return next()(pBaseEntity, pOtherEntity);
};
});
@@ -152,7 +158,10 @@ private void HookTouch()
{
return (pBaseEntity, pOtherEntity) =>
{
- EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = new CBaseEntityImpl(pBaseEntity), OtherEntity = new CBaseEntityImpl(pOtherEntity), TouchType = EntityTouchType.EndTouch });
+ var entity = new CBaseEntityImpl(pBaseEntity);
+ var otherEntity = new CBaseEntityImpl(pOtherEntity);
+ EventPublisher.InvokeOnEntityEndTouch(new OnEntityEndTouchEvent { Entity = entity, OtherEntity = otherEntity });
+ EventPublisher.InvokeOnEntityTouchHook(new OnEntityTouchHookEvent { Entity = entity, OtherEntity = otherEntity, TouchType = EntityTouchType.EndTouch });
return next()(pBaseEntity, pOtherEntity);
};
});
diff --git a/managed/src/SwiftlyS2.Core/Services/PluginConfigurationService.cs b/managed/src/SwiftlyS2.Core/Services/PluginConfigurationService.cs
index 2bd92c92f..a12ae461f 100644
--- a/managed/src/SwiftlyS2.Core/Services/PluginConfigurationService.cs
+++ b/managed/src/SwiftlyS2.Core/Services/PluginConfigurationService.cs
@@ -1,47 +1,55 @@
using System.Data.Common;
-using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Core.Services;
using SwiftlyS2.Shared.Services;
+using Tomlyn;
namespace SwiftlyS2.Core.Services;
-internal class PluginConfigurationService : IPluginConfigurationService {
+internal class PluginConfigurationService : IPluginConfigurationService
+{
private ConfigurationService _ConfigurationService { get; init; }
private CoreContext _Id { get; init; }
private IConfigurationManager? _Manager { get; set; }
- public bool BasePathExists {
+ public bool BasePathExists
+ {
get => Path.Exists(BasePath);
}
- public PluginConfigurationService(CoreContext id, ConfigurationService configurationService) {
+ public PluginConfigurationService(CoreContext id, ConfigurationService configurationService)
+ {
_Id = id;
_ConfigurationService = configurationService;
}
public string BasePath => Path.Combine(_ConfigurationService.GetConfigRoot(), "plugins", _Id.Name);
- public string GetRoot() {
+ public string GetRoot()
+ {
var dir = Path.Combine(_ConfigurationService.GetConfigRoot(), "plugins", _Id.Name);
- if (!Directory.Exists(dir)) {
+ if (!Directory.Exists(dir))
+ {
Directory.CreateDirectory(dir);
}
return dir;
}
- public string GetConfigPath(string name) {
+ public string GetConfigPath(string name)
+ {
return Path.Combine(GetRoot(), name);
}
- public IPluginConfigurationService InitializeWithTemplate(string name, string templatePath) {
+ public IPluginConfigurationService InitializeWithTemplate(string name, string templatePath)
+ {
var configPath = GetConfigPath(name);
- if (File.Exists(configPath)) {
+ if (File.Exists(configPath))
+ {
return this;
}
@@ -54,7 +62,8 @@ public IPluginConfigurationService InitializeWithTemplate(string name, string te
var templateAbsPath = Path.Combine(_Id.BaseDirectory, "resources", "templates", templatePath);
- if (!File.Exists(templateAbsPath)) {
+ if (!File.Exists(templateAbsPath))
+ {
throw new FileNotFoundException($"Template file not found: {templateAbsPath}");
}
@@ -62,11 +71,13 @@ public IPluginConfigurationService InitializeWithTemplate(string name, string te
return this;
}
- public IPluginConfigurationService InitializeJsonWithModel(string name, string sectionName) where T : class, new() {
-
+ public IPluginConfigurationService InitializeJsonWithModel(string name, string sectionName) where T : class, new()
+ {
+
var configPath = GetConfigPath(name);
- if (File.Exists(configPath)) {
+ if (File.Exists(configPath))
+ {
return this;
}
@@ -75,7 +86,6 @@ public IPluginConfigurationService InitializeWithTemplate(string name, string te
{
Directory.CreateDirectory(dir);
}
- File.Create(configPath).Close();
var config = new T();
@@ -84,7 +94,8 @@ public IPluginConfigurationService InitializeWithTemplate(string name, string te
[sectionName] = config
};
- var options = new JsonSerializerOptions {
+ var options = new JsonSerializerOptions
+ {
WriteIndented = true,
IncludeFields = true,
PropertyNamingPolicy = null
@@ -96,21 +107,55 @@ public IPluginConfigurationService InitializeWithTemplate(string name, string te
return this;
}
- public IPluginConfigurationService Configure(Action configure) {
+ public IPluginConfigurationService InitializeTomlWithModel(string name, string sectionName) where T : class, new()
+ {
+
+ var configPath = GetConfigPath(name);
+
+ if (File.Exists(configPath))
+ {
+ return this;
+ }
+
+ var dir = Path.GetDirectoryName(configPath);
+ if (dir is not null)
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ var config = new T();
+
+ var wrapped = new Dictionary
+ {
+ [sectionName] = config
+ };
+
+ var tomlString = Toml.FromModel(wrapped);
+ File.WriteAllText(configPath, tomlString);
+
+ return this;
+ }
+
+ public IPluginConfigurationService Configure(Action configure)
+ {
configure(Manager);
return this;
}
- public IConfigurationManager Manager {
- get {
- if (!BasePathExists) {
- throw new Exception("Base path does not exist in file system. Please call InitializeWithTemplate or InitializeJsonWithModel before using the Manager.");
+ public IConfigurationManager Manager
+ {
+ get
+ {
+ if (!BasePathExists)
+ {
+ throw new Exception("Base path does not exist in file system. Please call InitializeWithTemplate, InitializeJsonWithModel or InitializeTomlWithModel before using the Manager.");
}
- if (_Manager is null) {
+ if (_Manager is null)
+ {
_Manager = new ConfigurationManager();
_Manager.SetBasePath(BasePath);
}
return _Manager;
}
}
-}
\ No newline at end of file
+}
diff --git a/managed/src/SwiftlyS2.Shared/Helper.cs b/managed/src/SwiftlyS2.Shared/Helper.cs
index 88a2e5dbd..49cd0b2ba 100644
--- a/managed/src/SwiftlyS2.Shared/Helper.cs
+++ b/managed/src/SwiftlyS2.Shared/Helper.cs
@@ -85,4 +85,39 @@ public static T AsSchema(nint ptr) where T : ISchemaClass
{
return T.From(ptr);
}
+
+ ///
+ /// Estimates the display width of a character based on its type.
+ /// Inspired by: https://github.com/spectreconsole/wcwidth
+ ///
+ /// The character to measure.
+ /// The estimated display width in relative units.
+ public static float GetCharWidth(char c) => c switch
+ {
+ >= '\u4E00' and <= '\u9FFF' => 2.0f, // CJK Unified Ideographs
+ >= '\u3000' and <= '\u303F' => 2.0f, // CJK Symbols and Punctuation
+ >= '\uFF00' and <= '\uFFEF' => 2.0f, // Halfwidth and Fullwidth Forms
+ >= '\uAC00' and <= '\uD7AF' => 2.2f, // Hangul Syllables
+ >= '\u1100' and <= '\u11FF' => 2.2f, // Hangul Jamo
+ >= '\u3130' and <= '\u318F' => 2.2f, // Hangul Compatibility Jamo
+ >= '\u3040' and <= '\u309F' => 2.05f, // Hiragana
+ >= '\u30A0' and <= '\u30FF' => 2.05f, // Katakana
+ >= '\u31F0' and <= '\u31FF' => 2.05f, // Katakana Phonetic Extensions
+ >= 'A' and <= 'Z' => 1.2f,
+ >= 'a' and <= 'z' => 1.0f,
+ >= '0' and <= '9' => 1.0f,
+ ' ' => 0.5f,
+ >= '!' and <= '/' => 0.8f,
+ >= ':' and <= '@' => 0.8f,
+ >= '[' and <= '`' => 0.8f,
+ >= '{' and <= '~' => 0.8f,
+ _ => 1.0f
+ };
+
+ ///
+ /// Estimates the display width of a text string based on character types.
+ ///
+ /// The text string to measure.
+ /// The estimated display width in relative units.
+ public static float EstimateTextWidth(string text) => text.Sum(GetCharWidth);
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/HtmlGradient.cs b/managed/src/SwiftlyS2.Shared/HtmlGradient.cs
new file mode 100644
index 000000000..cb6c6310a
--- /dev/null
+++ b/managed/src/SwiftlyS2.Shared/HtmlGradient.cs
@@ -0,0 +1,78 @@
+namespace SwiftlyS2.Shared;
+
+///
+/// Provides utility methods for generating HTML text with gradient color effects.
+///
+public static class HtmlGradient
+{
+ ///
+ /// Generates gradient colored text by interpolating between two colors.
+ ///
+ /// The text to apply gradient to.
+ /// 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)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ var (startR, startG, startB) = ParseHexColor(startColor);
+ var (endR, endG, endB) = ParseHexColor(endColor);
+ var length = text.Length;
+
+ 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);
+ return $"{ch}";
+ }));
+ }
+
+ ///
+ /// Generates gradient colored text by interpolating across multiple color stops.
+ ///
+ /// 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
+ {
+ (null or "", _) => string.Empty,
+ (_, []) => text,
+ (_, [var single]) => $"{text}",
+ _ => GenerateMultiColorGradient(text, colors)
+ };
+
+ 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) =>
+ {
+ var position = length > 1 ? (float)i / (length - 1) : 0f;
+ var segmentIndex = position * (parsedColors.Length - 1);
+ var startIdx = (int)Math.Floor(segmentIndex);
+ var endIdx = Math.Min(startIdx + 1, parsedColors.Length - 1);
+ var ratio = segmentIndex - startIdx;
+
+ var (startR, startG, startB) = parsedColors[startIdx];
+ var (endR, endG, endB) = parsedColors[endIdx];
+
+ var r = (int)(startR + (endR - startR) * ratio);
+ var g = (int)(startG + (endG - startG) * ratio);
+ var b = (int)(startB + (endB - startB) * ratio);
+
+ return $"{ch}";
+ }));
+ }
+
+ 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);
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Engine/IEngineService.cs b/managed/src/SwiftlyS2.Shared/Modules/Engine/IEngineService.cs
index e3b078a4b..1ab0441d5 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Engine/IEngineService.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Engine/IEngineService.cs
@@ -1,4 +1,5 @@
using SwiftlyS2.Shared.Natives;
+using SwiftlyS2.Shared.SchemaDefinitions;
namespace SwiftlyS2.Shared.Services;
@@ -7,61 +8,74 @@ public interface IEngineService
///
/// The IP address of the server.
///
- string ServerIP { get; }
+ public string ServerIP { get; }
///
/// Gets the map that the server is running
///
[Obsolete("Use GlobalVars.MapName instead.")]
- string Map { get; }
+ public string Map { get; }
///
/// Gets a reference to the global variables structure.
///
- ref CGlobalVars GlobalVars { get; }
+ public ref CGlobalVars GlobalVars { get; }
///
/// Determines whether the specified map string represents a valid map in server files.
///
/// The map string to validate. It also supports Workshop ID.
/// true if the map is valid; otherwise, false.
- bool IsMapValid(string map);
+ public bool IsMapValid( string map );
///
/// Gets the maximum number of players allowed in the game.
///
[Obsolete("Use GlobalVars.MaxClients instead.")]
- int MaxPlayers { get; }
+ public int MaxPlayers { get; }
///
/// Executes the specified command string in the current context.
///
/// The command to execute. Cannot be null or empty.
- void ExecuteCommand(string command);
+ public void ExecuteCommand( string command );
///
/// Executes the specified command string in the current context.
///
/// The command to execute. Cannot be null or empty.
/// The callback to receive the output of the command.
- void ExecuteCommandWithBuffer(string command, Action bufferCallback);
+ public void ExecuteCommandWithBuffer( string command, Action bufferCallback );
///
/// The time since the server started.
///
[Obsolete("Use GlobalVars.CurrentTime instead.")]
- float CurrentTime { get; }
+ public float CurrentTime { get; }
///
/// The number of simulation ticks that have occurred since the server started.
///
[Obsolete("Use GlobalVars.TickCount instead.")]
- int TickCount { get; }
+ public int TickCount { get; }
///
/// Find a game system by name.
///
/// The name of the game system.
/// The game system handle. Null if not found.
- nint? FindGameSystemByName(string name);
+ public nint? FindGameSystemByName( string name );
+
+ ///
+ /// Dispatches a particle effect to the specified recipients.
+ ///
+ /// The name of the particle effect.
+ /// The type of attachment for the particle effect.
+ /// The attachment point for the particle effect.
+ /// The name of the attachment for the particle effect.
+ /// The recipient filter for the particle effect.
+ /// Whether to reset all particles on the entity.
+ /// The split screen slot for the particle effect.
+ /// The entity to attach the particle effect to.
+ public void DispatchParticleEffect( string particleName, ParticleAttachment_t attachmentType, byte attachmentPoint, CUtlSymbolLarge attachmentName, CRecipientFilter filter, bool resetAllParticlesOnEntity = false, int splitScreenSlot = 0, CBaseEntity? entity = null );
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs
index 8f6de8d08..94f4ae618 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventDelegates.cs
@@ -103,10 +103,23 @@ public class EventDelegates
///
public delegate void OnPrecacheResource(IOnPrecacheResourceEvent @event);
+ [Obsolete("OnEntityTouchHook is deprecated. Use OnEntityStartTouch, OnEntityTouch, or OnEntityEndTouch instead.")]
+ public delegate void OnEntityTouchHook(IOnEntityTouchHookEvent @event);
+
+ ///
+ /// Called when an entity starts touching another entity.
+ ///
+ public delegate void OnEntityStartTouch(IOnEntityStartTouchEvent @event);
+
///
/// Called when an entity is touching another entity.
///
- public delegate void OnEntityTouchHook(IOnEntityTouchHookEvent @event);
+ public delegate void OnEntityTouch(IOnEntityTouchEvent @event);
+
+ ///
+ /// Called when an entity ends touching another entity.
+ ///
+ public delegate void OnEntityEndTouch(IOnEntityEndTouchEvent @event);
///
/// Called when an item services can acquire hook is triggered.
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnEntityTouchHookEvent.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnEntityTouchHookEvent.cs
index e10759582..7891a0e87 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnEntityTouchHookEvent.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnEntityTouchHookEvent.cs
@@ -1,5 +1,6 @@
namespace SwiftlyS2.Shared.SchemaDefinitions
{
+ [Obsolete("EntityTouchType is deprecated. Use separate OnEntityStartTouch, OnEntityTouch, and OnEntityEndTouch events instead.")]
public enum EntityTouchType : byte
{
StartTouch = 0,
@@ -12,10 +13,7 @@ namespace SwiftlyS2.Shared.Events
{
using SwiftlyS2.Shared.SchemaDefinitions;
- ///
- /// Called when an entity touches another entity.
- /// This event is triggered for StartTouch, Touch, and EndTouch interactions.
- ///
+ [Obsolete("IOnEntityTouchHookEvent is deprecated. Use IOnEntityStartTouchEvent, IOnEntityTouchEvent, or IOnEntityEndTouchEvent instead.")]
public interface IOnEntityTouchHookEvent
{
@@ -34,4 +32,55 @@ public interface IOnEntityTouchHookEvent
///
public EntityTouchType TouchType { get; }
}
+
+ ///
+ /// Called when an entity starts touching another entity.
+ ///
+ public interface IOnEntityStartTouchEvent
+ {
+
+ ///
+ /// Gets the entity that initiated the touch.
+ ///
+ public CBaseEntity Entity { get; }
+
+ ///
+ /// Gets the entity being touched.
+ ///
+ public CBaseEntity OtherEntity { get; }
+ }
+
+ ///
+ /// Called when an entity is touching another entity.
+ ///
+ public interface IOnEntityTouchEvent
+ {
+
+ ///
+ /// Gets the entity that initiated the touch.
+ ///
+ public CBaseEntity Entity { get; }
+
+ ///
+ /// Gets the entity being touched.
+ ///
+ public CBaseEntity OtherEntity { get; }
+ }
+
+ ///
+ /// Called when an entity ends touching another entity.
+ ///
+ public interface IOnEntityEndTouchEvent
+ {
+
+ ///
+ /// Gets the entity that initiated the touch.
+ ///
+ public CBaseEntity Entity { get; }
+
+ ///
+ /// Gets the entity being touched.
+ ///
+ public CBaseEntity OtherEntity { get; }
+ }
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs
index a7671a5fb..ba694fb22 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Events/IEventSubscriber.cs
@@ -123,8 +123,21 @@ public interface IEventSubscriber
///
public event EventDelegates.OnCommandExecuteHook? OnCommandExecuteHook;
+ [Obsolete("OnEntityTouchHook is deprecated. Use OnEntityStartTouch, OnEntityTouch, or OnEntityEndTouch instead.")]
+ public event EventDelegates.OnEntityTouchHook? OnEntityTouchHook;
+
+ ///
+ /// Called when an entity starts touching another entity.
+ ///
+ public event EventDelegates.OnEntityStartTouch? OnEntityStartTouch;
+
///
/// Called when an entity is touching another entity.
///
- public event EventDelegates.OnEntityTouchHook? OnEntityTouchHook;
+ public event EventDelegates.OnEntityTouch? OnEntityTouch;
+
+ ///
+ /// Called when an entity ends touching another entity.
+ ///
+ public event EventDelegates.OnEntityEndTouch? OnEntityEndTouch;
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenu.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenu.cs
index f63cfdde5..12e6f4068 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenu.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenu.cs
@@ -175,7 +175,8 @@ public interface IMenu
/// Updates the menu display with current state and options.
///
/// The player to re-render the menu for.
- public void Rerender(IPlayer player);
+ /// True to update display text, false to render without updating display text.
+ public void Rerender(IPlayer player, bool updateDisplayText = false);
///
/// Determines whether the currently selected option is selectable for the specified player.
@@ -198,4 +199,186 @@ public interface IMenu
/// The player to set the freeze state for.
/// True to freeze the player, false to unfreeze.
public void SetFreezeState(IPlayer player, bool freeze);
+
+ ///
+ /// Gets or sets the vertical scroll style for the menu navigation.
+ /// Determines how the selection arrow moves when navigating through options.
+ ///
+ public MenuVerticalScrollStyle VerticalScrollStyle { get; set; }
+
+ ///
+ /// Gets or sets the horizontal text display style for menu options.
+ /// Controls maximum text width and overflow behavior. Null means no horizontal restrictions.
+ ///
+ public MenuHorizontalStyle? HorizontalStyle { get; set; }
+}
+
+///
+/// Defines the vertical scroll behavior style for menu navigation.
+///
+public enum MenuVerticalScrollStyle
+{
+ ///
+ /// Linear vertical scrolling mode where the selection indicator moves within the visible area.
+ /// Content displays linearly without wrapping, indicator adjusts position as selection changes.
+ ///
+ LinearScroll,
+
+ ///
+ /// Attempts to always keep the selection indicator at the preset center position.
+ /// Content scrolls vertically in a circular manner around the center, allowing wrap-around display (e.g., 7 8 1 2 3).
+ ///
+ CenterFixed,
+
+ ///
+ /// Waits for the selection indicator to reach the preset center, then maintains it there.
+ /// Indicator adjusts position at the edges but stays centered during mid-range vertical navigation.
+ ///
+ WaitingCenter
}
+
+///
+/// Defines the horizontal text overflow behavior for menu options.
+///
+public enum MenuHorizontalOverflowStyle
+{
+ ///
+ /// Truncates text at the end when it exceeds the maximum width, keeping the start portion.
+ /// Example: "Very Long Text Item" becomes "Very Long..."
+ ///
+ TruncateEnd,
+
+ ///
+ /// Truncates text from both ends when it exceeds the maximum width, keeping the middle portion.
+ /// Example: "Very Long Text Item" becomes "Long Text"
+ ///
+ TruncateBothEnds,
+
+ ///
+ /// Scrolls text to the left with fade-out effect.
+ /// Text scrolls left and gradually fades out at the left edge.
+ ///
+ ScrollLeftFade,
+
+ ///
+ /// Scrolls text to the right with fade-out effect.
+ /// Text scrolls right and gradually fades out at the right edge.
+ ///
+ ScrollRightFade,
+
+ ///
+ /// Scrolls text to the left in a continuous loop.
+ /// Text exits from the left edge and re-enters from the right edge.
+ ///
+ ScrollLeftLoop,
+
+ ///
+ /// Scrolls text to the right in a continuous loop.
+ /// Text exits from the right edge and re-enters from the left edge.
+ ///
+ ScrollRightLoop
+}
+
+///
+/// Horizontal text display style configuration for menu options.
+///
+public readonly record struct MenuHorizontalStyle
+{
+ private readonly float maxWidth;
+
+ ///
+ /// The maximum display width for menu option text in relative units.
+ ///
+ public required float MaxWidth
+ {
+ get => maxWidth;
+ init
+ {
+ if (value < 1f)
+ {
+ Spectre.Console.AnsiConsole.WriteException(new ArgumentOutOfRangeException(nameof(MaxWidth), $"MaxWidth: value {value:F3} is out of range."));
+ maxWidth = 1f;
+ }
+ else
+ {
+ maxWidth = value;
+ }
+ }
+ }
+
+ ///
+ /// The overflow behavior to apply when text exceeds MaxWidth.
+ ///
+ public MenuHorizontalOverflowStyle OverflowStyle { get; init; }
+
+ ///
+ /// Number of ticks before scrolling by one character.
+ ///
+ public int TicksPerScroll { get; init; }
+
+ ///
+ /// Number of ticks to pause after completing one scroll loop.
+ ///
+ public int PauseTicks { get; init; }
+
+ public MenuHorizontalStyle()
+ {
+ OverflowStyle = MenuHorizontalOverflowStyle.TruncateEnd;
+ TicksPerScroll = 16;
+ PauseTicks = 0;
+ }
+
+ ///
+ /// Creates a horizontal style with default behavior.
+ ///
+ public static MenuHorizontalStyle Default =>
+ new() { MaxWidth = 26, OverflowStyle = MenuHorizontalOverflowStyle.TruncateEnd };
+
+ ///
+ /// Creates a horizontal style with truncate end behavior.
+ ///
+ public static MenuHorizontalStyle TruncateEnd(float maxWidth) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.TruncateEnd };
+
+ ///
+ /// Creates a horizontal style with truncate both ends behavior.
+ ///
+ public static MenuHorizontalStyle TruncateBothEnds(float maxWidth) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.TruncateBothEnds };
+
+ ///
+ /// Creates a horizontal style with scroll left fade behavior.
+ ///
+ /// Maximum display width for text.
+ /// Number of ticks before scrolling by one character.
+ /// Number of ticks to pause after completing one scroll loop.
+ public static MenuHorizontalStyle ScrollLeftFade(float maxWidth, int ticksPerScroll = 16, int pauseTicks = 0) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.ScrollLeftFade, TicksPerScroll = ticksPerScroll, PauseTicks = pauseTicks };
+
+ ///
+ /// Creates a horizontal style with scroll right fade behavior.
+ ///
+ /// Maximum display width for text.
+ /// Number of ticks before scrolling by one character.
+ /// Number of ticks to pause after completing one scroll loop.
+ public static MenuHorizontalStyle ScrollRightFade(float maxWidth, int ticksPerScroll = 16, int pauseTicks = 0) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.ScrollRightFade, TicksPerScroll = ticksPerScroll, PauseTicks = pauseTicks };
+
+ ///
+ /// Creates a horizontal style with scroll left loop behavior.
+ ///
+ /// Maximum display width for text.
+ /// Number of ticks before scrolling by one character.
+ /// Number of ticks to pause after completing one scroll loop.
+ public static MenuHorizontalStyle ScrollLeftLoop(float maxWidth, int ticksPerScroll = 16, int pauseTicks = 0) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.ScrollLeftLoop, TicksPerScroll = ticksPerScroll, PauseTicks = pauseTicks };
+
+ ///
+ /// Creates a horizontal style with scroll right loop behavior.
+ ///
+ /// Maximum display width for text.
+ /// Number of ticks before scrolling by one character.
+ /// Number of ticks to pause after completing one scroll loop.
+ public static MenuHorizontalStyle ScrollRightLoop(float maxWidth, int ticksPerScroll = 16, int pauseTicks = 0) =>
+ new() { MaxWidth = maxWidth, OverflowStyle = MenuHorizontalOverflowStyle.ScrollRightLoop, TicksPerScroll = ticksPerScroll, PauseTicks = pauseTicks };
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuBuilder.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuBuilder.cs
index 997a1b000..b590d8c89 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuBuilder.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuBuilder.cs
@@ -9,6 +9,8 @@ namespace SwiftlyS2.Shared.Menus;
///
public interface IMenuBuilder
{
+ IMenuDesign Design { get; }
+
///
/// Sets the menu instance that this builder will modify.
/// This method is typically called internally to associate the builder with a specific menu.
@@ -31,8 +33,9 @@ public interface IMenuBuilder
/// The display text for the button.
/// Optional action to execute when the button is clicked. Receives the player as parameter.
/// The text size for the button display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddButton(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddButton(string text, Action? onClick = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a clickable button option to the menu.
@@ -41,8 +44,9 @@ public interface IMenuBuilder
/// The display text for the button.
/// Optional action to execute when the button is clicked. Receives the player and option as parameters.
/// The text size for the button display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddButton(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddButton(string text, Action? onClick, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a toggle switch option to the menu that can be turned on or off.
@@ -52,8 +56,9 @@ public interface IMenuBuilder
/// The initial state of the toggle. Defaults to false.
/// Optional action to execute when the toggle state changes. Receives the player and new boolean value.
/// The text size for the toggle display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddToggle(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddToggle(string text, bool defaultValue = false, Action? onToggle = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a toggle switch option to the menu that can be turned on or off.
@@ -63,8 +68,9 @@ public interface IMenuBuilder
/// The initial state of the toggle. Defaults to false.
/// Optional action to execute when the toggle state changes. Receives the player, option, and new boolean value.
/// The text size for the toggle display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddToggle(string text, bool defaultValue, Action? onToggle, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a slider option to the menu for selecting numeric values within a specified range.
@@ -77,8 +83,9 @@ public interface IMenuBuilder
/// The increment/decrement step size. Defaults to 1.
/// Optional action to execute when the slider value changes. Receives the player and new float value.
/// The text size for the slider display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step = 1, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a slider option to the menu for selecting numeric values within a specified range.
@@ -91,8 +98,9 @@ public interface IMenuBuilder
/// The increment/decrement step size.
/// Optional action to execute when the slider value changes. Receives the player, option, and new float value.
/// The text size for the slider display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddSlider(string text, float min, float max, float defaultValue, float step, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds an asynchronous button option to the menu that executes async operations.
@@ -101,8 +109,9 @@ public interface IMenuBuilder
/// The display text for the async button.
/// The async function to execute when the button is clicked. Receives the player as parameter.
/// The text size for the button display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds an asynchronous button option to the menu that executes async operations.
@@ -111,8 +120,9 @@ public interface IMenuBuilder
/// The display text for the async button.
/// The async function to execute when the button is clicked. Receives the player and option as parameters.
/// The text size for the button display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddAsyncButton(string text, Func onClickAsync, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a non-interactive text display option to the menu.
@@ -121,8 +131,9 @@ public interface IMenuBuilder
/// The text content to display.
/// The text alignment within the menu. Defaults to Left.
/// The text size for the display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddText(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddText(string text, ITextAlign alignment = ITextAlign.Left, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a submenu option that navigates to another menu when selected.
@@ -171,19 +182,22 @@ public interface IMenuBuilder
/// The initially selected choice. Defaults to null (first choice).
/// Optional action to execute when the choice changes. Receives the player and selected choice string.
/// The text size for the choice display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice = null, Action? onChange = null, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
- ///
- /// Adds a choice selection option that allows players to select from multiple predefined options.
- /// Players can cycle through the available choices using left/right navigation.
- ///
- /// The display text for the choice option.
- /// An array of available choice strings.
- /// The initially selected choice.
- /// Optional action to execute when the choice changes. Receives the player and selected choice string.
- /// The current menu builder instance for method chaining.
- IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange);
+ // ///
+ // /// Adds a choice selection option that allows players to select from multiple predefined options.
+ // /// Players can cycle through the available choices using left/right navigation.
+ // ///
+ // /// The display text for the choice option.
+ // /// An array of available choice strings.
+ // /// The initially selected choice.
+ // /// Optional action to execute when the choice changes. Receives the player and selected choice string.
+ // /// The overflow style for the text. Defaults to null.
+ // /// The current menu builder instance for method chaining.
+ // [Obsolete("This overload causes ambiguity. Use AddChoice(text, choices, defaultChoice, onChange, IMenuTextSize.Medium, overflowStyle) instead.")]
+ // IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a choice selection option that allows players to select from multiple predefined options.
@@ -194,8 +208,9 @@ public interface IMenuBuilder
/// The initially selected choice.
/// Optional action to execute when the choice changes. Receives the player, option, and selected choice string.
/// The text size for the choice display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddChoice(string text, string[] choices, string? defaultChoice, Action? onChange, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a visual separator line to the menu for organizing content.
@@ -212,8 +227,9 @@ public interface IMenuBuilder
/// A function that returns the current progress value (0.0 to 1.0).
/// The character width of the progress bar. Defaults to 20.
/// The text size for the progress bar display. Defaults to Medium.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium);
+ IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth = 20, IMenuTextSize size = IMenuTextSize.Medium, MenuHorizontalStyle? overflowStyle = null);
///
/// Adds a progress bar display option that shows dynamic progress information.
@@ -222,8 +238,9 @@ public interface IMenuBuilder
/// The display text for the progress bar.
/// A function that returns the current progress value (0.0 to 1.0).
/// The character width of the progress bar.
+ /// The overflow style for the text. Defaults to null.
/// The current menu builder instance for method chaining.
- IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth);
+ IMenuBuilder AddProgressBar(string text, Func progressProvider, int barWidth, MenuHorizontalStyle? overflowStyle = null);
///
/// Sets the parent menu for the menu being built, creating a hierarchical menu structure.
@@ -270,6 +287,7 @@ public interface IMenuBuilder
///
/// The names of the buttons to use for selection.
/// The current menu builder instance for method chaining.
+ [Obsolete("Use Design.OverrideSelectButton instead")]
IMenuBuilder OverrideSelectButton(params string[] buttonNames);
///
@@ -278,6 +296,7 @@ public interface IMenuBuilder
///
/// The names of the buttons to use for movement.
/// The current menu builder instance for method chaining.
+ [Obsolete("Use Design.OverrideMoveButton instead")]
IMenuBuilder OverrideMoveButton(params string[] buttonNames);
///
@@ -286,6 +305,7 @@ public interface IMenuBuilder
///
/// The names of the buttons to use for exiting.
/// The current menu builder instance for method chaining.
+ [Obsolete("Use Design.OverrideExitButton instead")]
IMenuBuilder OverrideExitButton(params string[] buttonNames);
///
@@ -294,6 +314,12 @@ public interface IMenuBuilder
///
/// The maximum number of visible items.
/// The current menu builder instance for method chaining.
+ ///
+ /// If the provided count is less than 1, it will be clamped to 1.
+ /// If the provided count is greater than 5, it will be clamped to 5.
+ /// A warning will be logged when clamping occurs.
+ ///
+ [Obsolete("Use Design.MaxVisibleItems instead")]
IMenuBuilder MaxVisibleItems(int count);
///
@@ -316,6 +342,7 @@ public interface IMenuBuilder
///
/// The color to use for menu rendering.
/// The current menu builder instance for method chaining.
+ [Obsolete("Use Design.SetColor instead")]
IMenuBuilder SetColor(Color color);
}
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesign.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesign.cs
new file mode 100644
index 000000000..86accb061
--- /dev/null
+++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuDesign.cs
@@ -0,0 +1,66 @@
+using SwiftlyS2.Shared.Natives;
+
+namespace SwiftlyS2.Shared.Menus;
+
+public interface IMenuDesign
+{
+ ///
+ /// Overrides the default button(s) used for selecting menu options.
+ /// Allows customization of the input controls for menu interaction.
+ ///
+ /// The names of the buttons to use for selection.
+ /// The current menu design instance for method chaining.
+ IMenuDesign OverrideSelectButton(params string[] buttonNames);
+
+ ///
+ /// Overrides the default button(s) used for moving through menu options.
+ /// Allows customization of the input controls for menu navigation.
+ ///
+ /// The names of the buttons to use for movement.
+ /// The current menu design instance for method chaining.
+ IMenuDesign OverrideMoveButton(params string[] buttonNames);
+
+ ///
+ /// Overrides the default button(s) used for exiting or closing the menu.
+ /// Allows customization of the input controls for menu exit.
+ ///
+ /// The names of the buttons to use for exiting.
+ /// The current menu design instance for method chaining.
+ IMenuDesign OverrideExitButton(params string[] buttonNames);
+
+ ///
+ /// Sets the maximum number of menu items visible at once.
+ /// When there are more items than this limit, the menu will be paginated.
+ ///
+ /// The maximum number of visible items.
+ /// The current menu design instance for method chaining.
+ ///
+ /// If the provided count is less than 1, it will be clamped to 1.
+ /// If the provided count is greater than 5, it will be clamped to 5.
+ /// A warning will be logged when clamping occurs.
+ ///
+ IMenuDesign MaxVisibleItems(int count);
+
+ ///
+ /// Sets the color used for rendering the menu.
+ /// Affects the visual appearance and styling of the menu display.
+ ///
+ /// The color to use for menu rendering.
+ /// The current menu design instance for method chaining.
+ IMenuDesign SetColor(Color color);
+
+ ///
+ /// Sets the vertical scroll style for the menu navigation.
+ ///
+ /// The vertical scroll style to use.
+ /// The current menu design instance for method chaining.
+ IMenuDesign SetVerticalScrollStyle(MenuVerticalScrollStyle style);
+
+ ///
+ /// Sets the global horizontal style for menu option text display.
+ /// Controls maximum text width and overflow behavior for all menu options.
+ ///
+ /// The global horizontal style to apply.
+ /// The current menu design instance for method chaining.
+ IMenuDesign SetGlobalHorizontalStyle(MenuHorizontalStyle style);
+}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuManager.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuManager.cs
index 1e28636c6..541b5a53f 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuManager.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IMenuManager.cs
@@ -110,6 +110,12 @@ public interface IMenuManager
/// The player's current menu, or null if no menu is open.
public IMenu? GetMenu(IPlayer player);
+ ///
+ /// Closes all open menus for all players.
+ /// This includes all menus in the parent chain.
+ ///
+ public void CloseAllMenus();
+
///
/// Closes the specified menu for all players who currently have it open.
/// This will trigger the OnClose event for each affected player.
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Menus/IOption.cs b/managed/src/SwiftlyS2.Shared/Modules/Menus/IOption.cs
index c44e65cf1..87d979bdd 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Menus/IOption.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Menus/IOption.cs
@@ -27,6 +27,11 @@ public interface IOption
///
public IMenu? Menu { get; set; }
+ ///
+ /// Gets the horizontal overflow style for this option's text display.
+ ///
+ public MenuHorizontalStyle? OverflowStyle { get; }
+
///
/// Determines whether this option should be shown to the specified player.
///
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayer.cs b/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayer.cs
index be4d8c271..a7644ff88 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayer.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayer.cs
@@ -38,12 +38,18 @@ public interface IPlayer : IEquatable
/// Gets the unique identifier for the player.
///
public int PlayerID { get; }
+
+ ///
+ /// Gets the slot of the player. Equals to the player ID.
+ ///
+ public int Slot { get; }
+
///
/// Sends a message of the specified type to the player.
///
/// The type of message to send. Determines how the message is processed or displayed.
/// The content of the message to send. Cannot be null.
- void SendMessage(MessageType kind, string message);
+ public void SendMessage(MessageType kind, string message);
///
/// Whether the client is a bot.
///
diff --git a/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayerManager.cs b/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayerManager.cs
index 17c5b49f0..3fdc7c9ed 100644
--- a/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayerManager.cs
+++ b/managed/src/SwiftlyS2.Shared/Modules/Players/IPlayerManager.cs
@@ -6,7 +6,7 @@ public interface IPlayerManagerService
/// Checks whether a specific player is currently online and connected to the server.
///
/// True if the player is online, false otherwise.
- bool IsPlayerOnline(int playerid);
+ public bool IsPlayerOnline(int playerid);
///
/// Gets the number of players currently in the game.
@@ -23,17 +23,17 @@ public interface IPlayerManagerService
///
/// The type of message display.
/// The text content to send to players.
- void SendMessage(MessageType kind, string message);
+ public void SendMessage(MessageType kind, string message);
///
/// Controls whether a specific entity should be blocked from being transmitted/synchronized to clients.
///
- void ShouldBlockTransmitEntity(int entityid, bool shouldBlockTransmit);
+ public void ShouldBlockTransmitEntity(int entityid, bool shouldBlockTransmit);
///
/// Removes all entity transmission blocks, allowing all previously blocked entities to be transmitted to clients again.
///
- void ClearAllBlockedTransmitEntities();
+ public void ClearAllBlockedTransmitEntities();
///
/// Retrieves the player associated with the specified player ID.
@@ -41,13 +41,13 @@ public interface IPlayerManagerService
/// The unique identifier of the player to retrieve. Must be a valid player ID.
/// An instance representing the player with the specified ID, or null if no such
/// player exists.
- IPlayer GetPlayer(int playerid);
+ public IPlayer GetPlayer(int playerid);
///
/// Retrieves all players currently online.
///
/// An enumerable collection of instances representing all online players.
- IEnumerable GetAllPlayers();
+ public IEnumerable GetAllPlayers();
///
/// Finds targetted players based on the provided search criteria.
@@ -56,5 +56,5 @@ public interface IPlayerManagerService
/// The target player name or identifier.
/// The search mode to apply.
/// A collection of players matching the search criteria.
- IEnumerable FindTargettedPlayers(IPlayer player, string target, TargetSearchMode searchMode);
+ public IEnumerable FindTargettedPlayers(IPlayer player, string target, TargetSearchMode searchMode);
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Natives/Structs/CRecipientFilter.cs b/managed/src/SwiftlyS2.Shared/Natives/Structs/CRecipientFilter.cs
index 06a7c3b14..c9aaf8d25 100644
--- a/managed/src/SwiftlyS2.Shared/Natives/Structs/CRecipientFilter.cs
+++ b/managed/src/SwiftlyS2.Shared/Natives/Structs/CRecipientFilter.cs
@@ -1,8 +1,9 @@
using System.Runtime.InteropServices;
+using SwiftlyS2.Core.Natives;
namespace SwiftlyS2.Shared.Natives;
-public enum NetChannelBufType_t: sbyte
+public enum NetChannelBufType_t : sbyte
{
BUF_DEFAULT = -1,
BUF_UNRELIABLE = 0,
@@ -18,7 +19,7 @@ public struct CRecipientFilter
public NetChannelBufType_t BufferType;
public bool InitMessage;
- public CRecipientFilter(NetChannelBufType_t BufType = NetChannelBufType_t.BUF_RELIABLE, bool bInitMessage = false)
+ public CRecipientFilter( NetChannelBufType_t BufType = NetChannelBufType_t.BUF_RELIABLE, bool bInitMessage = false )
{
_pVTable = CRecipientFilterVtable.pCRecipientFilterVTable;
RecipientsMask = 0;
@@ -26,23 +27,24 @@ public CRecipientFilter(NetChannelBufType_t BufType = NetChannelBufType_t.BUF_RE
BufferType = BufType;
}
- public static CRecipientFilter FromMask(ulong playerMask)
+ public static CRecipientFilter FromMask( ulong playerMask )
{
CRecipientFilter filter = new();
filter.RecipientsMask = playerMask;
return filter;
}
- public static CRecipientFilter FromPlayers(params int[] players)
+ public static CRecipientFilter FromPlayers( params int[] players )
{
CRecipientFilter filter = new();
- foreach (int player in players) {
+ foreach (var player in players)
+ {
filter.AddRecipient(player);
}
return filter;
}
- public static CRecipientFilter FromSingle(int player)
+ public static CRecipientFilter FromSingle( int player )
{
CRecipientFilter filter = new();
filter.AddRecipient(player);
@@ -56,8 +58,13 @@ public ulong ToMask()
public void AddAllPlayers()
{
- RecipientsMask = 0xFFFFFFFFFFFFFFFF;
- // @todo: When playermanager will be implemeneted, iterate through all the 64 players and if they're online and are authorized, add them here
+ for (var i = 0; i < NativePlayerManager.GetPlayerCap(); i++)
+ {
+ if (NativePlayerManager.IsPlayerOnline(i))
+ {
+ AddRecipient(i);
+ }
+ }
}
public void RemoveAllPlayers()
@@ -65,14 +72,14 @@ public void RemoveAllPlayers()
RecipientsMask = 0;
}
- public void AddRecipient(int playerid)
+ public void AddRecipient( int playerid )
{
if (playerid < 0 || playerid > 63) throw new IndexOutOfRangeException("PlayerID out of range (0-63).");
RecipientsMask |= 1UL << playerid;
}
- public void RemoveRecipient(int playerid)
+ public void RemoveRecipient( int playerid )
{
if (playerid < 0 || playerid > 63) throw new IndexOutOfRangeException("PlayerID out of range (0-63).");
@@ -82,8 +89,10 @@ public void RemoveRecipient(int playerid)
public int GetRecipientCount()
{
int count = 0;
- for (int i = 0; i < 64; i++) {
- if ((RecipientsMask & (1UL << i)) != 0) {
+ for (int i = 0; i < 64; i++)
+ {
+ if ((RecipientsMask & (1UL << i)) != 0)
+ {
count++;
}
}
@@ -91,36 +100,42 @@ public int GetRecipientCount()
}
}
-internal static class CRecipientFilterVtable {
+internal static class CRecipientFilterVtable
+{
public static nint pCRecipientFilterVTable;
[UnmanagedCallersOnly]
- public unsafe static void Destructor(CRecipientFilter* filter) {
+ public unsafe static void Destructor( CRecipientFilter* filter )
+ {
// do nothing
}
[UnmanagedCallersOnly]
- public unsafe static NetChannelBufType_t GetNetworkBufType(CRecipientFilter* filter) {
+ public unsafe static NetChannelBufType_t GetNetworkBufType( CRecipientFilter* filter )
+ {
return filter->BufferType;
}
[UnmanagedCallersOnly]
- public unsafe static bool IsInitMessage(CRecipientFilter* filter) {
+ public unsafe static bool IsInitMessage( CRecipientFilter* filter )
+ {
return filter->InitMessage;
}
[UnmanagedCallersOnly]
- public unsafe static ulong* GetRecipients(CRecipientFilter* filter) {
+ public unsafe static ulong* GetRecipients( CRecipientFilter* filter )
+ {
return &filter->RecipientsMask;
}
- static unsafe CRecipientFilterVtable() {
+ static unsafe CRecipientFilterVtable()
+ {
pCRecipientFilterVTable = Marshal.AllocHGlobal(sizeof(nint) * 4);
Span vtable = new((void*)pCRecipientFilterVTable, 4);
- vtable[0] = (nint)(delegate* unmanaged)(&Destructor);
- vtable[1] = (nint)(delegate* unmanaged)(&GetNetworkBufType);
- vtable[2] = (nint)(delegate* unmanaged)(&IsInitMessage);
- vtable[3] = (nint)(delegate* unmanaged)(&GetRecipients);
+ vtable[0] = (nint)(delegate* unmanaged< CRecipientFilter*, void >)(&Destructor);
+ vtable[1] = (nint)(delegate* unmanaged< CRecipientFilter*, NetChannelBufType_t >)(&GetNetworkBufType);
+ vtable[2] = (nint)(delegate* unmanaged< CRecipientFilter*, bool >)(&IsInitMessage);
+ vtable[3] = (nint)(delegate* unmanaged< CRecipientFilter*, ulong* >)(&GetRecipients);
}
}
\ No newline at end of file
diff --git a/managed/src/SwiftlyS2.Shared/Services/IPluginConfigurationService.cs b/managed/src/SwiftlyS2.Shared/Services/IPluginConfigurationService.cs
index 90d4c3ab3..a1cdc66f9 100644
--- a/managed/src/SwiftlyS2.Shared/Services/IPluginConfigurationService.cs
+++ b/managed/src/SwiftlyS2.Shared/Services/IPluginConfigurationService.cs
@@ -2,7 +2,8 @@
namespace SwiftlyS2.Shared.Services;
-public interface IPluginConfigurationService {
+public interface IPluginConfigurationService
+{
///
/// Get the base path of plugin configuration.
@@ -33,7 +34,15 @@ public interface IPluginConfigurationService {
/// The name of the configuration file.
/// The name of the section in the configuration file.
public IPluginConfigurationService InitializeJsonWithModel(string name, string sectionName) where T : class, new();
-
+
+ ///
+ /// Initialize the TOML configuration file with a class as template.
+ ///
+ /// The type of the configuration model.
+ /// The name of the configuration file.
+ /// The name of the section in the configuration file.
+ public IPluginConfigurationService InitializeTomlWithModel(string name, string sectionName) where T : class, new();
+
///
/// Configure the internal configuration manager.
///
@@ -52,4 +61,4 @@ public interface IPluginConfigurationService {
///
public bool BasePathExists { get; }
-}
\ No newline at end of file
+}
diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs
index 592b964a5..4895f243f 100644
--- a/managed/src/TestPlugin/TestPlugin.cs
+++ b/managed/src/TestPlugin/TestPlugin.cs
@@ -28,6 +28,7 @@
using BenchmarkDotNet.Toolchains.InProcess.Emit;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Loggers;
+using SwiftlyS2.Shared.Menus;
namespace TestPlugin;
@@ -110,26 +111,31 @@ public override void Load(bool hotReload)
// Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.Command[0], @event.Command.ArgS);
// };
- 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.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) =>
{
@@ -548,6 +554,88 @@ public HookResult TestServerNetMessageHandler(CCSUsrMsg_SendPlayerItemDrops msg)
return HookResult.Continue;
}
+ [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)
{
@@ -588,12 +676,13 @@ public void MenuCommand(ICommandContext context)
player.SendMessage(MessageType.Chat, "You clicked Button 8");
})
.AddSeparator()
- .AddText("hello!", size: SwiftlyS2.Shared.Menus.IMenuTextSize.ExtraLarge)
- .SetColor(new(0, 186, 105, 255))
+ .AddText("hello!", size: IMenuTextSize.ExtraLarge)
.AutoClose(15f)
.HasSound(true)
.ForceFreeze();
+ menu.Builder.Design.SetColor(new(0, 186, 105, 255));
+
Core.Menus.OpenMenu(player, menu);
}
diff --git a/plugin_files/gamedata/cs2/core/signatures.jsonc b/plugin_files/gamedata/cs2/core/signatures.jsonc
index 2068cc3af..ff103e4fe 100644
--- a/plugin_files/gamedata/cs2/core/signatures.jsonc
+++ b/plugin_files/gamedata/cs2/core/signatures.jsonc
@@ -196,5 +196,11 @@
"lib": "server",
"windows": "48 89 5C 24 ? 57 48 83 EC ? 33 FF 4C 8B CA 8B D9",
"linux": "55 31 D2 48 89 E5 41 56 41 55 41 54"
+ },
+ // Search for "ParticleEffect" and the function contains it
+ "DispatchParticleEffect": {
+ "lib": "server",
+ "windows": "48 89 5C 24 08 48 89 74 24 10 48 89 7C 24 18 4C 89 74 24 20 55 48 8D 6C 24 D1",
+ "linux": "55 48 89 E5 41 57 41 56 41 55 41 54 41 89 CC 53 48 89 D3"
}
}
\ No newline at end of file
diff --git a/src/core/managed/host/host.cpp b/src/core/managed/host/host.cpp
index 74e151fcd..bdf67aabb 100644
--- a/src/core/managed/host/host.cpp
+++ b/src/core/managed/host/host.cpp
@@ -5,6 +5,8 @@
#include
#include
+#define MIN(a,b) (((a) < (b)) ? (a) : (b))
+
hostfxr_initialize_for_runtime_config_fn _initialize_for_runtime_config = nullptr;
hostfxr_get_runtime_delegate_fn _get_runtime_delegate = nullptr;
hostfxr_close_fn _close = nullptr;
@@ -47,6 +49,14 @@ std::string widenedOriginPath;
std::string original_path;
bool InitializeHostFXR(std::string origin_path) {
+#ifdef _WIN32
+ for (size_t i = 0; i < origin_path.size(); ++i) {
+ if (origin_path[i] == '/') {
+ origin_path[i] = '\\';
+ }
+ }
+#endif
+
original_path = origin_path;
#ifdef _WIN32
@@ -55,8 +65,18 @@ bool InitializeHostFXR(std::string origin_path) {
widenedOriginPath = origin_path;
#endif
- hostfxr_lib = load_library((WIN_LIN(widenedOriginPath + L"bin\\win64\\" + L"hostfxr.dll", widenedOriginPath + "bin/linuxsteamrt64/" + "libhostfxr.so")).c_str());
- if (!hostfxr_lib) return false;
+ // Construct hostfxr library path
+#ifdef _WIN32
+ std::wstring hostfxr_path = widenedOriginPath + L"bin\\win64\\hostfxr.dll";
+#else
+ std::string hostfxr_path = widenedOriginPath + "bin/linuxsteamrt64/libhostfxr.so";
+#endif
+
+ hostfxr_lib = load_library(hostfxr_path.c_str());
+ if (!hostfxr_lib) {
+ std::cerr << "[Swiftly] Error: Failed to load hostfxr library from: " << original_path << std::endl;
+ return false;
+ }
_initialize_for_runtime_config = (hostfxr_initialize_for_runtime_config_fn)get_export(hostfxr_lib, "hostfxr_initialize_for_runtime_config");
if (!_initialize_for_runtime_config) return false;
@@ -70,25 +90,52 @@ bool InitializeHostFXR(std::string origin_path) {
_set_runtime_prop_value = (hostfxr_set_runtime_property_value_fn)get_export(hostfxr_lib, "hostfxr_set_runtime_property_value");
if (!_set_runtime_prop_value) return false;
+ // Initialize params structure completely
hostfxr_initialize_parameters params;
+ memset(¶ms, 0, sizeof(params));
params.size = sizeof(hostfxr_initialize_parameters);
+
#ifdef _WIN32
- std::wstring path = widenedOriginPath + L"bin\\managed\\dotnet";
+ std::wstring dotnet_root_path = widenedOriginPath + L"bin\\managed\\dotnet";
+ std::wstring runtime_config_path = widenedOriginPath + L"bin\\managed\\SwiftlyS2.CS2.runtimeconfig.json";
#else
- std::string path = widenedOriginPath + "bin/managed/dotnet";
+ std::string dotnet_root_path = widenedOriginPath + "bin/managed/dotnet";
+ std::string runtime_config_path = widenedOriginPath + "bin/managed/SwiftlyS2.CS2.runtimeconfig.json";
#endif
- memcpy(dotnet_path, path.c_str(), path.size() * sizeof(char_t) >= 1024 ? 1023 : path.size() * sizeof(char_t));
+ // Validate origin path
+ if (widenedOriginPath.empty()) {
+ std::cerr << "[Swiftly] Error: Origin path is empty!" << std::endl;
+ return false;
+ }
+
+ // Validate constructed paths
+ if (dotnet_root_path.empty() || runtime_config_path.empty()) {
+ std::cerr << "[Swiftly] Error: Runtime paths are empty. Origin path: " << original_path << std::endl;
+ return false;
+ }
+
+ // Clear and copy dotnet root path to buffer with bounds checking
+ memset(dotnet_path, 0, sizeof(dotnet_path));
+#ifdef _WIN32
+ size_t copy_size = MIN(dotnet_root_path.size() * sizeof(wchar_t), sizeof(dotnet_path) - sizeof(wchar_t));
+#else
+ size_t copy_size = MIN(dotnet_root_path.size(), sizeof(dotnet_path) - 1);
+#endif
+ memcpy(dotnet_path, dotnet_root_path.c_str(), copy_size);
params.dotnet_root = dotnet_path;
- int returnCode = _initialize_for_runtime_config((widenedOriginPath + WIN_LIN(L"bin\\managed\\SwiftlyS2.CS2.runtimeconfig.json", "bin/managed/SwiftlyS2.CS2.runtimeconfig.json")).c_str(), ¶ms, &fxrcxt);
+ // Initialize .NET runtime (using local variable to avoid dangling pointer)
+ int returnCode = _initialize_for_runtime_config(runtime_config_path.c_str(), ¶ms, &fxrcxt);
if (returnCode != 0) {
+ std::cerr << "[Swiftly] Error: Failed to initialize .NET runtime (code: " << returnCode << ")" << std::endl;
+ std::cerr << "[Swiftly] Config path: " << original_path << std::endl;
_close(fxrcxt);
return false;
}
- _set_runtime_prop_value(fxrcxt, WIN_LIN(L"APP_CONTEXT_BASE_DIRECTORY", "APP_CONTEXT_BASE_DIRECTORY"), WIN_LIN(path.c_str(), dotnet_path));
+ _set_runtime_prop_value(fxrcxt, WIN_LIN(L"APP_CONTEXT_BASE_DIRECTORY", "APP_CONTEXT_BASE_DIRECTORY"), dotnet_path);
returnCode = _get_runtime_delegate(fxrcxt, hdt_load_assembly_and_get_function_pointer, (void**)&_load_assembly_and_get_function_pointer);
if (returnCode != 0 || (void*)_load_assembly_and_get_function_pointer == nullptr) {
@@ -104,12 +151,24 @@ bool InitializeDotNetAPI(void* scripting_table, int scripting_table_size) {
static custom_loader_fn custom_loader = nullptr;
if (custom_loader == nullptr) {
+ // Construct DLL path as local variable to avoid dangling pointer
+#ifdef _WIN32
+ std::wstring dll_path = widenedOriginPath + L"bin\\managed\\SwiftlyS2.CS2.dll";
+#else
+ std::string dll_path = widenedOriginPath + "bin/managed/SwiftlyS2.CS2.dll";
+#endif
+
int returnCode = _load_assembly_and_get_function_pointer(
- (widenedOriginPath + WIN_LIN(L"bin\\managed\\SwiftlyS2.CS2.dll", "bin/managed/SwiftlyS2.CS2.dll")).c_str(),
- STR("SwiftlyS2.Entrypoint, SwiftlyS2.CS2"), STR("Start"), UNMANAGEDCALLERSONLY_METHOD, nullptr, (void**)&custom_loader
+ dll_path.c_str(),
+ STR("SwiftlyS2.Entrypoint, SwiftlyS2.CS2"),
+ STR("Start"),
+ UNMANAGEDCALLERSONLY_METHOD,
+ nullptr,
+ (void**)&custom_loader
);
if (returnCode != 0 || (void*)custom_loader == nullptr) {
+ std::cerr << "[Swiftly] Error: Failed to load .NET assembly (code: " << returnCode << ")" << std::endl;
return false;
}
diff --git a/src/scripting/server/player.cpp b/src/scripting/server/player.cpp
index 44b34c18d..192cf6164 100644
--- a/src/scripting/server/player.cpp
+++ b/src/scripting/server/player.cpp
@@ -281,7 +281,7 @@ void Bridge_Player_ExecuteCommand(int playerid, const char* command)
CCommand cmd;
cmd.Tokenize(command);
- ConCommandRef cmdRef(command[0]);
+ ConCommandRef cmdRef(cmd[0]);
if (cmdRef.IsValidRef()) {
CCommandContext context(CommandTarget_t::CT_FIRST_SPLITSCREEN_CLIENT, CPlayerSlot(player->GetSlot()));