Compare commits

...

21 Commits

Author SHA1 Message Date
Pieter-Jan Briers
b65e0c64ea Version: 226.2.2 2024-08-11 19:54:41 +02:00
Pieter-Jan Briers
4c388bc03d Use absolute path for explorer.exe
frick me

(cherry picked from commit 0284eb0430)
2024-08-11 19:54:41 +02:00
Pieter-Jan Briers
658dee1591 Version: 226.2.1 2024-08-11 17:54:19 +02:00
Pieter-Jan Briers
b6e5cca127 Security updates (#5353)
* Fix security bug in WritableDirProvider.OpenOsWindow()

Reported by @NarryG and @nyeogmi

* Sandbox updates

* Update ImageSharp again

(cherry picked from commit 7d778248ee)
(cherry picked from commit f66cda74e95619ddba2221bda644bf4394619805)
2024-08-11 17:54:19 +02:00
metalgearsloth
da5416a2da Version: 226.2.0 2024-06-20 17:28:11 +10:00
metalgearsloth
021845d956 Add some System.Random methods (#5177)
* Add some System.Random methods

* weh
2024-06-20 17:23:47 +10:00
Leon Friedrich
7fab9f3b8d Fix ContainerSystem debug assert (#5254) 2024-06-20 17:23:38 +10:00
Pieter-Jan Briers
69c1161562 FormattedMessage/DebugConsole performance improvements (#5244)
* Add VisibilityChanged virtual to Control

* Defer updating invisible OutputPanels on UIScale change

DebugConsole falls under this when not hidden, and it significantly improves perf of e.g. resizing the window when there's a lot of stuff in there.

* Avoid redundant UI Scale updates on window resize.

Window resizing can change the UI scale, due to the auto-scaling system. This system had multiple perf issues:

UI scale was set and propagated even if it didn't change (system disabled, not effective, etc). This was just wasted processing.

UI scale was updated for every window resize event. When the game is lagging (due to the aforementioned UI scale updates being expensive...) this means multiple window resize events in a single frame ALL cause a UI scale update, which is useless.

UI scale updates from resizing now avoid doing *nothing* and are deferred until later in the frame for natural batching.

* Reduce allocations/memory usage of various rich-text related things

Just allocate a buncha dictionaries what could possibly go wrong.

I kept to non-breaking-changes which means this couldn't as effective as it should be.

There's some truly repulsive stuff here. Ugh.

* Cap debug console content size.

It's a CVar.

OutputPanel has been switched to use a new RingBufferList datastructure to make removal of the oldest entry efficient.

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2024-06-20 17:22:12 +10:00
metalgearsloth
095fe9d60f Turn broadphase contacts into a job (#5245)
Okay when I said no more physics this was a low-hanging fruit as we can get rid of the mapmanager getmapentityid for every contact so.
2024-06-20 17:19:26 +10:00
Leon Friedrich
14138fbcc2 Separate PVS serialization from compression & sending (#5246) 2024-06-20 17:18:51 +10:00
Pieter-Jan Briers
48ce24e98b Remove race condition invoking ThreadPool.SetMinThreads call
For some reason we call ThreadPool.SetMinThreads on startup of the game server. Calling this function this early seems to put us at high risk of triggering the following deadlock bug in the .NET runtime: https://github.com/dotnet/runtime/issues/93175

Given I have zero trust in whether this manual ThreadPool fuckery is even helpful, I'm just gonna nuke it and call it a day.
2024-06-20 03:12:01 +02:00
Pieter-Jan Briers
9cde21a7b3 Lower default MTU again.
Yet more reports of people running into issues with the current default.
2024-06-20 00:15:27 +02:00
Pieter-Jan Briers
ae1051e813 Cache non-existence of ResourceCache TryGetResource.
Many patterns (both in engine and content) make use of regular TryGetResource returning null. The problem is that if the resource doesn't exist, it won't be cached and the code attempts to load it from disk *every single time*.

For example, opening an inventory in SS14 would hang the client for ages on some UI themes due to the UITheme texture fallback system constantly trying to load a texture that doesn't exist.
2024-06-19 22:50:09 +02:00
Leon Friedrich
a3f80ac7dd Increase default value of res.rsi_atlas_size (#5250) 2024-06-19 22:09:39 +02:00
CaasGit
f98ef78a21 Update LoaderApi to the latest commit. (#5256) 2024-06-19 19:52:49 +02:00
metalgearsloth
bf8054b181 Version: 226.1.0 2024-06-18 21:50:18 +10:00
metalgearsloth
6b875e6676 Add local entities APIs (#5178)
Need for some vgroid stuff
2024-06-18 21:41:38 +10:00
Vasilis
a687c0a6c0 Change "to" to "from" on advert error (#5247)
It's a message FROM the hub

Currently, if you get "You are banned from the hub, if you believe this is an error contact us" it may confuse someone that they have to visit the hub URL where they will be met with a 404 because it's not an actual website. Seems it looks like "contact us to website"

Similarly, with "Failed to contact status address" makes it look like it's an error message coming from robust failing to connect to the hub server. When it's actually coming from the hub, telling you probably don't have your ports open.

I believe changing it to "from" will get the message acros that this is a message from the HUB and not robust.
2024-06-17 15:15:30 +02:00
Pieter-Jan Briers
0580cf3ff7 Drop SQL exporter in Robust.Benchmarks to fix compilation.
It was using an old Npgsql version, which broke compilation. Updating it breaks some of the custom JSON mapping code.

Comment out the entire thing, it's not being used anymore anyways.
2024-06-17 02:00:11 +02:00
Pieter-Jan Briers
590964d5bf Update SpaceWizards.HttpListener to 0.1.1
This fixes an EXTREMELY RARE crash on server startup due to a race condition. Yes, it did cause a crash in practice that's how I noticed it.
2024-06-16 21:34:15 +02:00
Pieter-Jan Briers
ceda39813d Fix MsgPlayerList being capped to 255
WHY WAS THIS A BYTE.

This prevented having more than 255 people on a server, beyond that the game might get stuck as people's player states wouldn't necessarily get sent.
2024-06-16 21:31:57 +02:00
43 changed files with 1230 additions and 330 deletions

View File

@@ -43,7 +43,7 @@
<PackageVersion Include="NUnit.Analyzers" Version="3.10.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Nett" Version="0.15.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<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" />
@@ -55,8 +55,8 @@
<PackageVersion Include="Serilog" Version="3.1.1" />
<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.3" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.1.1" />
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.0.2" />
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
@@ -71,4 +71,4 @@
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="PolySharp" Version="1.14.1" />
</ItemGroup>
</Project>
</Project>

View File

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

View File

@@ -54,6 +54,51 @@ END TEMPLATE-->
*None yet*
## 226.2.2
## 226.2.1
## 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.0
### Breaking changes

View File

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

View File

@@ -15,11 +15,10 @@ 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
@@ -98,7 +97,9 @@ public sealed class SQLExporter : IExporter
public string Name => "sql";
}
*/
/*
// https://github.com/npgsql/efcore.pg/issues/1107#issuecomment-945126627
class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
@@ -138,6 +139,7 @@ class JsonOverrideTypeHandlerResolverFactory : TypeHandlerResolverFactory
=> null; // Let the built-in resolver do this
}
}
*/
public sealed class DesignTimeContextFactoryPostgres : IDesignTimeDbContextFactory<BenchmarkContext>
{

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Console.Commands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var type = Type.GetType(args[0]);
var type = GetType(args[0]);
if (type == null)
{
@@ -25,6 +25,17 @@ namespace Robust.Client.Console.Commands
shell.WriteLine(sig);
}
}
private Type? GetType(string name)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
if (assembly.GetType(name) is { } type)
return type;
}
return null;
}
}
#endif
}

View File

@@ -47,7 +47,7 @@ namespace Robust.Client.ResourceManagement
{
sawmill.Debug("Preloading textures...");
var sw = Stopwatch.StartNew();
var resList = GetTypeDict<TextureResource>();
var resList = GetTypeData<TextureResource>().Resources;
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 = GetTypeDict<RSIResource>();
var resList = GetTypeData<RSIResource>().Resources;
var rsiList = _manager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"))

View File

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

View File

@@ -212,9 +212,18 @@ namespace Robust.Client.UserInterface
}
}
/// <summary>
/// Called when this control's visibility in the control tree changed.
/// </summary>
protected virtual void VisibilityChanged(bool newVisible)
{
}
private void _propagateVisibilityChanged(bool newVisible)
{
VisibilityChanged(newVisible);
OnVisibilityChanged?.Invoke(this);
if (!VisibleInTree)
{
UserInterfaceManagerInternal.ControlHidden(this);

View File

@@ -1,9 +1,8 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.RichText;
using Robust.Shared.Collections;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
@@ -20,7 +19,7 @@ namespace Robust.Client.UserInterface.Controls
public const string StylePropertyStyleBox = "stylebox";
private readonly List<RichTextEntry> _entries = new();
private readonly RingBufferList<RichTextEntry> _entries = new();
private bool _isAtBottom = true;
private int _totalContentHeight;
@@ -30,6 +29,8 @@ namespace Robust.Client.UserInterface.Controls
public bool ScrollFollowing { get; set; } = true;
private bool _invalidOnVisible;
public OutputPanel()
{
IoCManager.InjectDependencies(this);
@@ -45,6 +46,8 @@ namespace Robust.Client.UserInterface.Controls
_scrollBar.OnValueChanged += _ => _isAtBottom = _scrollBar.IsAtEnd;
}
public int EntryCount => _entries.Count;
public StyleBox? StyleBoxOverride
{
get => _styleBoxOverride;
@@ -91,7 +94,7 @@ namespace Robust.Client.UserInterface.Controls
{
var entry = new RichTextEntry(message, this, _tagManager, null);
entry.Update(_getFont(), _getContentBox().Width, UIScale);
entry.Update(_tagManager, _getFont(), _getContentBox().Width, UIScale);
_entries.Add(entry);
var font = _getFont();
@@ -134,7 +137,7 @@ namespace Robust.Client.UserInterface.Controls
// So when a new color tag gets hit this stack gets the previous color pushed on.
var context = new MarkupDrawingContext(2);
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
foreach (ref var entry in _entries)
{
if (entryOffset + entry.Height < 0)
{
@@ -147,7 +150,7 @@ namespace Robust.Client.UserInterface.Controls
break;
}
entry.Draw(handle, font, contentBox, entryOffset, context, UIScale);
entry.Draw(_tagManager, handle, font, contentBox, entryOffset, context, UIScale);
entryOffset += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -185,9 +188,9 @@ namespace Robust.Client.UserInterface.Controls
_totalContentHeight = 0;
var font = _getFont();
var sizeX = _getContentBox().Width;
foreach (ref var entry in CollectionsMarshal.AsSpan(_entries))
foreach (ref var entry in _entries)
{
entry.Update(font, sizeX, UIScale);
entry.Update(_tagManager, font, sizeX, UIScale);
_totalContentHeight += entry.Height + font.GetLineSeparation(UIScale);
}
@@ -239,7 +242,13 @@ namespace Robust.Client.UserInterface.Controls
protected internal override void UIScaleChanged()
{
_invalidateEntries();
// If this control isn't visible, don't invalidate entries immediately.
// This saves invalidating the debug console if it's hidden,
// which is a huge boon as auto-scaling changes UI scale a lot in that scenario.
if (!VisibleInTree)
_invalidOnVisible = true;
else
_invalidateEntries();
base.UIScaleChanged();
}
@@ -257,5 +266,14 @@ namespace Robust.Client.UserInterface.Controls
// existing ones were valid when the UI scale was set.
_invalidateEntries();
}
protected override void VisibilityChanged(bool newVisible)
{
if (newVisible && _invalidOnVisible)
{
_invalidateEntries();
_invalidOnVisible = false;
}
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Robust.Client.UserInterface.Controls
}
var font = _getFont();
_entry.Update(font, availableSize.X * UIScale, UIScale, LineHeightScale);
_entry.Update(_tagManager, font, availableSize.X * UIScale, UIScale, LineHeightScale);
return new Vector2(_entry.Width / UIScale, _entry.Height / UIScale);
}
@@ -82,7 +82,7 @@ namespace Robust.Client.UserInterface.Controls
return;
}
_entry.Draw(handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
_entry.Draw(_tagManager, handle, _getFont(), SizeBox, 0, new MarkupDrawingContext(), UIScale, LineHeightScale);
}
[Pure]

View File

@@ -13,6 +13,12 @@ namespace Robust.Client.UserInterface.Controls
}
public override float UIScale => UIScaleSet;
internal float UIScaleSet { get; set; }
/// <summary>
/// Set after the window is resized, to batch up UI scale updates on window resizes.
/// </summary>
internal bool UIScaleUpdateNeeded { get; set; }
public override IClydeWindow Window { get; }
/// <summary>

View File

@@ -7,6 +7,7 @@ using Robust.Client.AutoGenerated;
using Robust.Client.Console;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Input;
@@ -51,6 +52,8 @@ namespace Robust.Client.UserInterface.CustomControls
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
private readonly ISawmill _logger;
private int _maxEntries;
public DebugConsole()
{
RobustXamlLoader.Load(this);
@@ -78,6 +81,7 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.AddString += OnAddString;
_consoleHost.AddFormatted += OnAddFormatted;
_consoleHost.ClearText += OnClearText;
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
}
@@ -89,10 +93,17 @@ namespace Robust.Client.UserInterface.CustomControls
_consoleHost.AddString -= OnAddString;
_consoleHost.AddFormatted -= OnAddFormatted;
_consoleHost.ClearText -= OnClearText;
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
}
private void MaxEntriesChanged(int value)
{
_maxEntries = value;
TrimExtraOutputEntries();
}
private void OnClearText(object? _, EventArgs args)
{
Clear();
@@ -165,6 +176,15 @@ namespace Robust.Client.UserInterface.CustomControls
private void _addFormattedLineInternal(FormattedMessage message)
{
Output.AddMessage(message);
TrimExtraOutputEntries();
}
private void TrimExtraOutputEntries()
{
while (Output.EntryCount > _maxEntries)
{
Output.RemoveEntry(0);
}
}
private void _flushQueue()

View File

@@ -17,7 +17,6 @@ namespace Robust.Client.UserInterface
internal struct RichTextEntry
{
private readonly Color _defaultColor;
private readonly MarkupTagManager _tagManager;
private readonly Type[]? _tagsAllowed;
public readonly FormattedMessage Message;
@@ -37,7 +36,7 @@ namespace Robust.Client.UserInterface
/// </summary>
public ValueList<int> LineBreaks;
private readonly Dictionary<int, Control> _tagControls = new();
private readonly Dictionary<int, Control>? _tagControls;
public RichTextEntry(FormattedMessage message, Control parent, MarkupTagManager tagManager, Type[]? tagsAllowed = null, Color? defaultColor = null)
{
@@ -46,23 +45,26 @@ namespace Robust.Client.UserInterface
Width = 0;
LineBreaks = default;
_defaultColor = defaultColor ?? new(200, 200, 200);
_tagManager = tagManager;
_tagsAllowed = tagsAllowed;
Dictionary<int, Control>? tagControls = null;
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
if (node.Name == null)
continue;
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag) || !tag.TryGetControl(node, out var control))
continue;
parent.Children.Add(control);
_tagControls.Add(nodeIndex, control);
tagControls ??= new Dictionary<int, Control>();
tagControls.Add(nodeIndex, control);
}
_tagControls = tagControls;
}
/// <summary>
@@ -72,7 +74,7 @@ namespace Robust.Client.UserInterface
/// <param name="maxSizeX">The maximum horizontal size of the container of this entry.</param>
/// <param name="uiScale"></param>
/// <param name="lineHeightScale"></param>
public void Update(Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
public void Update(MarkupTagManager tagManager, Font defaultFont, float maxSizeX, float uiScale, float lineHeightScale = 1)
{
// This method is gonna suck due to complexity.
// Bear with me here.
@@ -91,10 +93,10 @@ namespace Robust.Client.UserInterface
// Nodes can change the markup drawing context and return additional text.
// It's also possible for nodes to return inline controls. They get treated as one large rune.
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(node, context);
var text = ProcessNode(tagManager, node, context);
if (!context.Font.TryPeek(out var font))
font = defaultFont;
@@ -113,7 +115,7 @@ namespace Robust.Client.UserInterface
return;
}
if (!_tagControls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
if (ProcessRune(ref this, new Rune(' '), out breakLine))
@@ -166,6 +168,7 @@ namespace Robust.Client.UserInterface
}
public readonly void Draw(
MarkupTagManager tagManager,
DrawingHandleScreen handle,
Font defaultFont,
UIBox2 drawBox,
@@ -184,10 +187,10 @@ namespace Robust.Client.UserInterface
var controlYAdvance = 0f;
var nodeIndex = -1;
foreach (var node in Message.Nodes)
foreach (var node in Message)
{
nodeIndex++;
var text = ProcessNode(node, context);
var text = ProcessNode(tagManager, node, context);
if (!context.Color.TryPeek(out var color) || !context.Font.TryPeek(out var font))
{
color = _defaultColor;
@@ -210,7 +213,7 @@ namespace Robust.Client.UserInterface
globalBreakCounter += 1;
}
if (!_tagControls.TryGetValue(nodeIndex, out var control))
if (_tagControls == null || !_tagControls.TryGetValue(nodeIndex, out var control))
continue;
var invertedScale = 1f / uiScale;
@@ -223,24 +226,22 @@ namespace Robust.Client.UserInterface
}
}
private readonly string ProcessNode(MarkupNode node, MarkupDrawingContext context)
private readonly string ProcessNode(MarkupTagManager tagManager, MarkupNode node, MarkupDrawingContext context)
{
// If a nodes name is null it's a text node.
if (node.Name == null)
return node.Value.StringValue ?? "";
//Skip the node if there is no markup tag for it.
if (!_tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
if (!tagManager.TryGetMarkupTag(node.Name, _tagsAllowed, out var tag))
return "";
if (!node.Closing)
{
context.Tags.Add(tag);
tag.PushDrawContext(node, context);
return tag.TextBefore(node);
}
context.Tags.Remove(tag);
tag.PopDrawContext(node, context);
return tag.TextAfter(node);
}

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
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;
@@ -115,6 +117,16 @@ internal sealed class PvsSession(ICommonSession session, ResizableMemoryRegion<P
/// </summary>
public GameState? State;
/// <summary>
/// The serialized <see cref="State"/> object.
/// </summary>
public MemoryStream? StateStream;
/// <summary>
/// Whether we should force reliable sending of the <see cref="MsgState"/>.
/// </summary>
public bool ForceSendReliably { get; set; }
/// <summary>
/// Clears all stored game state data. This should only be used after the game state has been serialized.
/// </summary>

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,6 @@ 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;
@@ -27,49 +25,6 @@ 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))
@@ -104,7 +59,7 @@ internal sealed partial class PvsSystem
session.PlayerStates,
_deletedEntities);
session.State.ForceSendReliably = session.RequestedFull
session.ForceSendReliably = session.RequestedFull
|| _gameTiming.CurTick > session.LastReceivedAck + (uint) ForceAckThreshold;
}

View File

@@ -3,10 +3,8 @@ 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;
@@ -16,9 +14,6 @@ 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;
@@ -99,6 +94,10 @@ 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;
@@ -183,52 +182,25 @@ 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.
BeforeSendState();
BeforeSerializeStates();
// Construct & send the game state to each player.
SendStates(players);
// Construct & serialize the game state for each player (and for the replay).
SerializeStates();
// Compress & send the states.
SendStates();
// Cull deletion history
AfterSendState(players);
AfterSerializeStates();
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;
}
ProcessLeavePvs();
}
private void ResetParallelism(int _) => ResetParallelism();
@@ -414,23 +386,11 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void BeforeSendState()
private void BeforeSerializeStates()
{
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.
@@ -443,6 +403,21 @@ 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);
@@ -452,9 +427,9 @@ internal sealed partial class PvsSystem : EntitySystem
}
}
private void AfterSendState(ICommonSession[] players)
private void AfterSerializeStates()
{
CleanupDirty(players);
CleanupDirty();
if (_oldestAck == GameTick.MaxValue.Value)
{

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,7 @@ namespace Robust.Shared
/// <seealso cref="NetMtuExpand"/>
/// <seealso cref="NetMtuIpv6"/>
public static readonly CVarDef<int> NetMtu =
CVarDef.Create("net.mtu", 900, CVar.ARCHIVE);
CVarDef.Create("net.mtu", 700, 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", 8192, CVar.CLIENTONLY);
CVarDef.Create("res.rsi_atlas_size", 12288, CVar.CLIENTONLY);
// TODO: Currently unimplemented.
/// <summary>
@@ -1560,6 +1560,12 @@ namespace Robust.Shared
public static readonly CVarDef<int> ConCompletionMargin =
CVarDef.Create("con.completion_margin", 3, CVar.CLIENTONLY);
/// <summary>
/// Maximum amount of entries stored by the debug console.
/// </summary>
public static readonly CVarDef<int> ConMaxEntries =
CVarDef.Create("con.max_entries", 3_000, CVar.CLIENTONLY);
/*
* THREAD
*/

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Robust.Shared.Utility;
using ArgumentNullException = System.ArgumentNullException;
namespace Robust.Shared.Collections;
/// <summary>
/// Datastructure that acts like a <see cref="List{T}"/>, but is actually stored as a ring buffer internally.
/// This facilitates efficient removal from the start.
/// </summary>
/// <typeparam name="T">Type of item contained in the collection.</typeparam>
internal sealed class RingBufferList<T> : IList<T>
{
private T[] _items;
private int _read;
private int _write;
public RingBufferList(int capacity)
{
_items = new T[capacity];
}
public RingBufferList()
{
_items = [];
}
public int Capacity => _items.Length;
private bool IsFull => _items.Length == 0 || NextIndex(_write) == _read;
public void Add(T item)
{
if (IsFull)
Expand();
DebugTools.Assert(!IsFull);
_items[_write] = item;
_write = NextIndex(_write);
}
public void Clear()
{
_read = 0;
_write = 0;
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
Array.Clear(_items);
}
public bool Contains(T item)
{
return IndexOf(item) >= 0;
}
public void CopyTo(T[] array, int arrayIndex)
{
ArgumentNullException.ThrowIfNull(array);
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
CopyTo(array.AsSpan(arrayIndex));
}
private void CopyTo(Span<T> dest)
{
if (dest.Length < Count)
throw new ArgumentException("Not enough elements in destination!");
var i = 0;
foreach (var item in this)
{
dest[i++] = item;
}
}
public bool Remove(T item)
{
var index = IndexOf(item);
if (index < 0)
return false;
RemoveAt(index);
return true;
}
public int Count
{
get
{
var length = _write - _read;
if (length >= 0)
return length;
return length + _items.Length;
}
}
public bool IsReadOnly => false;
public int IndexOf(T item)
{
var i = 0;
foreach (var containedItem in this)
{
if (EqualityComparer<T>.Default.Equals(item, containedItem))
return i;
i += 1;
}
return -1;
}
public void Insert(int index, T item)
{
throw new NotSupportedException();
}
public void RemoveAt(int index)
{
var length = Count;
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, length);
if (index == 0)
{
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
_items[_read] = default!;
_read = NextIndex(_read);
}
else if (index == length - 1)
{
_write = WrapInv(_write - 1);
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
_items[_write] = default!;
}
else
{
// If past me had better foresight I wouldn't be spending so much effort writing this right now.
var realIdx = RealIndex(index);
var origValue = _items[realIdx];
T result;
if (realIdx < _read)
{
// Scenario one: to-remove index is after break.
// One shift is needed.
// v
// X X X O X X
// W R
DebugTools.Assert(_write < _read);
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
}
else if (_write < _read)
{
// Scenario two: to-remove index is before break, but write is after.
// Two shifts are needed.
// v
// X O X X X X
// W R
var fromEnd = ShiftDown(_items.AsSpan(0, _write), default!);
result = ShiftDown(_items.AsSpan(realIdx), fromEnd);
}
else
{
// Scenario two: array is contiguous.
// One shift is needed.
// v
// X X X X O O
// R W
result = ShiftDown(_items.AsSpan()[realIdx.._write], default!);
}
// Just make sure we didn't bulldozer something.
DebugTools.Assert(EqualityComparer<T>.Default.Equals(origValue, result));
_write = WrapInv(_write - 1);
}
}
private static T ShiftDown(Span<T> span, T substitution)
{
if (span.Length == 0)
return substitution;
var first = span[0];
span[1..].CopyTo(span[..^1]);
span[^1] = substitution!;
return first;
}
public T this[int index]
{
get => GetSlot(index);
set => GetSlot(index) = value;
}
private ref T GetSlot(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count);
return ref _items[RealIndex(index)];
}
private int RealIndex(int index)
{
return Wrap(index + _read);
}
private int NextIndex(int index) => Wrap(index + 1);
private int Wrap(int index)
{
if (index >= _items.Length)
index -= _items.Length;
return index;
}
private int WrapInv(int index)
{
if (index < 0)
index = _items.Length - 1;
return index;
}
private void Expand()
{
var prevSize = _items.Length;
var newSize = Math.Max(4, prevSize * 2);
Array.Resize(ref _items, newSize);
if (_write >= _read)
return;
// Write is behind read pointer, so we need to copy the items to be after the read pointer.
var toCopy = _items.AsSpan(0, _write);
var copyDest = _items.AsSpan(prevSize);
toCopy.CopyTo(copyDest);
if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
toCopy.Clear();
_write += prevSize;
}
public Enumerator GetEnumerator()
{
return new Enumerator(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<T>
{
private readonly RingBufferList<T> _ringBufferList;
private int _readPos;
internal Enumerator(RingBufferList<T> ringBufferList)
{
_ringBufferList = ringBufferList;
_readPos = _ringBufferList._read - 1;
}
public bool MoveNext()
{
_readPos = _ringBufferList.NextIndex(_readPos);
return _readPos != _ringBufferList._write;
}
public void Reset()
{
this = new Enumerator(_ringBufferList);
}
public ref T Current => ref _ringBufferList._items[_readPos];
T IEnumerator<T>.Current => Current;
object? IEnumerator.Current => Current;
void IDisposable.Dispose()
{
}
}
}

View File

@@ -40,8 +40,8 @@ public abstract partial class SharedContainerSystem
DebugTools.AssertOwner(container.Owner, containerXform);
DebugTools.AssertOwner(toInsert, physics);
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)));
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID));
DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)), "entity is expected");
DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID), "manager does not own the container");
// 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);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0, "invalid metadata flags before insertion");
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());
DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid(), "invalid broadphase");
// 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);
DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after insertion");
// 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);
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));
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");
Dirty(container.Owner, container.Manager);
return true;

View File

@@ -41,7 +41,7 @@ public abstract partial class SharedContainerSystem
return false;
DebugTools.AssertNotNull(container.Manager);
DebugTools.Assert(Exists(toRemove));
DebugTools.Assert(Exists(toRemove), "toRemove does not exist");
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));
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));
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");
// 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));
DebugTools.Assert(destination == null || xform.Coordinates.Equals(destination.Value), "failed to set destination");
Dirty(container.Owner, container.Manager);
return true;

View File

@@ -32,7 +32,7 @@ namespace Robust.Shared.ContentPack
String("short").ThenReturn(PrimitiveTypeCode.Int16);
private static readonly Parser<char, PrimitiveTypeCode> UInt16TypeParser =
String("ushort").ThenReturn(PrimitiveTypeCode.UInt32);
String("ushort").ThenReturn(PrimitiveTypeCode.UInt16);
private static readonly Parser<char, PrimitiveTypeCode> Int32TypeParser =
String("int").ThenReturn(PrimitiveTypeCode.Int32);

View File

@@ -84,12 +84,146 @@ Types:
- "bool get_HasContents()"
Lidgren.Network:
NetBuffer:
All: True
Methods:
- "byte[] get_Data()"
- "void set_Data(byte[])"
- "int get_LengthBytes()"
- "void set_LengthBytes(int)"
- "int get_LengthBits()"
- "void set_LengthBits(int)"
- "long get_Position()"
- "void set_Position(long)"
- "int get_PositionInBytes()"
- "byte[] PeekDataBuffer()"
- "bool PeekBoolean()"
- "byte PeekByte()"
- "sbyte PeekSByte()"
- "byte PeekByte(int)"
- "System.Span`1<byte> PeekBytes(System.Span`1<byte>)"
- "byte[] PeekBytes(int)"
- "void PeekBytes(byte[], int, int)"
- "short PeekInt16()"
- "ushort PeekUInt16()"
- "int PeekInt32()"
- "int PeekInt32(int)"
- "uint PeekUInt32()"
- "uint PeekUInt32(int)"
- "ulong PeekUInt64()"
- "long PeekInt64()"
- "ulong PeekUInt64(int)"
- "long PeekInt64(int)"
- "float PeekFloat()"
- "System.Half PeekHalf()"
- "float PeekSingle()"
- "double PeekDouble()"
- "string PeekString()"
- "int PeekStringSize()"
- "bool ReadBoolean()"
- "byte ReadByte()"
- "bool ReadByte(ref byte)"
- "sbyte ReadSByte()"
- "byte ReadByte(int)"
- "System.Span`1<byte> ReadBytes(System.Span`1<byte>)"
- "byte[] ReadBytes(int)"
- "bool ReadBytes(int, ref byte[])"
- "bool TryReadBytes(System.Span`1<byte>)"
- "void ReadBytes(byte[], int, int)"
- "void ReadBits(System.Span`1<byte>, int)"
- "void ReadBits(byte[], int, int)"
- "short ReadInt16()"
- "ushort ReadUInt16()"
- "int ReadInt32()"
- "bool ReadInt32(ref int)"
- "int ReadInt32(int)"
- "uint ReadUInt32()"
- "bool ReadUInt32(ref uint)"
- "uint ReadUInt32(int)"
- "ulong ReadUInt64()"
- "long ReadInt64()"
- "ulong ReadUInt64(int)"
- "long ReadInt64(int)"
- "float ReadFloat()"
- "System.Half ReadHalf()"
- "float ReadSingle()"
- "bool ReadSingle(ref float)"
- "double ReadDouble()"
- "uint ReadVariableUInt32()"
- "bool ReadVariableUInt32(ref uint)"
- "int ReadVariableInt32()"
- "long ReadVariableInt64()"
- "ulong ReadVariableUInt64()"
- "float ReadSignedSingle(int)"
- "float ReadUnitSingle(int)"
- "float ReadRangedSingle(float, float, int)"
- "int ReadRangedInteger(int, int)"
- "long ReadRangedInteger(long, long)"
- "string ReadString()"
- "bool ReadString(ref string)"
- "double ReadTime(Lidgren.Network.NetConnection, bool)"
- "System.Net.IPEndPoint ReadIPEndPoint()"
- "void SkipPadBits()"
- "void ReadPadBits()"
- "void SkipPadBits(int)"
- "void EnsureBufferSize(int)"
- "void Write(bool)"
- "void Write(byte)"
- "void WriteAt(int, byte)"
- "void Write(sbyte)"
- "void Write(byte, int)"
- "void Write(byte[])"
- "void Write(System.ReadOnlySpan`1<byte>)"
- "void Write(byte[], int, int)"
- "void Write(ushort)"
- "void WriteAt(int, ushort)"
- "void Write(ushort, int)"
- "void Write(short)"
- "void WriteAt(int, short)"
- "void Write(int)"
- "void WriteAt(int, int)"
- "void Write(uint)"
- "void WriteAt(int, uint)"
- "void Write(uint, int)"
- "void Write(int, int)"
- "void Write(ulong)"
- "void WriteAt(int, ulong)"
- "void Write(ulong, int)"
- "void Write(long)"
- "void Write(long, int)"
- "void Write(System.Half)"
- "void Write(float)"
- "void Write(double)"
- "int WriteVariableUInt32(uint)"
- "int WriteVariableInt32(int)"
- "int WriteVariableInt64(long)"
- "int WriteVariableUInt64(ulong)"
- "void WriteSignedSingle(float, int)"
- "void WriteUnitSingle(float, int)"
- "void WriteRangedSingle(float, float, float, int)"
- "int WriteRangedInteger(int, int, int)"
- "int WriteRangedInteger(long, long, long)"
- "void Write(string)"
- "void Write(System.Net.IPEndPoint)"
- "void WriteTime(bool)"
- "void WriteTime(double, bool)"
- "void WritePadBits()"
- "void WritePadBits(int)"
- "void Write(Lidgren.Network.NetBuffer)"
- "void Zero(int)"
- "void .ctor()"
NetDeliveryMethod: { }
NetIncomingMessage:
All: True
Methods:
- "Lidgren.Network.NetIncomingMessageType get_MessageType()"
- "Lidgren.Network.NetDeliveryMethod get_DeliveryMethod()"
- "int get_SequenceChannel()"
- "System.Net.IPEndPoint get_SenderEndPoint()"
- "Lidgren.Network.NetConnection get_SenderConnection()"
- "double get_ReceiveTime()"
- "double ReadTime(bool)"
- "string ToString()"
NetOutgoingMessage:
All: True
Methods:
- "string ToString()"
Nett:
CommentLocation: { } # Enum
Toml:

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Robust.Shared.Utility;
namespace Robust.Shared.ContentPack
@@ -135,11 +136,37 @@ namespace Robust.Shared.ContentPack
path = path.Directory;
var fullPath = GetFullPath(path);
Process.Start(new ProcessStartInfo
if (OperatingSystem.IsWindows())
{
UseShellExecute = true,
FileName = fullPath,
});
Process.Start(new ProcessStartInfo
{
FileName = $"{Environment.GetEnvironmentVariable("SystemRoot")}\\explorer.exe",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsMacOS())
{
Process.Start(new ProcessStartInfo
{
FileName = "open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
{
Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = ".",
WorkingDirectory = fullPath,
});
}
else
{
throw new NotSupportedException("Opening OS windows not supported on this OS");
}
}
#endregion

View File

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

View File

@@ -12,14 +12,13 @@ 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)
{
Plyrs = new List<SessionState>();
PlyCount = buffer.ReadByte();
for (var i = 0; i < PlyCount; i++)
var playerCount = buffer.ReadInt32();
Plyrs = new List<SessionState>(playerCount);
for (var i = 0; i < playerCount; i++)
{
var plyNfo = new SessionState
{
@@ -34,7 +33,7 @@ namespace Robust.Shared.Network.Messages
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(PlyCount);
buffer.Write(Plyrs.Count);
foreach (var ply in Plyrs)
{

View File

@@ -17,15 +17,21 @@ 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, compress it with deflate.
// If a state is larger than this, we will compress it
// TODO PVS make this a cvar
// TODO PVS figure out optimal value
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 HasWritten;
internal bool ForceSendReliably;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
@@ -60,26 +66,19 @@ namespace Robust.Shared.Network.Messages
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
using var stateStream = RobustMemoryManager.GetMemoryStream();
serializer.SerializeDirect(stateStream, State);
buffer.WriteVariableInt32((int)stateStream.Length);
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.
@@ -87,10 +86,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;
}
@@ -101,21 +100,12 @@ namespace Robust.Shared.Network.Messages
/// <returns></returns>
public bool ShouldSendReliably()
{
DebugTools.Assert(_hasWritten, "Attempted to determine sending method before determining packet size.");
return State.ForceSendReliably || MsgSize > ReliableThreshold;
DebugTools.Assert(HasWritten, "Attempted to determine sending method before determining packet size.");
return ForceSendReliably || MsgSize > ReliableThreshold;
}
public override NetDeliveryMethod DeliveryMethod
{
get
{
if (ShouldSendReliably())
{
return NetDeliveryMethod.ReliableUnordered;
}
return base.DeliveryMethod;
}
}
public override NetDeliveryMethod DeliveryMethod => ShouldSendReliably()
? NetDeliveryMethod.ReliableUnordered
: base.DeliveryMethod;
}
}

View File

@@ -31,11 +31,14 @@ 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
@@ -43,23 +46,21 @@ namespace Robust.Shared.Physics.Systems
* Hence we need to check which broadphases it does intersect and checkar for colliding bodies.
*/
/// <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);
private BroadphaseContactJob _contactJob;
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>();
@@ -71,7 +72,11 @@ namespace Robust.Shared.Physics.Systems
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
}
private void SetBroadphaseExpand(float value) => _broadphaseExpand = value;
private void SetBroadphaseExpand(float value)
{
_contactJob.BroadphaseExpand = value;
_broadphaseExpand = value;
}
#region Find Contacts
@@ -176,65 +181,34 @@ namespace Robust.Shared.Physics.Systems
if (moveBuffer.Count == 0)
return;
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;
_contactJob.MapUid = _mapManager.GetMapEntityIdOrThrow(mapId);
_contactJob.MoveBuffer.Clear();
foreach (var (proxy, aabb) in moveBuffer)
{
contactBuffer[idx] = _bufferPool.Get();
pMoveBuffer[idx++] = (proxy, aabb);
_contactJob.MoveBuffer.Add((proxy, aabb));
}
var options = new ParallelOptions
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
{
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
};
_contactJob.ContactBuffer.Add(new List<FixtureProxy>());
}
var batches = (int)MathF.Ceiling((float) count / PairBufferParallel);
var count = moveBuffer.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);
}
});
_parallel.ProcessNow(_contactJob, count);
for (var i = 0; i < count; i++)
{
var proxyA = pMoveBuffer[i].Proxy;
var proxies = contactBuffer[i];
var proxies = _contactJob.ContactBuffer[i];
if (proxies.Count == 0)
continue;
var proxyA = _contactJob.MoveBuffer[i].Proxy;
var proxyABody = proxyA.Body;
FixturesComponent? manager = null;
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
foreach (var other in proxies)
{
@@ -253,13 +227,8 @@ 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();
}
@@ -516,5 +485,51 @@ namespace Robust.Shared.Physics.Systems
}
}
}
private record struct BroadphaseContactJob() : IParallelRobustJob
{
public SharedBroadphaseSystem System = default!;
public IMapManager _mapManager = default!;
public float BroadphaseExpand;
public EntityUid MapUid;
public List<List<FixtureProxy>> ContactBuffer = new();
public List<(FixtureProxy Proxy, Box2 WorldAABB)> MoveBuffer = new();
public int BatchSize => 8;
public void Execute(int index)
{
var (proxy, worldAABB) = MoveBuffer[index];
var buffer = ContactBuffer[index];
buffer.Clear();
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (System, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(MapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
return true;
},
approx: true,
includeMap: false);
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
System.FindPairs(proxy, worldAABB, MapUid, buffer);
}
}
}
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -128,6 +129,9 @@ 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.
@@ -141,6 +145,9 @@ public static class RandomExtensions
return random.Next() * 4.6566128752458E-10f;
}
public static float NextFloat(this System.Random random, float minValue, float maxValue)
=> random.NextFloat() * (maxValue - minValue) + minValue;
/// <summary>
/// Have a certain chance to return a boolean.
/// </summary>

View File

@@ -220,14 +220,16 @@ 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)
object? arg,
string? message = null)
{
if (arg == null)
{
throw new DebugAssertException();
throw new DebugAssertException(message?? "value cannot be null");
}
}
@@ -236,14 +238,16 @@ 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)
object? arg,
string? message = null)
{
if (arg != null)
{
throw new DebugAssertException();
throw new DebugAssertException(message ?? "value should be null");
}
}
@@ -290,7 +294,7 @@ namespace Robust.Shared.Utility
{
}
public DebugAssertException(string message) : base(message)
public DebugAssertException(string? message) : base(message)
{
}
}

View File

@@ -16,26 +16,30 @@ namespace Robust.Shared.Utility;
/// </summary>
[PublicAPI]
[Serializable, NetSerializable]
public sealed partial class FormattedMessage
public sealed partial class FormattedMessage : IReadOnlyList<MarkupNode>
{
public static FormattedMessage Empty => new();
/// <summary>
/// The list of nodes the formatted message is made out of
/// </summary>
public IReadOnlyList<MarkupNode> Nodes => _nodes.AsReadOnly();
public IReadOnlyList<MarkupNode> Nodes => _nodes;
/// <summary>
/// true if the formatted message doesn't contain any nodes
/// </summary>
public bool IsEmpty => _nodes.Count == 0;
public int Count => _nodes.Count;
public MarkupNode this[int index] => _nodes[index];
private readonly List<MarkupNode> _nodes;
/// <summary>
/// Used for inserting the correct closing node when calling <see cref="Pop"/>
/// </summary>
private readonly Stack<MarkupNode> _openNodeStack = new();
private Stack<MarkupNode>? _openNodeStack;
public FormattedMessage()
{
@@ -199,6 +203,7 @@ public sealed partial class FormattedMessage
return;
}
_openNodeStack ??= new Stack<MarkupNode>();
_openNodeStack.Push(markupNode);
}
@@ -207,7 +212,7 @@ public sealed partial class FormattedMessage
/// </summary>
public void Pop()
{
if (!_openNodeStack.TryPop(out var node))
if (_openNodeStack == null || !_openNodeStack.TryPop(out var node))
return;
_nodes.Add(new MarkupNode(node.Name, null, null, true));
@@ -238,6 +243,16 @@ public sealed partial class FormattedMessage
return new FormattedMessageRuneEnumerator(this);
}
public NodeEnumerator GetEnumerator()
{
return new NodeEnumerator(_nodes.GetEnumerator());
}
IEnumerator<MarkupNode> IEnumerable<MarkupNode>.GetEnumerator()
{
return GetEnumerator();
}
/// <returns>The string without markup tags.</returns>
public override string ToString()
{
@@ -252,6 +267,11 @@ public sealed partial class FormattedMessage
return builder.ToString();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <returns>The string without filtering out markup tags.</returns>
public string ToMarkup()
{
@@ -261,13 +281,13 @@ public sealed partial class FormattedMessage
public struct FormattedMessageRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
{
private readonly FormattedMessage _msg;
private IEnumerator<MarkupNode> _tagEnumerator;
private List<MarkupNode>.Enumerator _tagEnumerator;
private StringRuneEnumerator _runeEnumerator;
internal FormattedMessageRuneEnumerator(FormattedMessage msg)
{
_msg = msg;
_tagEnumerator = msg.Nodes.GetEnumerator();
_tagEnumerator = msg._nodes.GetEnumerator();
// Rune enumerator will immediately give false on first iteration so I dont' need to special case anything.
_runeEnumerator = "".EnumerateRunes();
}
@@ -301,7 +321,7 @@ public sealed partial class FormattedMessage
public void Reset()
{
_tagEnumerator = _msg.Nodes.GetEnumerator();
_tagEnumerator = _msg._nodes.GetEnumerator();
_runeEnumerator = "".EnumerateRunes();
}
@@ -313,4 +333,33 @@ public sealed partial class FormattedMessage
{
}
}
public struct NodeEnumerator : IEnumerator<MarkupNode>
{
private List<MarkupNode>.Enumerator _enumerator;
internal NodeEnumerator(List<MarkupNode>.Enumerator enumerator)
{
_enumerator = enumerator;
}
public bool MoveNext()
{
return _enumerator.MoveNext();
}
void IEnumerator.Reset()
{
((IEnumerator) _enumerator).Reset();
}
public MarkupNode Current => _enumerator.Current;
object IEnumerator.Current => Current;
public void Dispose()
{
_enumerator.Dispose();
}
}
}

View File

@@ -11,7 +11,7 @@ public sealed class MarkupNode : IComparable<MarkupNode>
{
public readonly string? Name;
public readonly MarkupParameter Value;
public readonly Dictionary<string, MarkupParameter> Attributes = new();
public readonly Dictionary<string, MarkupParameter> Attributes;
public readonly bool Closing;
/// <summary>
@@ -20,6 +20,7 @@ public sealed class MarkupNode : IComparable<MarkupNode>
/// <param name="text">The plaintext the tag will consist of</param>
public MarkupNode(string text)
{
Attributes = new Dictionary<string, MarkupParameter>();
Value = new MarkupParameter(text);
}

View File

@@ -0,0 +1,95 @@
using NUnit.Framework;
using Robust.Shared.Collections;
namespace Robust.UnitTesting.Shared.Collections;
[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)]
[TestFixture, TestOf(typeof(RingBufferList<>))]
public sealed class RingBufferListTest
{
[Test]
public void TestBasicAdd()
{
var list = new RingBufferList<int>();
list.Add(1);
list.Add(2);
list.Add(3);
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3}));
}
[Test]
public void TestBasicAddAfterWrap()
{
var list = new RingBufferList<int>(6);
list.Add(1);
list.Add(2);
list.Add(3);
list.RemoveAt(0);
list.Add(4);
list.Add(5);
list.Add(6);
Assert.Multiple(() =>
{
// Ensure wrapping properly happened and we didn't expand.
// (one slot is wasted by nature of implementation)
Assert.That(list.Capacity, NUnit.Framework.Is.EqualTo(6));
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] { 2, 3, 4, 5, 6 }));
});
}
[Test]
public void TestMiddleRemoveAtScenario1()
{
var list = new RingBufferList<int>(6);
list.Add(-1);
list.Add(-1);
list.Add(-1);
list.Add(-1);
list.Add(1);
list.RemoveAt(0);
list.RemoveAt(0);
list.RemoveAt(0);
list.RemoveAt(0);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
list.Remove(4);
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5}));
}
[Test]
public void TestMiddleRemoveAtScenario2()
{
var list = new RingBufferList<int>(6);
list.Add(-1);
list.Add(-1);
list.Add(1);
list.RemoveAt(0);
list.RemoveAt(0);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
list.Remove(3);
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 4, 5}));
}
[Test]
public void TestMiddleRemoveAtScenario3()
{
var list = new RingBufferList<int>(6);
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
list.Remove(4);
Assert.That(list, NUnit.Framework.Is.EquivalentTo(new[] {1, 2, 3, 5}));
}
}