mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
FormattedMessage/DebugConsole performance improvements (#5244)
* Add VisibilityChanged virtual to Control * Defer updating invisible OutputPanels on UIScale change DebugConsole falls under this when not hidden, and it significantly improves perf of e.g. resizing the window when there's a lot of stuff in there. * Avoid redundant UI Scale updates on window resize. Window resizing can change the UI scale, due to the auto-scaling system. This system had multiple perf issues: UI scale was set and propagated even if it didn't change (system disabled, not effective, etc). This was just wasted processing. UI scale was updated for every window resize event. When the game is lagging (due to the aforementioned UI scale updates being expensive...) this means multiple window resize events in a single frame ALL cause a UI scale update, which is useless. UI scale updates from resizing now avoid doing *nothing* and are deferred until later in the frame for natural batching. * Reduce allocations/memory usage of various rich-text related things Just allocate a buncha dictionaries what could possibly go wrong. I kept to non-breaking-changes which means this couldn't as effective as it should be. There's some truly repulsive stuff here. Ugh. * Cap debug console content size. It's a CVar. OutputPanel has been switched to use a new RingBufferList datastructure to make removal of the oldest entry efficient. --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
095fe9d60f
commit
69c1161562
@@ -39,7 +39,7 @@ END TEMPLATE-->
|
||||
|
||||
### New features
|
||||
|
||||
*None yet*
|
||||
* `Control.VisibilityChanged()` virtual function.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -47,6 +47,9 @@ END TEMPLATE-->
|
||||
|
||||
### Other
|
||||
|
||||
* Fix internal networking logic
|
||||
* Updates of `OutputPanel` contents caused by change in UI scale are now deferred until visible. Especially important to avoid updates from debug console.
|
||||
* Debug console is now limited to only keep `con.max_entries` entries.
|
||||
* Non-existent resources are cached by `IResourceCache.TryGetResource`. This avoids the game constantly trying to re-load non-existent resources in common patterns such as UI theme texture fallbacks.
|
||||
* Default IPv4 MTU has been lowered to 700.
|
||||
|
||||
|
||||
@@ -212,9 +212,18 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this control's visibility in the control tree changed.
|
||||
/// </summary>
|
||||
protected virtual void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
}
|
||||
|
||||
private void _propagateVisibilityChanged(bool newVisible)
|
||||
{
|
||||
VisibilityChanged(newVisible);
|
||||
OnVisibilityChanged?.Invoke(this);
|
||||
|
||||
if (!VisibleInTree)
|
||||
{
|
||||
UserInterfaceManagerInternal.ControlHidden(this);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public const string StylePropertyStyleBox = "stylebox";
|
||||
|
||||
private readonly List<RichTextEntry> _entries = new();
|
||||
private readonly RingBufferList<RichTextEntry> _entries = new();
|
||||
private bool _isAtBottom = true;
|
||||
|
||||
private int _totalContentHeight;
|
||||
@@ -30,6 +29,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public bool ScrollFollowing { get; set; } = true;
|
||||
|
||||
private bool _invalidOnVisible;
|
||||
|
||||
public OutputPanel()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -45,6 +46,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
|
||||
}
|
||||
|
||||
public int EntryCount => _entries.Count;
|
||||
|
||||
public StyleBox? StyleBoxOverride
|
||||
{
|
||||
get => _styleBoxOverride;
|
||||
@@ -91,7 +94,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
var entry = new RichTextEntry(message, this, _tagManager, null);
|
||||
|
||||
entry.Update(_getFont(), _getContentBox().Width, UIScale);
|
||||
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
|
||||
|
||||
_entries.Add(entry);
|
||||
var font = _getFont();
|
||||
@@ -134,7 +137,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
// So when a new color tag gets hit this stack gets the previous color pushed on.
|
||||
var context = new MarkupDrawingContext(2);
|
||||
|
||||
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
|
||||
foreach (ref var entry in _entries)
|
||||
{
|
||||
if (entryOffset + entry.Height < 0)
|
||||
{
|
||||
@@ -147,7 +150,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
break;
|
||||
}
|
||||
|
||||
entry.Draw(handle, font, contentBox, entryOffset, context, UIScale);
|
||||
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
|
||||
|
||||
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
|
||||
}
|
||||
@@ -185,9 +188,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_totalContentHeight = 0;
|
||||
var font = _getFont();
|
||||
var sizeX = _getContentBox().Width;
|
||||
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
|
||||
foreach (ref var entry in _entries)
|
||||
{
|
||||
entry.Update(font, sizeX, UIScale);
|
||||
entry.Update(_tagManager, font, sizeX, UIScale);
|
||||
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
|
||||
}
|
||||
|
||||
@@ -239,7 +242,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected internal override void UIScaleChanged()
|
||||
{
|
||||
_invalidateEntries();
|
||||
// If this control isn't visible, don't invalidate entries immediately.
|
||||
// This saves invalidating the debug console if it's hidden,
|
||||
// which is a huge boon as auto-scaling changes UI scale a lot in that scenario.
|
||||
if (!VisibleInTree)
|
||||
_invalidOnVisible = true;
|
||||
else
|
||||
_invalidateEntries();
|
||||
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
@@ -257,5 +266,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
// existing ones were valid when the UI scale was set.
|
||||
_invalidateEntries();
|
||||
}
|
||||
|
||||
protected override void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
if (newVisible && _invalidOnVisible)
|
||||
{
|
||||
_invalidateEntries();
|
||||
_invalidOnVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
var font = _getFont();
|
||||
_entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
|
||||
|
||||
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
|
||||
}
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
|
||||
@@ -13,6 +13,12 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
public override float UIScale => UIScaleSet;
|
||||
internal float UIScaleSet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set after the window is resized, to batch up UI scale updates on window resizes.
|
||||
/// </summary>
|
||||
internal bool UIScaleUpdateNeeded { get; set; }
|
||||
|
||||
public override IClydeWindow Window { get; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
@@ -51,6 +52,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
|
||||
private readonly ISawmill _logger;
|
||||
|
||||
private int _maxEntries;
|
||||
|
||||
public DebugConsole()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -78,6 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString += OnAddString;
|
||||
_consoleHost.AddFormatted += OnAddFormatted;
|
||||
_consoleHost.ClearText += OnClearText;
|
||||
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
|
||||
|
||||
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
|
||||
}
|
||||
@@ -89,10 +93,17 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString -= OnAddString;
|
||||
_consoleHost.AddFormatted -= OnAddFormatted;
|
||||
_consoleHost.ClearText -= OnClearText;
|
||||
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
|
||||
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
|
||||
}
|
||||
|
||||
private void MaxEntriesChanged(int value)
|
||||
{
|
||||
_maxEntries = value;
|
||||
TrimExtraOutputEntries();
|
||||
}
|
||||
|
||||
private void OnClearText(object? _, EventArgs args)
|
||||
{
|
||||
Clear();
|
||||
@@ -165,6 +176,15 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
private void _addFormattedLineInternal(FormattedMessage message)
|
||||
{
|
||||
Output.AddMessage(message);
|
||||
TrimExtraOutputEntries();
|
||||
}
|
||||
|
||||
private void TrimExtraOutputEntries()
|
||||
{
|
||||
while (Output.EntryCount > _maxEntries)
|
||||
{
|
||||
Output.RemoveEntry(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void _flushQueue()
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace Robust.Client.UserInterface
|
||||
internal struct RichTextEntry
|
||||
{
|
||||
private readonly Color _defaultColor;
|
||||
private readonly MarkupTagManager _tagManager;
|
||||
private readonly Type[]? _tagsAllowed;
|
||||
|
||||
public readonly FormattedMessage Message;
|
||||
@@ -37,7 +36,7 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
public ValueList<int> LineBreaks;
|
||||
|
||||
private readonly Dictionary<int, Control> _tagControls = new();
|
||||
private readonly Dictionary<int, Control>? _tagControls;
|
||||
|
||||
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
@@ -46,23 +45,26 @@ namespace Robust.Client.UserInterface
|
||||
Width = 0;
|
||||
LineBreaks = default;
|
||||
_defaultColor = defaultColor ?? new(200, 200, 200);
|
||||
_tagManager = tagManager;
|
||||
_tagsAllowed = tagsAllowed;
|
||||
Dictionary<int, Control>? tagControls = null;
|
||||
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
|
||||
if (node.Name == null)
|
||||
continue;
|
||||
|
||||
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
|
||||
continue;
|
||||
|
||||
parent.Children.Add(control);
|
||||
_tagControls.Add(nodeIndex, control);
|
||||
tagControls ??= new Dictionary<int, Control>();
|
||||
tagControls.Add(nodeIndex, control);
|
||||
}
|
||||
|
||||
_tagControls = tagControls;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -72,7 +74,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
|
||||
/// <param name="uiScale"></param>
|
||||
/// <param name="lineHeightScale"></param>
|
||||
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
|
||||
{
|
||||
// This method is gonna suck due to complexity.
|
||||
// Bear with me here.
|
||||
@@ -91,10 +93,10 @@ namespace Robust.Client.UserInterface
|
||||
// Nodes can change the markup drawing context and return additional text.
|
||||
// It's also possible for nodes to return inline controls. They get treated as one large rune.
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
var text = ProcessNode(node, context);
|
||||
var text = ProcessNode(tagManager, node, context);
|
||||
|
||||
if (!context.Font.TryPeek(out var font))
|
||||
font = defaultFont;
|
||||
@@ -113,7 +115,7 @@ namespace Robust.Client.UserInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
if (ProcessRune(ref this, new Rune(' '), out breakLine))
|
||||
@@ -166,6 +168,7 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
|
||||
public readonly void Draw(
|
||||
MarkupTagManager tagManager,
|
||||
DrawingHandleScreen handle,
|
||||
Font defaultFont,
|
||||
UIBox2 drawBox,
|
||||
@@ -184,10 +187,10 @@ namespace Robust.Client.UserInterface
|
||||
var controlYAdvance = 0f;
|
||||
|
||||
var nodeIndex = -1;
|
||||
foreach (var node in Message.Nodes)
|
||||
foreach (var node in Message)
|
||||
{
|
||||
nodeIndex++;
|
||||
var text = ProcessNode(node, context);
|
||||
var text = ProcessNode(tagManager, node, context);
|
||||
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
|
||||
{
|
||||
color = _defaultColor;
|
||||
@@ -210,7 +213,7 @@ namespace Robust.Client.UserInterface
|
||||
globalBreakCounter += 1;
|
||||
}
|
||||
|
||||
if (!_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
|
||||
continue;
|
||||
|
||||
var invertedScale = 1f / uiScale;
|
||||
@@ -223,24 +226,22 @@ namespace Robust.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context)
|
||||
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
|
||||
{
|
||||
// If a nodes name is null it's a text node.
|
||||
if (node.Name == null)
|
||||
return node.Value.StringValue ?? "";
|
||||
|
||||
//Skip the node if there is no markup tag for it.
|
||||
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
|
||||
return "";
|
||||
|
||||
if (!node.Closing)
|
||||
{
|
||||
context.Tags.Add(tag);
|
||||
tag.PushDrawContext(node, context);
|
||||
return tag.TextBefore(node);
|
||||
}
|
||||
|
||||
context.Tags.Remove(tag);
|
||||
tag.PopDrawContext(node, context);
|
||||
return tag.TextAfter(node);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ internal partial class UserInterfaceManager
|
||||
|
||||
private void UpdateUIScale(WindowRoot root)
|
||||
{
|
||||
root.UIScaleSet = CalculateAutoScale(root);
|
||||
var newScale = CalculateAutoScale(root);
|
||||
// ReSharper disable once CompareOfFloatsByEqualityOperator
|
||||
if (newScale == root.UIScaleSet)
|
||||
return;
|
||||
|
||||
root.UIScaleSet = newScale;
|
||||
_propagateUIScaleChanged(root);
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
@@ -142,7 +147,21 @@ internal partial class UserInterfaceManager
|
||||
{
|
||||
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
|
||||
return;
|
||||
UpdateUIScale(root);
|
||||
|
||||
root.UIScaleUpdateNeeded = true;
|
||||
root.InvalidateMeasure();
|
||||
}
|
||||
|
||||
private void CheckRootUIScaleUpdate(WindowRoot root)
|
||||
{
|
||||
if (!root.UIScaleUpdateNeeded)
|
||||
return;
|
||||
|
||||
using (_prof.Group("UIScaleUpdate"))
|
||||
{
|
||||
UpdateUIScale(root);
|
||||
}
|
||||
|
||||
root.UIScaleUpdateNeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,8 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
CheckRootUIScaleUpdate(root);
|
||||
|
||||
using (_prof.Group("Root"))
|
||||
{
|
||||
var totalUpdated = root.DoFrameUpdateRecursive(args);
|
||||
|
||||
@@ -1560,6 +1560,12 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> ConCompletionMargin =
|
||||
CVarDef.Create("con.completion_margin", 3, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of entries stored by the debug console.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ConMaxEntries =
|
||||
CVarDef.Create("con.max_entries", 3_000, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* THREAD
|
||||
*/
|
||||
|
||||
304
Robust.Shared/Collections/RingBufferList.cs
Normal file
304
Robust.Shared/Collections/RingBufferList.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Utility;
|
||||
using ArgumentNullException = System.ArgumentNullException;
|
||||
|
||||
namespace Robust.Shared.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Datastructure that acts like a <see cref="List{T}"/>, but is actually stored as a ring buffer internally.
|
||||
/// This facilitates efficient removal from the start.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of item contained in the collection.</typeparam>
|
||||
internal sealed class RingBufferList<T> : IList<T>
|
||||
{
|
||||
private T[] _items;
|
||||
private int _read;
|
||||
private int _write;
|
||||
|
||||
public RingBufferList(int capacity)
|
||||
{
|
||||
_items = new T[capacity];
|
||||
}
|
||||
|
||||
public RingBufferList()
|
||||
{
|
||||
_items = [];
|
||||
}
|
||||
|
||||
public int Capacity => _items.Length;
|
||||
|
||||
private bool IsFull => _items.Length == 0 || NextIndex(_write) == _read;
|
||||
|
||||
public void Add(T item)
|
||||
{
|
||||
if (IsFull)
|
||||
Expand();
|
||||
|
||||
DebugTools.Assert(!IsFull);
|
||||
|
||||
_items[_write] = item;
|
||||
_write = NextIndex(_write);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_read = 0;
|
||||
_write = 0;
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
Array.Clear(_items);
|
||||
}
|
||||
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return IndexOf(item) >= 0;
|
||||
}
|
||||
|
||||
public void CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(array);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
|
||||
|
||||
CopyTo(array.AsSpan(arrayIndex));
|
||||
}
|
||||
|
||||
private void CopyTo(Span<T> dest)
|
||||
{
|
||||
if (dest.Length < Count)
|
||||
throw new ArgumentException("Not enough elements in destination!");
|
||||
|
||||
var i = 0;
|
||||
foreach (var item in this)
|
||||
{
|
||||
dest[i++] = item;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(T item)
|
||||
{
|
||||
var index = IndexOf(item);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
var length = _write - _read;
|
||||
if (length >= 0)
|
||||
return length;
|
||||
|
||||
return length + _items.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public int IndexOf(T item)
|
||||
{
|
||||
var i = 0;
|
||||
foreach (var containedItem in this)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(item, containedItem))
|
||||
return i;
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Insert(int index, T item)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
var length = Count;
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, length);
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
_items[_read] = default!;
|
||||
|
||||
_read = NextIndex(_read);
|
||||
}
|
||||
else if (index == length - 1)
|
||||
{
|
||||
_write = WrapInv(_write - 1);
|
||||
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
_items[_write] = default!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If past me had better foresight I wouldn't be spending so much effort writing this right now.
|
||||
|
||||
var realIdx = RealIndex(index);
|
||||
var origValue = _items[realIdx];
|
||||
T result;
|
||||
|
||||
if (realIdx < _read)
|
||||
{
|
||||
// Scenario one: to-remove index is after break.
|
||||
// One shift is needed.
|
||||
// v
|
||||
// X X X O X X
|
||||
// W R
|
||||
DebugTools.Assert(_write < _read);
|
||||
|
||||
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
|
||||
}
|
||||
else if (_write < _read)
|
||||
{
|
||||
// Scenario two: to-remove index is before break, but write is after.
|
||||
// Two shifts are needed.
|
||||
// v
|
||||
// X O X X X X
|
||||
// W R
|
||||
|
||||
var fromEnd = ShiftDown(_items.AsSpan(0, _write), default!);
|
||||
result = ShiftDown(_items.AsSpan(realIdx), fromEnd);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scenario two: array is contiguous.
|
||||
// One shift is needed.
|
||||
// v
|
||||
// X X X X O O
|
||||
// R W
|
||||
|
||||
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
|
||||
}
|
||||
|
||||
// Just make sure we didn't bulldozer something.
|
||||
DebugTools.Assert(EqualityComparer<T>.Default.Equals(origValue, result));
|
||||
|
||||
_write = WrapInv(_write - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static T ShiftDown(Span<T> span, T substitution)
|
||||
{
|
||||
if (span.Length == 0)
|
||||
return substitution;
|
||||
|
||||
var first = span[0];
|
||||
span[1..].CopyTo(span[..^1]);
|
||||
span[^1] = substitution!;
|
||||
return first;
|
||||
}
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get => GetSlot(index);
|
||||
set => GetSlot(index) = value;
|
||||
}
|
||||
|
||||
private ref T GetSlot(int index)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
|
||||
|
||||
return ref _items[RealIndex(index)];
|
||||
}
|
||||
|
||||
private int RealIndex(int index)
|
||||
{
|
||||
return Wrap(index + _read);
|
||||
}
|
||||
|
||||
private int NextIndex(int index) => Wrap(index + 1);
|
||||
|
||||
private int Wrap(int index)
|
||||
{
|
||||
if (index >= _items.Length)
|
||||
index -= _items.Length;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private int WrapInv(int index)
|
||||
{
|
||||
if (index < 0)
|
||||
index = _items.Length - 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private void Expand()
|
||||
{
|
||||
var prevSize = _items.Length;
|
||||
var newSize = Math.Max(4, prevSize * 2);
|
||||
Array.Resize(ref _items, newSize);
|
||||
|
||||
if (_write >= _read)
|
||||
return;
|
||||
|
||||
// Write is behind read pointer, so we need to copy the items to be after the read pointer.
|
||||
var toCopy = _items.AsSpan(0, _write);
|
||||
var copyDest = _items.AsSpan(prevSize);
|
||||
toCopy.CopyTo(copyDest);
|
||||
|
||||
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
|
||||
toCopy.Clear();
|
||||
|
||||
_write += prevSize;
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly RingBufferList<T> _ringBufferList;
|
||||
private int _readPos;
|
||||
|
||||
internal Enumerator(RingBufferList<T> ringBufferList)
|
||||
{
|
||||
_ringBufferList = ringBufferList;
|
||||
_readPos = _ringBufferList._read - 1;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_readPos = _ringBufferList.NextIndex(_readPos);
|
||||
return _readPos != _ringBufferList._write;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
this = new Enumerator(_ringBufferList);
|
||||
}
|
||||
|
||||
public ref T Current => ref _ringBufferList._items[_readPos];
|
||||
|
||||
T IEnumerator<T>.Current => Current;
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,26 +16,30 @@ namespace Robust.Shared.Utility;
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class FormattedMessage
|
||||
public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
|
||||
{
|
||||
public static FormattedMessage Empty => new();
|
||||
|
||||
/// <summary>
|
||||
/// The list of nodes the formatted message is made out of
|
||||
/// </summary>
|
||||
public IReadOnlyList<MarkupNode> Nodes => _nodes.AsReadOnly();
|
||||
public IReadOnlyList<MarkupNode> Nodes => _nodes;
|
||||
|
||||
/// <summary>
|
||||
/// true if the formatted message doesn't contain any nodes
|
||||
/// </summary>
|
||||
public bool IsEmpty => _nodes.Count == 0;
|
||||
|
||||
public int Count => _nodes.Count;
|
||||
|
||||
public MarkupNode this[int index] => _nodes[index];
|
||||
|
||||
private readonly List<MarkupNode> _nodes;
|
||||
|
||||
/// <summary>
|
||||
/// Used for inserting the correct closing node when calling <see cref="Pop"/>
|
||||
/// </summary>
|
||||
private readonly Stack<MarkupNode> _openNodeStack = new();
|
||||
private Stack<MarkupNode>? _openNodeStack;
|
||||
|
||||
public FormattedMessage()
|
||||
{
|
||||
@@ -199,6 +203,7 @@ public sealed partial class FormattedMessage
|
||||
return;
|
||||
}
|
||||
|
||||
_openNodeStack ??= new Stack<MarkupNode>();
|
||||
_openNodeStack.Push(markupNode);
|
||||
}
|
||||
|
||||
@@ -207,7 +212,7 @@ public sealed partial class FormattedMessage
|
||||
/// </summary>
|
||||
public void Pop()
|
||||
{
|
||||
if (!_openNodeStack.TryPop(out var node))
|
||||
if (_openNodeStack == null || !_openNodeStack.TryPop(out var node))
|
||||
return;
|
||||
|
||||
_nodes.Add(new MarkupNode(node.Name, null, null, true));
|
||||
@@ -238,6 +243,16 @@ public sealed partial class FormattedMessage
|
||||
return new FormattedMessageRuneEnumerator(this);
|
||||
}
|
||||
|
||||
public NodeEnumerator GetEnumerator()
|
||||
{
|
||||
return new NodeEnumerator(_nodes.GetEnumerator());
|
||||
}
|
||||
|
||||
IEnumerator<MarkupNode> IEnumerable<MarkupNode>.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <returns>The string without markup tags.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -252,6 +267,11 @@ public sealed partial class FormattedMessage
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <returns>The string without filtering out markup tags.</returns>
|
||||
public string ToMarkup()
|
||||
{
|
||||
@@ -261,13 +281,13 @@ public sealed partial class FormattedMessage
|
||||
public struct FormattedMessageRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
|
||||
{
|
||||
private readonly FormattedMessage _msg;
|
||||
private IEnumerator<MarkupNode> _tagEnumerator;
|
||||
private List<MarkupNode>.Enumerator _tagEnumerator;
|
||||
private StringRuneEnumerator _runeEnumerator;
|
||||
|
||||
internal FormattedMessageRuneEnumerator(FormattedMessage msg)
|
||||
{
|
||||
_msg = msg;
|
||||
_tagEnumerator = msg.Nodes.GetEnumerator();
|
||||
_tagEnumerator = msg._nodes.GetEnumerator();
|
||||
// Rune enumerator will immediately give false on first iteration so I dont' need to special case anything.
|
||||
_runeEnumerator = "".EnumerateRunes();
|
||||
}
|
||||
@@ -301,7 +321,7 @@ public sealed partial class FormattedMessage
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_tagEnumerator = _msg.Nodes.GetEnumerator();
|
||||
_tagEnumerator = _msg._nodes.GetEnumerator();
|
||||
_runeEnumerator = "".EnumerateRunes();
|
||||
}
|
||||
|
||||
@@ -313,4 +333,33 @@ public sealed partial class FormattedMessage
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodeEnumerator : IEnumerator<MarkupNode>
|
||||
{
|
||||
private List<MarkupNode>.Enumerator _enumerator;
|
||||
|
||||
internal NodeEnumerator(List<MarkupNode>.Enumerator enumerator)
|
||||
{
|
||||
_enumerator = enumerator;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
return _enumerator.MoveNext();
|
||||
}
|
||||
|
||||
void IEnumerator.Reset()
|
||||
{
|
||||
((IEnumerator) _enumerator).Reset();
|
||||
}
|
||||
|
||||
public MarkupNode Current => _enumerator.Current;
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_enumerator.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class MarkupNode : IComparable<MarkupNode>
|
||||
{
|
||||
public readonly string? Name;
|
||||
public readonly MarkupParameter Value;
|
||||
public readonly Dictionary<string, MarkupParameter> Attributes = new();
|
||||
public readonly Dictionary<string, MarkupParameter> Attributes;
|
||||
public readonly bool Closing;
|
||||
|
||||
/// <summary>
|
||||
@@ -20,6 +20,7 @@ public sealed class MarkupNode : IComparable<MarkupNode>
|
||||
/// <param name="text">The plaintext the tag will consist of</param>
|
||||
public MarkupNode(string text)
|
||||
{
|
||||
Attributes = new Dictionary<string, MarkupParameter>();
|
||||
Value = new MarkupParameter(text);
|
||||
}
|
||||
|
||||
|
||||
95
Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs
Normal file
95
Robust.UnitTesting/Shared/Collections/RingBufferListTest.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Collections;
|
||||
|
||||
namespace Robust.UnitTesting.Shared.Collections;
|
||||
|
||||
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
|
||||
[TestFixture, TestOf(typeof(RingBufferList<>))]
|
||||
public sealed class RingBufferListTest
|
||||
{
|
||||
[Test]
|
||||
public void TestBasicAdd()
|
||||
{
|
||||
var list = new RingBufferList<int>();
|
||||
list.Add(1);
|
||||
list.Add(2);
|
||||
list.Add(3);
|
||||
|
||||
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicAddAfterWrap()
|
||||
{
|
||||
var list = new RingBufferList<int>(6);
|
||||
list.Add(1);
|
||||
list.Add(2);
|
||||
list.Add(3);
|
||||
list.RemoveAt(0);
|
||||
list.Add(4);
|
||||
list.Add(5);
|
||||
list.Add(6);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// Ensure wrapping properly happened and we didn't expand.
|
||||
// (one slot is wasted by nature of implementation)
|
||||
Assert.That(list.Capacity, NUnit.Framework.Is.EqualTo(6));
|
||||
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] { 2, 3, 4, 5, 6 }));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMiddleRemoveAtScenario1()
|
||||
{
|
||||
var list = new RingBufferList<int>(6);
|
||||
list.Add(-1);
|
||||
list.Add(-1);
|
||||
list.Add(-1);
|
||||
list.Add(-1);
|
||||
list.Add(1);
|
||||
list.RemoveAt(0);
|
||||
list.RemoveAt(0);
|
||||
list.RemoveAt(0);
|
||||
list.RemoveAt(0);
|
||||
list.Add(2);
|
||||
list.Add(3);
|
||||
list.Add(4);
|
||||
list.Add(5);
|
||||
list.Remove(4);
|
||||
|
||||
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMiddleRemoveAtScenario2()
|
||||
{
|
||||
var list = new RingBufferList<int>(6);
|
||||
list.Add(-1);
|
||||
list.Add(-1);
|
||||
list.Add(1);
|
||||
list.RemoveAt(0);
|
||||
list.RemoveAt(0);
|
||||
list.Add(2);
|
||||
list.Add(3);
|
||||
list.Add(4);
|
||||
list.Add(5);
|
||||
list.Remove(3);
|
||||
|
||||
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 4, 5}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMiddleRemoveAtScenario3()
|
||||
{
|
||||
var list = new RingBufferList<int>(6);
|
||||
list.Add(1);
|
||||
list.Add(2);
|
||||
list.Add(3);
|
||||
list.Add(4);
|
||||
list.Add(5);
|
||||
list.Remove(4);
|
||||
|
||||
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user