Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public IReadOnlyList<IMenuOption> Options {
// public event EventHandler<MenuEventArgs>? OptionLeaving;

private readonly ISwiftlyCore core;
private readonly List<IMenuOption> options = new();
private readonly List<IMenuOption> options = [];
private readonly Lock optionsLock = new(); // Lock for synchronizing modifications to the `options`
private readonly ConcurrentDictionary<int, int> selectedOptionIndex = new(); // Stores the currently selected option index for each player
// NOTE: Menu selection movement is entirely driven by changes to `desiredOptionIndex` (independent of any other variables)
Expand All @@ -118,9 +118,9 @@ public IReadOnlyList<IMenuOption> Options {
private readonly ConcurrentDictionary<int, CancellationTokenSource> autoCloseCancelTokens = new();

// private readonly ConcurrentDictionary<int, string> renderCache = new();
private readonly CancellationTokenSource renderLoopCancellationTokenSource = new();
private readonly ConcurrentDictionary<Task, CancellationTokenSource> renderLoopTasks = new();
private readonly Lock viewersLock = new();
private readonly HashSet<int> viewers = new();
private readonly HashSet<int> viewers = [];

private volatile bool disposed;

Expand Down Expand Up @@ -154,27 +154,6 @@ public MenuAPI( ISwiftlyCore core, MenuConfiguration configuration, MenuKeybindO
// maxDisplayLines = 0;

// core.Event.OnTick += OnTick;

_ = Task.Run(async () =>
{
var token = renderLoopCancellationTokenSource.Token;
var delayMilliseconds = (int)(1000f / 64f / 2f);
while (!token.IsCancellationRequested || disposed)
{
try
{
OnRender();
await Task.Delay(delayMilliseconds, token);
}
catch (OperationCanceledException)
{
break;
}
catch
{
}
}
}, renderLoopCancellationTokenSource.Token);
}

~MenuAPI()
Expand Down Expand Up @@ -222,8 +201,15 @@ public void Dispose()

// core.Event.OnTick -= OnTick;

renderLoopCancellationTokenSource?.Cancel();
renderLoopCancellationTokenSource?.Dispose();
renderLoopTasks.Keys.ToList().ForEach(task =>
{
if (renderLoopTasks.TryRemove(task, out var cts))
{
cts.Cancel();
cts.Dispose();
}
});
renderLoopTasks.Clear();

disposed = true;
GC.SuppressFinalize(this);
Expand Down Expand Up @@ -478,6 +464,38 @@ public void ShowForPlayer( IPlayer player )

if (viewers.Count == 1)
{
renderLoopTasks.Keys.ToList().ForEach(task =>
{
if (renderLoopTasks.TryRemove(task, out var cts))
{
cts.Cancel();
cts.Dispose();
}
});

var cts = new CancellationTokenSource();
var token = cts.Token;
var delayMilliseconds = (int)(1000f / 64f);
var task = Task.Run(async () =>
{
while (!token.IsCancellationRequested && !disposed)
{
try
{
OnRender();
await Task.Delay(delayMilliseconds, token);
}
catch (OperationCanceledException)
{
break;
}
catch
{
}
}
}, token);
_ = renderLoopTasks.TryAdd(task, cts);

lock (optionsLock)
{
options.OfType<OptionsBase.MenuOptionBase>().ToList().ForEach(option => option.ResumeTextAnimation());
Expand Down Expand Up @@ -526,6 +544,15 @@ public void HideForPlayer( IPlayer player )

if (viewers.Count == 0)
{
renderLoopTasks.Keys.ToList().ForEach(task =>
{
if (renderLoopTasks.TryRemove(task, out var cts))
{
cts.Cancel();
cts.Dispose();
}
});

lock (optionsLock)
{
options.OfType<OptionsBase.MenuOptionBase>().ToList().ForEach(option => option.PauseTextAnimation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ namespace SwiftlyS2.Core.Menus.OptionsBase.Helpers;
internal sealed partial class TextStyleProcessor : IDisposable
{
private readonly ConcurrentDictionary<string, int> scrollOffsets = new();
private readonly ConcurrentDictionary<string, string> staticStyleCache = new();

private volatile bool disposed;

public TextStyleProcessor()
{
disposed = false;
scrollOffsets.Clear();
staticStyleCache.Clear();
}

~TextStyleProcessor()
Expand All @@ -32,6 +34,7 @@ public void Dispose()

// Console.WriteLine($"{GetType().Name} has been disposed.");
scrollOffsets.Clear();
staticStyleCache.Clear();

disposed = true;
GC.SuppressFinalize(this);
Expand All @@ -42,19 +45,45 @@ public void Dispose()

public (string styledText, int scrollOffset) ApplyHorizontalStyle( string text, MenuOptionTextStyle textStyle, float maxWidth )
{
return string.IsNullOrWhiteSpace(text)
? (text, -1)
: Helper.EstimateTextWidth(StripHtmlTags(text)) <= maxWidth
? (text, -1)
: textStyle switch {
MenuOptionTextStyle.TruncateEnd => TruncateTextEnd(text, maxWidth),
MenuOptionTextStyle.TruncateBothEnds => TruncateTextBothEnds(text, maxWidth),
MenuOptionTextStyle.ScrollLeftFade => ScrollTextWithFade(text, maxWidth, true),
MenuOptionTextStyle.ScrollRightFade => ScrollTextWithFade(text, maxWidth, false),
MenuOptionTextStyle.ScrollLeftLoop => ScrollTextWithLoop($"{text.TrimEnd()} ", maxWidth, true),
MenuOptionTextStyle.ScrollRightLoop => ScrollTextWithLoop($" {text.TrimStart()}", maxWidth, false),
_ => (text, -1)
};
if (string.IsNullOrWhiteSpace(text))
{
return (text, -1);
}

if (Helper.EstimateTextWidth(StripHtmlTags(text)) <= maxWidth)
{
return (text, -1);
}

if (textStyle == MenuOptionTextStyle.TruncateEnd || textStyle == MenuOptionTextStyle.TruncateBothEnds)
{
// Cache static styles (TruncateEnd and TruncateBothEnds)
var cacheKey = $"{text}_{textStyle}_{maxWidth}";
if (staticStyleCache.TryGetValue(cacheKey, out var cachedStyledText))
{
return (cachedStyledText, -1);
}

var (styledText, scrollOffset) = textStyle switch {
MenuOptionTextStyle.TruncateEnd => TruncateTextEnd(text, maxWidth),
MenuOptionTextStyle.TruncateBothEnds => TruncateTextBothEnds(text, maxWidth),
_ => (text, -1)
};

_ = staticStyleCache.TryAdd(cacheKey, styledText);
return (styledText, scrollOffset);
}
else
{
// Dynamic styles (scrolling animations)
return textStyle switch {
MenuOptionTextStyle.ScrollLeftFade => ScrollTextWithFade(text, maxWidth, true),
MenuOptionTextStyle.ScrollRightFade => ScrollTextWithFade(text, maxWidth, false),
MenuOptionTextStyle.ScrollLeftLoop => ScrollTextWithLoop($"{text.TrimEnd()} ", maxWidth, true),
MenuOptionTextStyle.ScrollRightLoop => ScrollTextWithLoop($" {text.TrimStart()}", maxWidth, false),
_ => (text, -1)
};
}
}

private (string styledText, int scrollOffset) ScrollTextWithFade( string text, float maxWidth, bool scrollLeft )
Expand Down
10 changes: 9 additions & 1 deletion managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,20 @@ void ShowPlayerList()
void ShowServerStatus()
{
var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime;
ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out var availableCompletionPortThreads);
ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var maxCompletionPortThreads);
var busyWorkerThreads = maxWorkerThreads - availableWorkerThreads;
var processThreadCount = System.Diagnostics.Process.GetCurrentProcess().Threads.Count;

var output = string.Join("\n", [
$"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s",
$"Managed Heap Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB",
$"Process Threads: {processThreadCount}",
$"ThreadPool Worker Threads: {busyWorkerThreads}/{maxWorkerThreads} (Busy/Max)",
$"ThreadPool Completion Port Threads: {maxCompletionPortThreads - availableCompletionPortThreads}/{maxCompletionPortThreads} (Busy/Max)",
$"Loaded Plugins: {pluginManager.GetPlugins().Count}",
$"Players: {core.PlayerManager.PlayerCount}/{core.Engine.GlobalVars.MaxClients}",
$"Map: {core.Engine.GlobalVars.MapName.Value}"
$"Map: {core.Engine.GlobalVars.MapName.Value}",
]);
logger.LogInformation("{Output}", output);
}
Expand Down
Loading