mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 20:20:46 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bb3d3c4bc | ||
|
|
a9f07dd63c | ||
|
|
2423c861c2 | ||
|
|
87ffd7b7af |
@@ -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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +216,6 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
CheckRootUIScaleUpdate(root);
|
||||
|
||||
using (_prof.Group("Root"))
|
||||
{
|
||||
var totalUpdated = root.DoFrameUpdateRecursive(args);
|
||||
|
||||
Submodule Robust.LoaderApi updated: 86a02eef16...99a2f4b880
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -143,6 +143,7 @@ namespace Robust.Server.Player
|
||||
list.Add(info);
|
||||
}
|
||||
netMsg.Plyrs = list;
|
||||
netMsg.PlyCount = (byte)list.Count;
|
||||
|
||||
channel.SendMessage(netMsg);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace Robust.Server
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadPool.SetMinThreads(Environment.ProcessorCount * 2, Environment.ProcessorCount);
|
||||
|
||||
ParsedMain(parsed, contentStart, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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: { }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user