Compare commits

..

4 Commits

Author SHA1 Message Date
Pieter-Jan Briers
5bb3d3c4bc Version: 226.0.2 2024-08-11 19:54:42 +02:00
Pieter-Jan Briers
a9f07dd63c Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:42 +02:00
Pieter-Jan Briers
2423c861c2 Version: 226.0.1 2024-08-11 17:56:05 +02:00
Pieter-Jan Briers
87ffd7b7af Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
(cherry picked from commit db8ba83866c523e08e4fba0b80cd954f4f190613)
2024-08-11 17:56:05 +02:00
44 changed files with 346 additions and 1552 deletions

View File

@@ -43,7 +43,7 @@
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageVersion Include="OpenTK.OpenAL" Version="4.7.7" />
<PackageVersion Include="OpenToolkit.Graphics" Version="4.0.0-pre9.1" />
<PackageVersion Include="Pidgin" Version="3.2.2" />
@@ -56,7 +56,7 @@
<PackageVersion Include="Serilog.Sinks.Loki" Version="4.0.0-beta3" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.2-beta2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,57 +54,10 @@ END TEMPLATE-->
*None yet*
## 226.3.2
## 226.0.2
## 226.3.1
## 226.3.0
### New features
* `System.Collections.IList` and `System.Collections.ICollection` are now sandbox safe, this fixes some collection expression cases.
* The sandboxing system will now report the methods responsible for references to illegal items.
## 226.2.0
### New features
* `Control.VisibilityChanged()` virtual function.
* Add some System.Random methods for NextFloat and NextPolarVector2.
### Bugfixes
* Fixes ContainerSystem failing client-side debug asserts when an entity gets unanchored & inserted into a container on the same tick.
* Remove potential race condition on server startup from invoking ThreadPool.SetMinThreads.
### Other
* Increase default value of res.rsi_atlas_size.
* 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.
* Update Robust.LoaderApi.
### Internal
* Split out PVS serialization from compression and sending game states.
* Turn broadphase contacts into an IParallelRobustJob and remove unnecessary GetMapEntityIds for every contact.
## 226.1.0
### New features
* Add some GetLocalEntitiesIntersecting methods for `Entity<T>`.
### Other
* Fix internal networking logic
## 226.0.1
## 226.0.0

View File

@@ -26,8 +26,7 @@ public sealed class DefaultSQLConfig : IConfig
public IEnumerable<IExporter> GetExporters()
{
//yield return SQLExporter.Default;
yield break;
yield return SQLExporter.Default;
}
public IEnumerable<IColumnProvider> GetColumnProviders() => DefaultConfig.Instance.GetColumnProviders();

View File

@@ -15,10 +15,11 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Npgsql;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandlers;
using Npgsql.Internal.TypeHandling;
namespace Robust.Benchmarks.Exporters;
/*
public sealed class SQLExporter : IExporter
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions
@@ -97,9 +98,7 @@ public sealed class SQLExporter : IExporter
public string Name => "sql";
}
*/
/*
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
@@ -139,7 +138,6 @@ class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
=> null; // Let the built-in resolver do this
}
}
*/
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
{

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.ResourceManagement
{
sawmill.Debug("Preloading textures...");
var sw = Stopwatch.StartNew();
var resList = GetTypeData<TextureResource>().Resources;
var resList = GetTypeDict<TextureResource>();
var texList = _manager.ContentFindFiles("/Textures/")
// Skip PNG files inside RSIs.
@@ -119,7 +119,7 @@ namespace Robust.Client.ResourceManagement
private void PreloadRsis(ISawmill sawmill)
{
var sw = Stopwatch.StartNew();
var resList = GetTypeData<RSIResource>().Resources;
var resList = GetTypeDict<RSIResource>();
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))

View File

@@ -17,7 +17,9 @@ namespace Robust.Client.ResourceManagement;
/// </summary>
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
{
private readonly Dictionary<Type, TypeData> _cachedResources = new();
private readonly Dictionary<Type, Dictionary<ResPath, BaseResource>> _cachedResources =
new();
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
public T GetResource<T>(string path, bool useFallback = true) where T : BaseResource, new()
@@ -27,8 +29,8 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public T GetResource<T>(ResPath path, bool useFallback = true) where T : BaseResource, new()
{
var cache = GetTypeData<T>();
if (cache.Resources.TryGetValue(path, out var cached))
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
{
return (T) cached;
}
@@ -38,7 +40,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
{
var dependencies = IoCManager.Instance!;
resource.Load(dependencies, path);
cache.Resources[path] = resource;
cache[path] = resource;
return resource;
}
catch (Exception e)
@@ -65,31 +67,24 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public bool TryGetResource<T>(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new()
{
var cache = GetTypeData<T>();
if (cache.Resources.TryGetValue(path, out var cached))
var cache = GetTypeDict<T>();
if (cache.TryGetValue(path, out var cached))
{
resource = (T) cached;
return true;
}
if (cache.NonExistent.Contains(path))
{
resource = null;
return false;
}
var _resource = new T();
try
{
var dependencies = IoCManager.Instance!;
_resource.Load(dependencies, path);
resource = _resource;
cache.Resources[path] = resource;
cache[path] = resource;
return true;
}
catch (FileNotFoundException)
{
cache.NonExistent.Add(path);
resource = null;
return false;
}
@@ -114,9 +109,9 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public void ReloadResource<T>(ResPath path) where T : BaseResource, new()
{
var cache = GetTypeData<T>();
var cache = GetTypeDict<T>();
if (!cache.Resources.TryGetValue(path, out var res))
if (!cache.TryGetValue(path, out var res))
{
return;
}
@@ -150,7 +145,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public void CacheResource<T>(ResPath path, T resource) where T : BaseResource, new()
{
GetTypeData<T>().Resources[path] = resource;
GetTypeDict<T>()[path] = resource;
}
public T GetFallback<T>() where T : BaseResource, new()
@@ -173,7 +168,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
public IEnumerable<KeyValuePair<ResPath, T>> GetAllResources<T>() where T : BaseResource, new()
{
return GetTypeData<T>().Resources.Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
return GetTypeDict<T>().Select(p => new KeyValuePair<ResPath, T>(p.Key, (T) p.Value));
}
public event Action<TextureLoadedEventArgs>? OnRawTextureLoaded;
@@ -198,7 +193,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
if (disposing)
{
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values))
foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Values))
{
res.Dispose();
}
@@ -215,9 +210,15 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
#endregion IDisposable Members
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TypeData GetTypeData<T>()
protected Dictionary<ResPath, BaseResource> GetTypeDict<T>()
{
return _cachedResources.GetOrNew(typeof(T));
if (!_cachedResources.TryGetValue(typeof(T), out var ret))
{
ret = new Dictionary<ResPath, BaseResource>();
_cachedResources.Add(typeof(T), ret);
}
return ret;
}
public void TextureLoaded(TextureLoadedEventArgs eventArgs)
@@ -229,13 +230,4 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
{
OnRsiLoaded?.Invoke(eventArgs);
}
private sealed class TypeData
{
public readonly Dictionary<ResPath, BaseResource> Resources = new();
// List of resources which DON'T exist.
// Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk.
public readonly HashSet<ResPath> NonExistent = new();
}
}

View File

@@ -212,18 +212,9 @@ 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);

View File

@@ -15,8 +15,6 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePseudoClassHover = "hover";
public const string StylePseudoClassDisabled = "disabled";
public StyleBox? StyleBoxOverride { get; set; }
public ContainerButton()
{
DrawModeChanged();
@@ -26,11 +24,6 @@ namespace Robust.Client.UserInterface.Controls
{
get
{
if (StyleBoxOverride != null)
{
return StyleBoxOverride;
}
if (TryGetStyleProperty<StyleBox>(StylePropertyStyleBox, out var box))
{
return box;

View File

@@ -1,8 +1,9 @@
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;
@@ -19,7 +20,7 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyStyleBox = "stylebox";
private readonly RingBufferList<RichTextEntry> _entries = new();
private readonly List<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
private int _totalContentHeight;
@@ -29,8 +30,6 @@ namespace Robust.Client.UserInterface.Controls
public bool ScrollFollowing { get; set; } = true;
private bool _invalidOnVisible;
public OutputPanel()
{
IoCManager.InjectDependencies(this);
@@ -46,8 +45,6 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
}
public int EntryCount => _entries.Count;
public StyleBox? StyleBoxOverride
{
get => _styleBoxOverride;
@@ -94,7 +91,7 @@ namespace Robust.Client.UserInterface.Controls
{
var entry = new RichTextEntry(message, this, _tagManager, null);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
entry.Update(_getFont(), _getContentBox().Width, UIScale);
_entries.Add(entry);
var font = _getFont();
@@ -137,7 +134,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 _entries)
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
{
if (entryOffset + entry.Height < 0)
{
@@ -150,7 +147,7 @@ namespace Robust.Client.UserInterface.Controls
break;
}
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entry.Draw(handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -188,9 +185,9 @@ namespace Robust.Client.UserInterface.Controls
_totalContentHeight = 0;
var font = _getFont();
var sizeX = _getContentBox().Width;
foreach (ref var entry in _entries)
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
{
entry.Update(_tagManager, font, sizeX, UIScale);
entry.Update(font, sizeX, UIScale);
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -242,13 +239,7 @@ namespace Robust.Client.UserInterface.Controls
protected internal override void UIScaleChanged()
{
// 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();
_invalidateEntries();
base.UIScaleChanged();
}
@@ -266,14 +257,5 @@ 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;
}
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
}
var font = _getFont();
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
_entry.Update(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(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -65,10 +65,6 @@ namespace Robust.Client.UserInterface.Controls
}
}
public StyleBox? PanelStyleBoxOverride { get; set; }
public Color? TabFontColorOverride { get; set; }
public Color? TabFontColorInactiveOverride { get; set; }
public event Action<int>? OnTabChanged;
public TabContainer()
@@ -365,9 +361,6 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorActive()
{
if (TabFontColorOverride != null)
return TabFontColorOverride.Value;
if (TryGetStyleProperty(stylePropertyTabFontColor, out Color color))
{
return color;
@@ -378,9 +371,6 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private Color _getTabFontColorInactive()
{
if (TabFontColorInactiveOverride != null)
return TabFontColorInactiveOverride.Value;
if (TryGetStyleProperty(StylePropertyTabFontColorInactive, out Color color))
{
return color;
@@ -391,9 +381,6 @@ namespace Robust.Client.UserInterface.Controls
[System.Diagnostics.Contracts.Pure]
private StyleBox? _getPanel()
{
if (PanelStyleBoxOverride != null)
return PanelStyleBoxOverride;
TryGetStyleProperty<StyleBox>(StylePropertyPanelStyleBox, out var box);
return box;
}

View File

@@ -13,12 +13,6 @@ 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>

View File

@@ -7,7 +7,6 @@ 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;
@@ -52,8 +51,6 @@ namespace Robust.Client.UserInterface.CustomControls
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
private readonly ISawmill _logger;
private int _maxEntries;
public DebugConsole()
{
RobustXamlLoader.Load(this);
@@ -81,7 +78,6 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.AddString += OnAddString;
_consoleHost.AddFormatted += OnAddFormatted;
_consoleHost.ClearText += OnClearText;
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
}
@@ -93,17 +89,10 @@ 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();
@@ -176,15 +165,6 @@ 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()

View File

@@ -17,6 +17,7 @@ namespace Robust.Client.UserInterface
internal struct RichTextEntry
{
private readonly Color _defaultColor;
private readonly MarkupTagManager _tagManager;
private readonly Type[]? _tagsAllowed;
public readonly FormattedMessage Message;
@@ -36,7 +37,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control>? _tagControls;
private readonly Dictionary<int, Control> _tagControls = new();
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -45,26 +46,23 @@ 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)
foreach (var node in Message.Nodes)
{
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 ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
_tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
}
/// <summary>
@@ -74,7 +72,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(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -93,10 +91,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)
foreach (var node in Message.Nodes)
{
nodeIndex++;
var text = ProcessNode(tagManager, node, context);
var text = ProcessNode(node, context);
if (!context.Font.TryPeek(out var font))
font = defaultFont;
@@ -115,7 +113,7 @@ namespace Robust.Client.UserInterface
return;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (!_tagControls.TryGetValue(nodeIndex, out var control))
continue;
if (ProcessRune(ref this, new Rune(' '), out breakLine))
@@ -168,7 +166,6 @@ namespace Robust.Client.UserInterface
}
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleScreen handle,
Font defaultFont,
UIBox2 drawBox,
@@ -187,10 +184,10 @@ namespace Robust.Client.UserInterface
var controlYAdvance = 0f;
var nodeIndex = -1;
foreach (var node in Message)
foreach (var node in Message.Nodes)
{
nodeIndex++;
var text = ProcessNode(tagManager, node, context);
var text = ProcessNode(node, context);
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
{
color = _defaultColor;
@@ -213,7 +210,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
if (!_tagControls.TryGetValue(nodeIndex, out var control))
continue;
var invertedScale = 1f / uiScale;
@@ -226,22 +223,24 @@ namespace Robust.Client.UserInterface
}
}
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
private readonly string ProcessNode(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);
}

View File

@@ -123,12 +123,7 @@ internal partial class UserInterfaceManager
private void UpdateUIScale(WindowRoot root)
{
var newScale = CalculateAutoScale(root);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (newScale == root.UIScaleSet)
return;
root.UIScaleSet = newScale;
root.UIScaleSet = CalculateAutoScale(root);
_propagateUIScaleChanged(root);
root.InvalidateMeasure();
}
@@ -147,21 +142,7 @@ internal partial class UserInterfaceManager
{
if (!_windowsToRoot.TryGetValue(windowResizedEventArgs.Window.Id, out var root))
return;
root.UIScaleUpdateNeeded = true;
UpdateUIScale(root);
root.InvalidateMeasure();
}
private void CheckRootUIScaleUpdate(WindowRoot root)
{
if (!root.UIScaleUpdateNeeded)
return;
using (_prof.Group("UIScaleUpdate"))
{
UpdateUIScale(root);
}
root.UIScaleUpdateNeeded = false;
}
}

View File

@@ -216,8 +216,6 @@ namespace Robust.Client.UserInterface
{
foreach (var root in _roots)
{
CheckRootUIScaleUpdate(root);
using (_prof.Group("Root"))
{
var totalUpdated = root.DoFrameUpdateRecursive(args);

View File

@@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Robust.Shared.Collections;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -117,16 +115,6 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
/// </summary>
public GameState? State;
/// <summary>
/// The serialized <see cref="State"/> object.
/// </summary>
public MemoryStream? StateStream;
/// <summary>
/// Whether we should force reliable sending of the <see cref="MsgState"/>.
/// </summary>
public bool ForceSendReliably { get; set; }
/// <summary>
/// Clears all stored game state data. This should only be used after the game state has been serialized.
/// </summary>

View File

@@ -75,15 +75,15 @@ namespace Robust.Server.GameStates
return true;
}
private void CleanupDirty()
private void CleanupDirty(ICommonSession[] sessions)
{
using var _ = Histogram.WithLabels("Clean Dirty").NewTimer();
if (!CullingEnabled)
{
_seenAllEnts.Clear();
foreach (var player in _sessions)
foreach (var player in sessions)
{
_seenAllEnts.Add(player.Session);
_seenAllEnts.Add(player);
}
}

View File

@@ -17,12 +17,13 @@ internal sealed partial class PvsSystem
{
private WaitHandle? _leaveTask;
private void ProcessLeavePvs()
private void ProcessLeavePvs(ICommonSession[] sessions)
{
if (!CullingEnabled || _sessions.Length == 0)
if (!CullingEnabled || sessions.Length == 0)
return;
DebugTools.AssertNull(_leaveTask);
_leaveJob.Setup(sessions);
if (_async)
{
@@ -75,19 +76,29 @@ internal sealed partial class PvsSystem
{
public int BatchSize => 2;
private PvsSystem _pvs = _pvs;
public int Count => _pvs._sessions.Length;
public int Count => _sessions.Length;
private PvsSession[] _sessions;
public void Execute(int index)
{
try
{
_pvs.ProcessLeavePvs(_pvs._sessions[index]);
_pvs.ProcessLeavePvs(_sessions[index]);
}
catch (Exception e)
{
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
}
}
public void Setup(ICommonSession[] sessions)
{
// Copy references to PvsSession, in case players disconnect while the job is running.
Array.Resize(ref _sessions, sessions.Length);
for (var i = 0; i < sessions.Length; i++)
{
_sessions[i] = _pvs.PlayerData[sessions[i]];
}
}
}
}

View File

@@ -1,85 +0,0 @@
using System;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.Log;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PvsSystem
{
/// <summary>
/// Compress and send game states to connected clients.
/// </summary>
private void SendStates()
{
// TODO PVS make this async
// AFAICT ForEachAsync doesn't support using a threadlocal PvsThreadResources.
// Though if it is getting pooled, does it really matter?
// If this does get run async, then ProcessDisconnections() has to ensure that the job has finished before modifying
// the sessions array
using var _ = Histogram.WithLabels("Send States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
Parallel.ForEach(_sessions, opts, _threadResourcesPool.Get, SendSessionState, _threadResourcesPool.Return);
}
private PvsThreadResources SendSessionState(PvsSession data, ParallelLoopState state, PvsThreadResources resource)
{
try
{
SendSessionState(data, resource.CompressionContext);
}
catch (Exception e)
{
Log.Log(LogLevel.Error, e, $"Caught exception while sending mail for {data.Session}.");
}
return resource;
}
private void SendSessionState(PvsSession data, ZStdCompressionContext ctx)
{
DebugTools.AssertEqual(data.State, null);
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data.Session.Channel is not DummyChannel)
{
DebugTools.AssertNotEqual(data.StateStream, null);
var msg = new MsgState
{
StateStream = data.StateStream,
ForceSendReliably = data.ForceSendReliably,
CompressionContext = ctx
};
_netMan.ServerSendMessage(msg, data.Session.Channel);
if (msg.ShouldSendReliably())
{
data.RequestedFull = false;
data.LastReceivedAck = _gameTiming.CurTick;
lock (PendingAcks)
{
PendingAcks.Add(data.Session);
}
}
}
else
{
// Always "ack" dummy sessions.
data.LastReceivedAck = _gameTiming.CurTick;
data.RequestedFull = false;
lock (PendingAcks)
{
PendingAcks.Add(data.Session);
}
}
data.StateStream?.Dispose();
data.StateStream = null;
}
}

View File

@@ -1,73 +0,0 @@
using System;
using System.Threading.Tasks;
using Prometheus;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Server.GameStates;
internal sealed partial class PvsSystem
{
[Dependency] private readonly IRobustSerializer _serializer = default!;
/// <summary>
/// Get and serialize <see cref="GameState"/> objects for each player. Compressing & sending the states is done later.
/// </summary>
private void SerializeStates()
{
using var _ = Histogram.WithLabels("Serialize States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
_oldestAck = GameTick.MaxValue.Value;
Parallel.For(-1, _sessions.Length, opts, SerializeState);
}
/// <summary>
/// Get and serialize a <see cref="GameState"/> for a single session (or the current replay).
/// </summary>
private void SerializeState(int i)
{
try
{
var guid = i >= 0 ? _sessions[i].Session.UserId.UserId : default;
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
if (i >= 0)
SerializeSessionState(_sessions[i]);
else
_replay.Update();
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
}
catch (Exception e) // Catch EVERY exception
{
var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
}
}
/// <summary>
/// Get and serialize a <see cref="GameState"/> for a single session.
/// </summary>
private void SerializeSessionState(PvsSession data)
{
ComputeSessionState(data);
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
DebugTools.AssertEqual(data.StateStream, null);
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (data.Session.Channel is not DummyChannel)
{
data.StateStream = RobustMemoryManager.GetMemoryStream();
_serializer.SerializeDirect(data.StateStream, data.State);
}
data.ClearState();
}
}

View File

@@ -7,6 +7,8 @@ using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -25,6 +27,49 @@ internal sealed partial class PvsSystem
private List<ICommonSession> _disconnected = new();
private void SendStateUpdate(ICommonSession session, PvsThreadResources resources)
{
var data = GetOrNewPvsSession(session);
ComputeSessionState(data);
InterlockedHelper.Min(ref _oldestAck, data.FromTick.Value);
// actually send the state
var msg = new MsgState
{
State = data.State,
CompressionContext = resources.CompressionContext
};
// PVS benchmarks use dummy sessions.
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (session.Channel is not DummyChannel)
{
_netMan.ServerSendMessage(msg, session.Channel);
if (msg.ShouldSendReliably())
{
data.RequestedFull = false;
data.LastReceivedAck = _gameTiming.CurTick;
lock (PendingAcks)
{
PendingAcks.Add(session);
}
}
}
else
{
// Always "ack" dummy sessions.
data.LastReceivedAck = _gameTiming.CurTick;
data.RequestedFull = false;
lock (PendingAcks)
{
PendingAcks.Add(session);
}
}
data.ClearState();
}
private PvsSession GetOrNewPvsSession(ICommonSession session)
{
if (!PlayerData.TryGetValue(session, out var pvsSession))
@@ -59,7 +104,7 @@ internal sealed partial class PvsSystem
session.PlayerStates,
_deletedEntities);
session.ForceSendReliably = session.RequestedFull
session.State.ForceSendReliably = session.RequestedFull
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
}

View File

@@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Prometheus;
using Robust.Server.Configuration;
@@ -14,6 +16,9 @@ using Robust.Server.Replays;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -94,10 +99,6 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
private readonly List<GameTick> _deletedTick = new();
/// <summary>
/// The sessions that are currently being processed. Note that this is in general used by parallel & async tasks.
/// Hence player disconnection processing is deferred and only run via <see cref="ProcessDisconnections"/>.
/// </summary>
private PvsSession[] _sessions = default!;
private bool _async;
@@ -182,25 +183,52 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
internal void SendGameStates(ICommonSession[] players)
{
// Wait for pending jobs and process disconnected players
ProcessDisconnections();
// Ensure each session has a PvsSession entry before starting any parallel jobs.
CacheSessionData(players);
// Get visible chunks, and update any dirty chunks.
BeforeSerializeStates();
BeforeSendState();
// Construct & serialize the game state for each player (and for the replay).
SerializeStates();
// Compress & send the states.
SendStates();
// Construct & send the game state to each player.
SendStates(players);
// Cull deletion history
AfterSerializeStates();
AfterSendState(players);
ProcessLeavePvs();
ProcessLeavePvs(players);
}
private void SendStates(ICommonSession[] players)
{
using var _ = Histogram.WithLabels("Send States").NewTimer();
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount};
_oldestAck = GameTick.MaxValue.Value;
// Replays process game states in parallel with players
Parallel.For(-1, players.Length, opts, _threadResourcesPool.Get, SendPlayer, _threadResourcesPool.Return);
PvsThreadResources SendPlayer(int i, ParallelLoopState state, PvsThreadResources resource)
{
try
{
var guid = i >= 0 ? players[i].UserId.UserId : default;
ServerGameStateManager.PvsEventSource.Log.WorkStart(_gameTiming.CurTick.Value, i, guid);
if (i >= 0)
SendStateUpdate(players[i], resource);
else
_replay.Update();
ServerGameStateManager.PvsEventSource.Log.WorkStop(_gameTiming.CurTick.Value, i, guid);
}
catch (Exception e) // Catch EVERY exception
{
var source = i >= 0 ? players[i].ToString() : "replays";
Log.Log(LogLevel.Error, e, $"Caught exception while generating mail for {source}.");
}
return resource;
}
}
private void ResetParallelism(int _) => ResetParallelism();
@@ -386,11 +414,23 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void BeforeSerializeStates()
private void BeforeSendState()
{
DebugTools.Assert(_chunks.Values.All(x => Exists(x.Map) && Exists(x.Root)));
DebugTools.Assert(_chunkSets.Keys.All(Exists));
_leaveTask?.WaitOne();
_leaveTask = null;
foreach (var session in _disconnected)
{
if (PlayerData.Remove(session, out var pvsSession))
{
ClearSendHistory(pvsSession);
FreeSessionDataMemory(pvsSession);
}
}
var ackJob = ProcessQueuedAcks();
// Figure out what chunks players can see and cache some chunk data.
@@ -403,21 +443,6 @@ internal sealed partial class PvsSystem : EntitySystem
ackJob?.WaitOne();
}
internal void ProcessDisconnections()
{
_leaveTask?.WaitOne();
_leaveTask = null;
foreach (var session in _disconnected)
{
if (PlayerData.Remove(session, out var pvsSession))
{
ClearSendHistory(pvsSession);
FreeSessionDataMemory(pvsSession);
}
}
}
internal void CacheSessionData(ICommonSession[] players)
{
Array.Resize(ref _sessions, players.Length);
@@ -427,9 +452,9 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void AfterSerializeStates()
private void AfterSendState(ICommonSession[] players)
{
CleanupDirty();
CleanupDirty(players);
if (_oldestAck == GameTick.MaxValue.Value)
{

View File

@@ -143,6 +143,7 @@ namespace Robust.Server.Player
list.Add(info);
}
netMsg.Plyrs = list;
netMsg.PlyCount = (byte)list.Count;
channel.SendMessage(netMsg);
}

View File

@@ -39,6 +39,8 @@ namespace Robust.Server
return;
}
ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount);
ParsedMain(parsed, contentStart, options);
}

View File

@@ -116,7 +116,7 @@ internal sealed class HubManager
if (!response.IsSuccessStatusCode)
{
var errorText = await response.Content.ReadAsStringAsync();
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, from {HubUrl}",
_sawmill.Error("Error status while advertising server: [{StatusCode}] {ErrorText}, to {HubUrl}",
response.StatusCode,
errorText,
hubUrl);

View File

@@ -70,7 +70,7 @@ namespace Robust.Shared
/// <seealso cref="NetMtuExpand"/>
/// <seealso cref="NetMtuIpv6"/>
public static readonly CVarDef<int> NetMtu =
CVarDef.Create("net.mtu", 700, CVar.ARCHIVE);
CVarDef.Create("net.mtu", 900, CVar.ARCHIVE);
/// <summary>
/// Maximum UDP payload size to send by default, for IPv6.
@@ -1374,7 +1374,7 @@ namespace Robust.Shared
/// the purpose of using an atlas if it gets too small.
/// </summary>
public static readonly CVarDef<int> ResRSIAtlasSize =
CVarDef.Create("res.rsi_atlas_size", 12288, CVar.CLIENTONLY);
CVarDef.Create("res.rsi_atlas_size", 8192, CVar.CLIENTONLY);
// TODO: Currently unimplemented.
/// <summary>
@@ -1560,12 +1560,6 @@ 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
*/

View File

@@ -1,304 +0,0 @@
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()
{
}
}
}

View File

@@ -40,8 +40,8 @@ public abstract partial class SharedContainerSystem
DebugTools.AssertOwner(container.Owner, containerXform);
DebugTools.AssertOwner(toInsert, physics);
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)), "entity is expected");
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID), "manager does not own the container");
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)));
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID));
// If someone is attempting to insert an entity into a container that is getting deleted, then we will
// automatically delete that entity. I.e., the insertion automatically "succeeds" and both entities get deleted.
@@ -82,14 +82,14 @@ public abstract partial class SharedContainerSystem
}
// Update metadata first, so that parent change events can check IsInContainer.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0, "invalid metadata flags before insertion");
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0);
meta.Flags |= MetaDataFlags.InContainer;
// Remove the entity and any children from broadphases.
// This is done before changing can collide to avoid unecceary updates.
// TODO maybe combine with RecursivelyUpdatePhysics to avoid fetching components and iterating parents twice?
_lookup.RemoveFromEntityTree(toInsert, transform);
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid(), "invalid broadphase");
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid());
// Avoid unnecessary broadphase updates while unanchoring, changing physics collision, and re-parenting.
var old = transform.Broadphase;
@@ -111,7 +111,7 @@ public abstract partial class SharedContainerSystem
transform.Broadphase = old;
// the transform.AttachParent() could previously result in the flag being unset, so check that this hasn't happened.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after insertion");
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
// Implementation specific insert logic
container.InternalInsert(toInsert, EntityManager);
@@ -125,11 +125,11 @@ public abstract partial class SharedContainerSystem
RaiseLocalEvent(toInsert, new EntGotInsertedIntoContainerMessage(toInsert, container), true);
// The sheer number of asserts tells you about how little I trust container and parenting code.
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after events");
DebugTools.Assert(!transform.Anchored, "entity is anchored");
DebugTools.AssertEqual(transform.LocalPosition, Vector2.Zero);
DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero), "Angle is not zero");
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide), "Invalid physics");
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0);
DebugTools.Assert(!transform.Anchored);
DebugTools.Assert(transform.LocalPosition == Vector2.Zero);
DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero));
DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide));
Dirty(container.Owner, container.Manager);
return true;

View File

@@ -41,7 +41,7 @@ public abstract partial class SharedContainerSystem
return false;
DebugTools.AssertNotNull(container.Manager);
DebugTools.Assert(Exists(toRemove), "toRemove does not exist");
DebugTools.Assert(Exists(toRemove));
if (!force && !CanRemove(toRemove, container))
return false;
@@ -60,11 +60,11 @@ public abstract partial class SharedContainerSystem
return false;
}
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent), "Entity is terminating");
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid(), "broadphase is invalid");
DebugTools.Assert(!xform.Anchored || _timing.ApplyingState, "anchor is invalid");
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0, "metadata is invalid");
DebugTools.Assert(!TryComp(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide), "physics is invalid");
DebugTools.Assert(meta.EntityLifeStage < EntityLifeStage.Terminating || (force && !reparent));
DebugTools.Assert(xform.Broadphase == null || !xform.Broadphase.Value.IsValid());
DebugTools.Assert(!xform.Anchored);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0x0);
DebugTools.Assert(!TryComp(toRemove, out PhysicsComponent? phys) || (!phys.Awake && !phys.CanCollide));
// Unset flag (before parent change events are raised).
meta.Flags &= ~MetaDataFlags.InContainer;
@@ -104,7 +104,7 @@ public abstract partial class SharedContainerSystem
RaiseLocalEvent(container.Owner, new EntRemovedFromContainerMessage(toRemove, container), true);
RaiseLocalEvent(toRemove, new EntGotRemovedFromContainerMessage(toRemove, container), false);
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), "failed to set destination");
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value));
Dirty(container.Owner, container.Manager);
return true;

View File

@@ -1,442 +0,0 @@
#if TOOLS
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Runtime.CompilerServices;
namespace Robust.Shared.ContentPack;
internal sealed partial class AssemblyTypeChecker
{
// This part of the code tries to find the originator of bad sandbox references.
private void ReportBadReferences(PEReader peReader, MetadataReader reader, IEnumerable<EntityHandle> reference)
{
_sawmill.Info("Started search for originator of bad references...");
var refs = reference.ToHashSet();
ExpandReferences(reader, refs);
foreach (var methodDefHandle in reader.MethodDefinitions)
{
var methodDef = reader.GetMethodDefinition(methodDefHandle);
if (methodDef.RelativeVirtualAddress == 0)
continue;
var methodName = reader.GetString(methodDef.Name);
var body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress);
var bytes = body.GetILBytes()!;
var ilReader = new ILReader(bytes);
var prefPosition = 0;
while (ilReader.MoveNext(out var instruction))
{
if (instruction.TryGetEntityHandle(out var handle))
{
if (refs.Contains(handle))
{
var type = GetTypeFromDefinition(reader, methodDef.GetDeclaringType());
_sawmill.Error(
$"Found reference to {DisplayHandle(reader, handle)} in method {type}.{methodName} at IL 0x{prefPosition:X4}");
}
}
prefPosition = ilReader.Position;
}
}
}
private static string DisplayHandle(MetadataReader reader, EntityHandle handle)
{
switch (handle.Kind)
{
case HandleKind.MemberReference:
var memberRef = reader.GetMemberReference((MemberReferenceHandle)handle);
var name = reader.GetString(memberRef.Name);
var parent = DisplayHandle(reader, memberRef.Parent);
return $"{parent}.{name}";
case HandleKind.TypeReference:
return $"{ParseTypeReference(reader, (TypeReferenceHandle)handle)}";
case HandleKind.TypeSpecification:
var typeSpec = reader.GetTypeSpecification((TypeSpecificationHandle)handle);
var provider = new TypeProvider();
var type = typeSpec.DecodeSignature(provider, 0);
return $"{type}";
default:
return $"({handle.Kind} handle)";
}
}
private static void ExpandReferences(MetadataReader reader, HashSet<EntityHandle> handles)
{
var toAdd = new List<EntityHandle>();
foreach (var memberRefHandle in reader.MemberReferences)
{
var memberRef = reader.GetMemberReference(memberRefHandle);
if (handles.Contains(memberRef.Parent))
{
toAdd.Add(memberRefHandle);
}
}
handles.UnionWith(toAdd);
}
private readonly struct ILInstruction
{
public readonly ILOpCode OpCode;
public readonly long Argument;
public readonly int[]? SwitchTargets;
public ILInstruction(ILOpCode opCode)
{
OpCode = opCode;
}
public ILInstruction(ILOpCode opCode, long argument)
{
OpCode = opCode;
Argument = argument;
}
public ILInstruction(ILOpCode opCode, long argument, int[] switchTargets)
{
OpCode = opCode;
Argument = argument;
SwitchTargets = switchTargets;
}
public bool TryGetEntityHandle(out EntityHandle handle)
{
switch (OpCode)
{
case ILOpCode.Call:
case ILOpCode.Callvirt:
case ILOpCode.Newobj:
case ILOpCode.Jmp:
case ILOpCode.Box:
case ILOpCode.Castclass:
case ILOpCode.Cpobj:
case ILOpCode.Initobj:
case ILOpCode.Isinst:
case ILOpCode.Ldelem:
case ILOpCode.Ldelema:
case ILOpCode.Ldfld:
case ILOpCode.Ldflda:
case ILOpCode.Ldobj:
case ILOpCode.Ldstr:
case ILOpCode.Ldtoken:
case ILOpCode.Ldvirtftn:
case ILOpCode.Mkrefany:
case ILOpCode.Newarr:
case ILOpCode.Refanyval:
case ILOpCode.Sizeof:
case ILOpCode.Stelem:
case ILOpCode.Stfld:
case ILOpCode.Stobj:
case ILOpCode.Stsfld:
case ILOpCode.Throw:
case ILOpCode.Unbox_any:
handle = Unsafe.BitCast<int, EntityHandle>((int)Argument);
return true;
default:
handle = default;
return false;
}
}
}
private sealed class ILReader(byte[] body)
{
public int Position;
public bool MoveNext(out ILInstruction instruction)
{
if (Position >= body.Length)
{
instruction = default;
return false;
}
var firstByte = body[Position++];
var opCode = (ILOpCode)firstByte;
if (firstByte == 0xFE)
opCode = 0xFE00 + (ILOpCode)body[Position++];
switch (opCode)
{
// no args.
case ILOpCode.Readonly:
case ILOpCode.Tail:
case ILOpCode.Volatile:
case ILOpCode.Add:
case ILOpCode.Add_ovf:
case ILOpCode.Add_ovf_un:
case ILOpCode.And:
case ILOpCode.Arglist:
case ILOpCode.Break:
case ILOpCode.Ceq:
case ILOpCode.Cgt:
case ILOpCode.Cgt_un:
case ILOpCode.Ckfinite:
case ILOpCode.Clt:
case ILOpCode.Clt_un:
case ILOpCode.Conv_i1:
case ILOpCode.Conv_i2:
case ILOpCode.Conv_i4:
case ILOpCode.Conv_i8:
case ILOpCode.Conv_r4:
case ILOpCode.Conv_r8:
case ILOpCode.Conv_u1:
case ILOpCode.Conv_u2:
case ILOpCode.Conv_u4:
case ILOpCode.Conv_u8:
case ILOpCode.Conv_i:
case ILOpCode.Conv_u:
case ILOpCode.Conv_r_un:
case ILOpCode.Conv_ovf_i1:
case ILOpCode.Conv_ovf_i2:
case ILOpCode.Conv_ovf_i4:
case ILOpCode.Conv_ovf_i8:
case ILOpCode.Conv_ovf_u4:
case ILOpCode.Conv_ovf_u8:
case ILOpCode.Conv_ovf_i:
case ILOpCode.Conv_ovf_u:
case ILOpCode.Conv_ovf_i1_un:
case ILOpCode.Conv_ovf_i2_un:
case ILOpCode.Conv_ovf_i4_un:
case ILOpCode.Conv_ovf_i8_un:
case ILOpCode.Conv_ovf_u4_un:
case ILOpCode.Conv_ovf_u8_un:
case ILOpCode.Conv_ovf_i_un:
case ILOpCode.Conv_ovf_u_un:
case ILOpCode.Cpblk:
case ILOpCode.Div:
case ILOpCode.Div_un:
case ILOpCode.Dup:
case ILOpCode.Endfilter:
case ILOpCode.Endfinally:
case ILOpCode.Initblk:
case ILOpCode.Ldarg_0:
case ILOpCode.Ldarg_1:
case ILOpCode.Ldarg_2:
case ILOpCode.Ldarg_3:
case ILOpCode.Ldc_i4_0:
case ILOpCode.Ldc_i4_1:
case ILOpCode.Ldc_i4_2:
case ILOpCode.Ldc_i4_3:
case ILOpCode.Ldc_i4_4:
case ILOpCode.Ldc_i4_5:
case ILOpCode.Ldc_i4_6:
case ILOpCode.Ldc_i4_7:
case ILOpCode.Ldc_i4_8:
case ILOpCode.Ldc_i4_m1:
case ILOpCode.Ldind_i1:
case ILOpCode.Ldind_u1:
case ILOpCode.Ldind_i2:
case ILOpCode.Ldind_u2:
case ILOpCode.Ldind_i4:
case ILOpCode.Ldind_u4:
case ILOpCode.Ldind_i8:
case ILOpCode.Ldind_i:
case ILOpCode.Ldind_r4:
case ILOpCode.Ldind_r8:
case ILOpCode.Ldind_ref:
case ILOpCode.Ldloc_0:
case ILOpCode.Ldloc_1:
case ILOpCode.Ldloc_2:
case ILOpCode.Ldloc_3:
case ILOpCode.Ldnull:
case ILOpCode.Localloc:
case ILOpCode.Mul:
case ILOpCode.Mul_ovf:
case ILOpCode.Mul_ovf_un:
case ILOpCode.Neg:
case ILOpCode.Nop:
case ILOpCode.Not:
case ILOpCode.Or:
case ILOpCode.Pop:
case ILOpCode.Rem:
case ILOpCode.Rem_un:
case ILOpCode.Ret:
case ILOpCode.Shl:
case ILOpCode.Shr:
case ILOpCode.Shr_un:
case ILOpCode.Stind_i1:
case ILOpCode.Stind_i2:
case ILOpCode.Stind_i4:
case ILOpCode.Stind_i8:
case ILOpCode.Stind_r4:
case ILOpCode.Stind_r8:
case ILOpCode.Stind_i:
case ILOpCode.Stind_ref:
case ILOpCode.Stloc_0:
case ILOpCode.Stloc_1:
case ILOpCode.Stloc_2:
case ILOpCode.Stloc_3:
case ILOpCode.Sub:
case ILOpCode.Sub_ovf:
case ILOpCode.Sub_ovf_un:
case ILOpCode.Xor:
case ILOpCode.Ldelem_i1:
case ILOpCode.Ldelem_u1:
case ILOpCode.Ldelem_i2:
case ILOpCode.Ldelem_u2:
case ILOpCode.Ldelem_i4:
case ILOpCode.Ldelem_u4:
case ILOpCode.Ldelem_i8:
case ILOpCode.Ldelem_i:
case ILOpCode.Ldelem_r4:
case ILOpCode.Ldelem_r8:
case ILOpCode.Ldelem_ref:
case ILOpCode.Ldlen:
case ILOpCode.Refanytype:
case ILOpCode.Rethrow:
case ILOpCode.Stelem_i1:
case ILOpCode.Stelem_i2:
case ILOpCode.Stelem_i4:
case ILOpCode.Stelem_i8:
case ILOpCode.Stelem_i:
case ILOpCode.Stelem_r4:
case ILOpCode.Stelem_r8:
case ILOpCode.Stelem_ref:
case ILOpCode.Throw:
instruction = new ILInstruction(opCode);
break;
// 1-byte arg.
case ILOpCode.Unaligned:
case ILOpCode.Beq_s:
case ILOpCode.Bge_s:
case ILOpCode.Bge_un_s:
case ILOpCode.Bgt_s:
case ILOpCode.Bgt_un_s:
case ILOpCode.Ble_s:
case ILOpCode.Ble_un_s:
case ILOpCode.Blt_s:
case ILOpCode.Blt_un_s:
case ILOpCode.Bne_un_s:
case ILOpCode.Br_s:
case ILOpCode.Brfalse_s:
case ILOpCode.Brtrue_s:
case ILOpCode.Ldarg_s:
case ILOpCode.Ldarga_s:
case ILOpCode.Ldc_i4_s:
case ILOpCode.Ldloc_s:
case ILOpCode.Ldloca_s:
case ILOpCode.Leave_s:
case ILOpCode.Starg_s:
case ILOpCode.Stloc_s:
instruction = new ILInstruction(opCode, body[Position]);
Position += 1;
break;
// 2-byte value
case ILOpCode.Ldarg:
case ILOpCode.Ldarga:
case ILOpCode.Ldloc:
case ILOpCode.Ldloca:
case ILOpCode.Starg:
case ILOpCode.Stloc:
var shortValue = BinaryPrimitives.ReadInt16LittleEndian(body.AsSpan(Position, 2));
Position += 2;
instruction = new ILInstruction(opCode, shortValue);
break;
// 4-byte value
case ILOpCode.Constrained:
case ILOpCode.Beq:
case ILOpCode.Bge:
case ILOpCode.Bge_un:
case ILOpCode.Bgt:
case ILOpCode.Bgt_un:
case ILOpCode.Ble:
case ILOpCode.Ble_un:
case ILOpCode.Blt:
case ILOpCode.Blt_un:
case ILOpCode.Bne_un:
case ILOpCode.Br:
case ILOpCode.Brfalse:
case ILOpCode.Brtrue:
case ILOpCode.Call:
case ILOpCode.Calli:
case ILOpCode.Jmp:
case ILOpCode.Ldc_i4:
case ILOpCode.Ldc_r4:
case ILOpCode.Ldftn:
case ILOpCode.Leave:
case ILOpCode.Box:
case ILOpCode.Callvirt:
case ILOpCode.Castclass:
case ILOpCode.Cpobj:
case ILOpCode.Initobj:
case ILOpCode.Isinst:
case ILOpCode.Ldelem:
case ILOpCode.Ldelema:
case ILOpCode.Ldfld:
case ILOpCode.Ldflda:
case ILOpCode.Ldobj:
case ILOpCode.Ldsfld:
case ILOpCode.Ldsflda:
case ILOpCode.Ldstr:
case ILOpCode.Ldtoken:
case ILOpCode.Ldvirtftn:
case ILOpCode.Mkrefany:
case ILOpCode.Newarr:
case ILOpCode.Newobj:
case ILOpCode.Refanyval:
case ILOpCode.Sizeof:
case ILOpCode.Stelem:
case ILOpCode.Stfld:
case ILOpCode.Stobj:
case ILOpCode.Stsfld:
case ILOpCode.Unbox:
case ILOpCode.Unbox_any:
var intValue = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
instruction = new ILInstruction(opCode, intValue);
break;
// 8-byte value
case ILOpCode.Ldc_i8:
case ILOpCode.Ldc_r8:
var longValue = BinaryPrimitives.ReadInt64LittleEndian(body.AsSpan(Position, 8));
Position += 8;
instruction = new ILInstruction(opCode, longValue);
break;
// Switch
case ILOpCode.Switch:
var switchLength = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
var switchArgs = new int[switchLength];
for (var i = 0; i < switchLength; i++)
{
switchArgs[i] = BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(Position, 4));
Position += 4;
}
instruction = new ILInstruction(opCode, switchLength, switchArgs);
break;
default:
throw new InvalidDataException($"Unknown opcode: {opCode}");
}
return true;
}
}
}
#endif

View File

@@ -148,7 +148,7 @@ namespace Robust.Shared.ContentPack
if ((Dump & DumpFlags.Types) != 0)
{
foreach (var (_, mType) in types)
foreach (var mType in types)
{
_sawmill.Debug($"RefType: {mType}");
}
@@ -156,7 +156,7 @@ namespace Robust.Shared.ContentPack
if ((Dump & DumpFlags.Members) != 0)
{
foreach (var (_, memberRef) in members)
foreach (var memberRef in members)
{
_sawmill.Debug($"RefMember: {memberRef}");
}
@@ -183,17 +183,14 @@ namespace Robust.Shared.ContentPack
var loadedConfig = _config.Result;
#pragma warning restore RA0004
var badRefs = new ConcurrentBag<EntityHandle>();
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.
// This is so that we can simplify handling of generic type specifications during member checking:
// we won't have to check that any types in their type arguments are whitelisted.
foreach (var (handle, type) in types)
foreach (var type in types)
{
if (!IsTypeAccessAllowed(loadedConfig, type, out _))
{
errors.Add(new SandboxError($"Access to type not allowed: {type}"));
badRefs.Add(handle);
}
}
@@ -211,20 +208,13 @@ namespace Robust.Shared.ContentPack
_sawmill.Debug($"Type abuse... {fullStopwatch.ElapsedMilliseconds}ms");
CheckMemberReferences(loadedConfig, members, errors, badRefs);
CheckMemberReferences(loadedConfig, members, errors);
foreach (var error in errors)
{
_sawmill.Error($"Sandbox violation: {error.Message}");
}
#if TOOLS
if (!badRefs.IsEmpty)
{
ReportBadReferences(peReader, reader, badRefs);
}
#endif
_sawmill.Debug($"Checked assembly in {fullStopwatch.ElapsedMilliseconds}ms");
return errors.IsEmpty;
@@ -361,13 +351,11 @@ namespace Robust.Shared.ContentPack
private void CheckMemberReferences(
SandboxConfig sandboxConfig,
List<(MemberReferenceHandle handle, MMemberRef parsed)> members,
ConcurrentBag<SandboxError> errors,
ConcurrentBag<EntityHandle> badReferences)
List<MMemberRef> members,
ConcurrentBag<SandboxError> errors)
{
Parallel.ForEach(members, entry =>
Parallel.ForEach(members, memberRef =>
{
var (handle, memberRef) = entry;
MType baseType = memberRef.ParentType;
while (!(baseType is MTypeReferenced))
{
@@ -428,7 +416,6 @@ namespace Robust.Shared.ContentPack
}
errors.Add(new SandboxError($"Access to field not allowed: {mMemberRefField}"));
badReferences.Add(handle);
break;
}
case MMemberRefMethod mMemberRefMethod:
@@ -457,7 +444,6 @@ namespace Robust.Shared.ContentPack
}
errors.Add(new SandboxError($"Access to method not allowed: {mMemberRefMethod}"));
badReferences.Add(handle);
break;
default:
throw new ArgumentOutOfRangeException(nameof(memberRef));
@@ -472,18 +458,18 @@ namespace Robust.Shared.ContentPack
{
// This inheritance whitelisting primarily serves to avoid content doing funny stuff
// by e.g. inheriting Type.
foreach (var (type, baseType, interfaces) in inherited)
foreach (var (_, baseType, interfaces) in inherited)
{
if (!CanInherit(baseType))
{
errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType} (by {type})"));
errors.Add(new SandboxError($"Inheriting of type not allowed: {baseType}"));
}
foreach (var @interface in interfaces)
{
if (!CanInherit(@interface))
{
errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface} (by {type})"));
errors.Add(new SandboxError($"Implementing of interface not allowed: {@interface}"));
}
}
@@ -561,25 +547,25 @@ namespace Robust.Shared.ContentPack
return nsDict.TryGetValue(type.Name, out cfg);
}
private List<(TypeReferenceHandle handle, MTypeReferenced parsed)> GetReferencedTypes(MetadataReader reader, ConcurrentBag<SandboxError> errors)
private List<MTypeReferenced> GetReferencedTypes(MetadataReader reader, ConcurrentBag<SandboxError> errors)
{
return reader.TypeReferences.Select(typeRefHandle =>
{
try
{
return (typeRefHandle, ParseTypeReference(reader, typeRefHandle));
return ParseTypeReference(reader, typeRefHandle);
}
catch (UnsupportedMetadataException e)
{
errors.Add(new SandboxError(e));
return default;
return null;
}
})
.Where(p => p.Item2 != null)
.Where(p => p != null)
.ToList()!;
}
private List<(MemberReferenceHandle handle, MMemberRef parsed)> GetReferencedMembers(MetadataReader reader, ConcurrentBag<SandboxError> errors)
private List<MMemberRef> GetReferencedMembers(MetadataReader reader, ConcurrentBag<SandboxError> errors)
{
return reader.MemberReferences.AsParallel()
.Select(memRefHandle =>
@@ -600,7 +586,7 @@ namespace Robust.Shared.ContentPack
catch (UnsupportedMetadataException u)
{
errors.Add(new SandboxError(u));
return default;
return null;
}
break;
@@ -614,7 +600,7 @@ namespace Robust.Shared.ContentPack
catch (UnsupportedMetadataException u)
{
errors.Add(new SandboxError(u));
return default;
return null;
}
break;
@@ -630,7 +616,7 @@ namespace Robust.Shared.ContentPack
{
// Ensure this isn't a self-defined type.
// This can happen due to generics since MethodSpec needs to point to MemberRef.
return default;
return null;
}
break;
@@ -639,18 +625,18 @@ namespace Robust.Shared.ContentPack
{
errors.Add(new SandboxError(
$"Module global variables and methods are unsupported. Name: {memName}"));
return default;
return null;
}
case HandleKind.MethodDefinition:
{
errors.Add(new SandboxError($"Vararg calls are unsupported. Name: {memName}"));
return default;
return null;
}
default:
{
errors.Add(new SandboxError(
$"Unsupported member ref parent type: {memRef.Parent.Kind}. Name: {memName}"));
return default;
return null;
}
}
@@ -681,9 +667,9 @@ namespace Robust.Shared.ContentPack
throw new ArgumentOutOfRangeException();
}
return (memRefHandle, memberRef);
return memberRef;
})
.Where(p => p.memberRef != null)
.Where(p => p != null)
.ToList()!;
}
@@ -794,6 +780,7 @@ namespace Robust.Shared.ContentPack
}
}
/// <exception href="UnsupportedMetadataException">
/// Thrown if the metadata does something funny we don't "support" like type forwarding.
/// </exception>

View File

@@ -454,10 +454,9 @@ Types:
ConcurrentStack`1: { All: True }
System.Collections:
BitArray: { All: True }
ICollection: { All: True }
IEnumerable: { All: True }
IEnumerator: { All: True }
IList: { All: True }
IReadOnlyList`1: { All: True }
System.ComponentModel:
CancelEventArgs: { All: True }
PropertyDescriptor: { }

View File

@@ -739,47 +739,6 @@ public sealed partial class EntityLookupSystem
#endregion
#region Local
/// <summary>
/// Gets the entities intersecting the specified broadphase entity using a local AABB.
/// </summary>
public void GetLocalEntitiesIntersecting<T>(
EntityUid gridUid,
Vector2i localTile,
HashSet<Entity<T>> intersecting,
float enlargement = TileEnlargementRadius,
LookupFlags flags = DefaultFlags,
MapGridComponent? gridComp = null) where T : IComponent
{
ushort tileSize = 1;
if (_gridQuery.Resolve(gridUid, ref gridComp))
{
tileSize = gridComp.TileSize;
}
var localAABB = GetLocalBounds(localTile, tileSize);
localAABB = localAABB.Enlarged(TileEnlargementRadius);
GetLocalEntitiesIntersecting(gridUid, localAABB, intersecting, flags);
}
/// <summary>
/// Gets the entities intersecting the specified broadphase entity using a local AABB.
/// </summary>
public void GetLocalEntitiesIntersecting<T>(
EntityUid gridUid,
Box2 localAABB,
HashSet<Entity<T>> intersecting,
LookupFlags flags = DefaultFlags) where T : IComponent
{
var query = GetEntityQuery<T>();
AddLocalEntitiesIntersecting(gridUid, intersecting, localAABB, flags, query);
AddContained(intersecting, flags, query);
}
#endregion
/// <summary>
/// Gets entities with the specified component with the specified parent.
/// </summary>

View File

@@ -12,13 +12,14 @@ namespace Robust.Shared.Network.Messages
{
public override MsgGroups MsgGroup => MsgGroups.Core;
public byte PlyCount { get; set; }
public List<SessionState> Plyrs { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
var playerCount = buffer.ReadInt32();
Plyrs = new List<SessionState>(playerCount);
for (var i = 0; i < playerCount; i++)
Plyrs = new List<SessionState>();
PlyCount = buffer.ReadByte();
for (var i = 0; i < PlyCount; i++)
{
var plyNfo = new SessionState
{
@@ -33,7 +34,7 @@ namespace Robust.Shared.Network.Messages
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Plyrs.Count);
buffer.Write(PlyCount);
foreach (var ply in Plyrs)
{

View File

@@ -17,21 +17,15 @@ namespace Robust.Shared.Network.Messages
// Ideally we would peg this to the actual configured MTU instead of the default constant, but oh well...
public const int ReliableThreshold = NetPeerConfiguration.kDefaultMTU - 20;
// If a state is larger than this, we will compress it
// TODO PVS make this a cvar
// TODO PVS figure out optimal value
// If a state is larger than this, compress it with deflate.
public const int CompressionThreshold = 256;
public override MsgGroups MsgGroup => MsgGroups.Entity;
public GameState State;
public MemoryStream StateStream;
public ZStdCompressionContext CompressionContext;
internal bool HasWritten;
internal bool ForceSendReliably;
internal bool _hasWritten;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
@@ -66,19 +60,26 @@ namespace Robust.Shared.Network.Messages
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.WriteVariableInt32((int)StateStream.Length);
using var stateStream = RobustMemoryManager.GetMemoryStream();
serializer.SerializeDirect(stateStream, State);
buffer.WriteVariableInt32((int)stateStream.Length);
// We compress the state.
if (StateStream.Length > CompressionThreshold)
if (stateStream.Length > CompressionThreshold)
{
// var sw = Stopwatch.StartNew();
StateStream.Position = 0;
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)StateStream.Length));
var length = CompressionContext.Compress2(buf, StateStream.AsSpan());
stateStream.Position = 0;
var buf = ArrayPool<byte>.Shared.Rent(ZStd.CompressBound((int)stateStream.Length));
var length = CompressionContext.Compress2(buf, stateStream.AsSpan());
buffer.WriteVariableInt32(length);
buffer.Write(buf.AsSpan(0, length));
// var elapsed = sw.Elapsed;
// System.Console.WriteLine(
// $"From: {State.FromSequence} To: {State.ToSequence} Size: {length} B Before: {stateStream.Length} B time: {elapsed}");
ArrayPool<byte>.Shared.Return(buf);
}
// The state is sent as is.
@@ -86,10 +87,10 @@ namespace Robust.Shared.Network.Messages
{
// 0 means that the state isn't compressed.
buffer.WriteVariableInt32(0);
buffer.Write(StateStream.AsSpan());
buffer.Write(stateStream.AsSpan());
}
HasWritten = true;
_hasWritten = true;
MsgSize = buffer.LengthBytes;
}
@@ -100,12 +101,21 @@ namespace Robust.Shared.Network.Messages
/// <returns></returns>
public bool ShouldSendReliably()
{
DebugTools.Assert(HasWritten, "Attempted to determine sending method before determining packet size.");
return ForceSendReliably || MsgSize > ReliableThreshold;
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
return State.ForceSendReliably || MsgSize > ReliableThreshold;
}
public override NetDeliveryMethod DeliveryMethod => ShouldSendReliably()
? NetDeliveryMethod.ReliableUnordered
: base.DeliveryMethod;
public override NetDeliveryMethod DeliveryMethod
{
get
{
if (ShouldSendReliably())
{
return NetDeliveryMethod.ReliableUnordered;
}
return base.DeliveryMethod;
}
}
}
}

View File

@@ -31,14 +31,11 @@ namespace Robust.Shared.Physics.Systems
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<BroadphaseComponent> _broadphaseQuery;
private EntityQuery<FixturesComponent> _fixturesQuery;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<PhysicsMapComponent> _mapQuery;
private float _broadphaseExpand;
/*
* Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required.
* Our problem is that we have nested broadphases (rather than being on separate maps) which makes this
@@ -46,21 +43,23 @@ namespace Robust.Shared.Physics.Systems
* Hence we need to check which broadphases it does intersect and checkar for colliding bodies.
*/
private BroadphaseContactJob _contactJob;
/// <summary>
/// How much to expand bounds by to check cross-broadphase collisions.
/// Ideally you want to set this to your largest body size.
/// This only has a noticeable performance impact where multiple broadphases are in close proximity.
/// </summary>
private float _broadphaseExpand;
private const int PairBufferParallel = 8;
private ObjectPool<List<FixtureProxy>> _bufferPool =
new DefaultObjectPool<List<FixtureProxy>>(new ListPolicy<FixtureProxy>(), 2048);
public override void Initialize()
{
base.Initialize();
_contactJob = new()
{
_mapManager = _mapManager,
System = this,
BroadphaseExpand = _broadphaseExpand,
};
_broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
_fixturesQuery = GetEntityQuery<FixturesComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
@@ -72,11 +71,7 @@ namespace Robust.Shared.Physics.Systems
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
}
private void SetBroadphaseExpand(float value)
{
_contactJob.BroadphaseExpand = value;
_broadphaseExpand = value;
}
private void SetBroadphaseExpand(float value) => _broadphaseExpand = value;
#region Find Contacts
@@ -181,34 +176,65 @@ namespace Robust.Shared.Physics.Systems
if (moveBuffer.Count == 0)
return;
_contactJob.MapUid = _mapManager.GetMapEntityIdOrThrow(mapId);
_contactJob.MoveBuffer.Clear();
var count = moveBuffer.Count;
var contactBuffer = ArrayPool<List<FixtureProxy>>.Shared.Rent(count);
var pMoveBuffer = ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Rent(count);
var idx = 0;
foreach (var (proxy, aabb) in moveBuffer)
{
_contactJob.MoveBuffer.Add((proxy, aabb));
contactBuffer[idx] = _bufferPool.Get();
pMoveBuffer[idx++] = (proxy, aabb);
}
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
var options = new ParallelOptions
{
_contactJob.ContactBuffer.Add(new List<FixtureProxy>());
}
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
};
var count = moveBuffer.Count;
var batches = (int)MathF.Ceiling((float) count / PairBufferParallel);
_parallel.ProcessNow(_contactJob, count);
Parallel.For(0, batches, options, i =>
{
var start = i * PairBufferParallel;
var end = Math.Min(start + PairBufferParallel, count);
for (var j = start; j < end; j++)
{
var (proxy, worldAABB) = pMoveBuffer[j];
var buffer = contactBuffer[j];
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (this, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
return true;
}, approx: true, includeMap: false);
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), buffer);
}
});
for (var i = 0; i < count; i++)
{
var proxies = _contactJob.ContactBuffer[i];
if (proxies.Count == 0)
continue;
var proxyA = _contactJob.MoveBuffer[i].Proxy;
var proxyA = pMoveBuffer[i].Proxy;
var proxies = contactBuffer[i];
var proxyABody = proxyA.Body;
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
FixturesComponent? manager = null;
foreach (var other in proxies)
{
@@ -227,8 +253,13 @@ namespace Robust.Shared.Physics.Systems
_physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other);
}
_bufferPool.Return(contactBuffer[i]);
pMoveBuffer[i] = default;
}
ArrayPool<List<FixtureProxy>>.Shared.Return(contactBuffer);
ArrayPool<(FixtureProxy Proxy, Box2 AABB)>.Shared.Return(pMoveBuffer);
moveBuffer.Clear();
movedGrids.Clear();
}
@@ -485,51 +516,5 @@ namespace Robust.Shared.Physics.Systems
}
}
}
private record struct BroadphaseContactJob() : IParallelRobustJob
{
public SharedBroadphaseSystem System = default!;
public IMapManager _mapManager = default!;
public float BroadphaseExpand;
public EntityUid MapUid;
public List<List<FixtureProxy>> ContactBuffer = new();
public List<(FixtureProxy Proxy, Box2 WorldAABB)> MoveBuffer = new();
public int BatchSize => 8;
public void Execute(int index)
{
var (proxy, worldAABB) = MoveBuffer[index];
var buffer = ContactBuffer[index];
buffer.Clear();
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (System, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(MapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
return true;
},
approx: true,
includeMap: false);
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
System.FindPairs(proxy, worldAABB, MapUid, buffer);
}
}
}
}

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Robust.Shared.Collections;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -129,9 +128,6 @@ public static class RandomExtensions
return minAngle + (maxAngle - minAngle) * random.NextDouble();
}
public static Vector2 NextPolarVector2(this System.Random random, float minMagnitude, float maxMagnitude)
=> random.NextAngle().RotateVec(new Vector2(random.NextFloat(minMagnitude, maxMagnitude), 0));
public static float NextFloat(this IRobustRandom random)
{
// This is pretty much the CoreFX implementation.
@@ -145,9 +141,6 @@ public static class RandomExtensions
return random.Next() * 4.6566128752458E-10f;
}
public static float NextFloat(this System.Random random, float minValue, float maxValue)
=> random.NextFloat() * (maxValue - minValue) + minValue;
/// <summary>
/// Have a certain chance to return a boolean.
/// </summary>

View File

@@ -220,16 +220,14 @@ namespace Robust.Shared.Utility
/// <paramref name="arg" /> is <see langword="null" />.
/// </summary>
/// <param name="arg">Condition that must be true.</param>
/// <param name="message">Exception message.</param>
[Conditional("DEBUG")]
[AssertionMethod]
public static void AssertNotNull([AssertionCondition(AssertionConditionType.IS_NOT_NULL)]
object? arg,
string? message = null)
object? arg)
{
if (arg == null)
{
throw new DebugAssertException(message?? "value cannot be null");
throw new DebugAssertException();
}
}
@@ -238,16 +236,14 @@ namespace Robust.Shared.Utility
/// <paramref name="arg" /> is not <see langword="null" />.
/// </summary>
/// <param name="arg">Condition that must be true.</param>
/// <param name="message">Exception message.</param>
[Conditional("DEBUG")]
[AssertionMethod]
public static void AssertNull([AssertionCondition(AssertionConditionType.IS_NULL)]
object? arg,
string? message = null)
object? arg)
{
if (arg != null)
{
throw new DebugAssertException(message ?? "value should be null");
throw new DebugAssertException();
}
}
@@ -294,7 +290,7 @@ namespace Robust.Shared.Utility
{
}
public DebugAssertException(string? message) : base(message)
public DebugAssertException(string message) : base(message)
{
}
}

View File

@@ -16,30 +16,26 @@ namespace Robust.Shared.Utility;
/// </summary>
[PublicAPI]
[Serializable, NetSerializable]
public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
public sealed partial class FormattedMessage
{
public static FormattedMessage Empty => new();
/// <summary>
/// The list of nodes the formatted message is made out of
/// </summary>
public IReadOnlyList<MarkupNode> Nodes => _nodes;
public IReadOnlyList<MarkupNode> Nodes => _nodes.AsReadOnly();
/// <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 Stack<MarkupNode>? _openNodeStack;
private readonly Stack<MarkupNode> _openNodeStack = new();
public FormattedMessage()
{
@@ -203,7 +199,6 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
return;
}
_openNodeStack ??= new Stack<MarkupNode>();
_openNodeStack.Push(markupNode);
}
@@ -212,7 +207,7 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
/// </summary>
public void Pop()
{
if (_openNodeStack == null || !_openNodeStack.TryPop(out var node))
if (!_openNodeStack.TryPop(out var node))
return;
_nodes.Add(new MarkupNode(node.Name, null, null, true));
@@ -243,16 +238,6 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
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()
{
@@ -267,11 +252,6 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
return builder.ToString();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <returns>The string without filtering out markup tags.</returns>
public string ToMarkup()
{
@@ -281,13 +261,13 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
public struct FormattedMessageRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
{
private readonly FormattedMessage _msg;
private List<MarkupNode>.Enumerator _tagEnumerator;
private IEnumerator<MarkupNode> _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();
}
@@ -321,7 +301,7 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
public void Reset()
{
_tagEnumerator = _msg._nodes.GetEnumerator();
_tagEnumerator = _msg.Nodes.GetEnumerator();
_runeEnumerator = "".EnumerateRunes();
}
@@ -333,33 +313,4 @@ public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
{
}
}
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();
}
}
}

View File

@@ -11,7 +11,7 @@ public sealed class MarkupNode : IComparable<MarkupNode>
{
public readonly string? Name;
public readonly MarkupParameter Value;
public readonly Dictionary<string, MarkupParameter> Attributes;
public readonly Dictionary<string, MarkupParameter> Attributes = new();
public readonly bool Closing;
/// <summary>
@@ -20,7 +20,6 @@ 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);
}

View File

@@ -1,95 +0,0 @@
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}));
}
}