From b7de935f3c755add405ce36b64630adf71854bdf Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Wed, 20 May 2026 02:06:43 +1000 Subject: [PATCH 001/178] Make windows keep relative position when game re-sizes Previously they would keep absolute pixel position but sometimes it can be annoying, this just keeps them where you'd expect them to be. --- .../UserInterface/UserInterfaceManagerTest.cs | 38 +++++++++++ .../CustomControls/BaseWindow.cs | 63 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs b/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs index ce5e891d6cb..4faef41172f 100644 --- a/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs +++ b/Robust.Client.IntegrationTests/UserInterface/UserInterfaceManagerTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Map; @@ -242,5 +243,42 @@ public void TestNotGrabKeyboardFocusOnClick() control.Orphan(); } + + /// + /// Assert that windows correctly re-position themselves if their parent is re-sized. + /// + [Test] + public void TestWindowTracksRelativePositionOnResize() + { + _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 100, 100)); + + var window = new TestWindow + { + SetSize = new Vector2(20, 20), + }; + + _userInterfaceManager.WindowRoot.AddChild(window); + + var initialPos = new Vector2(40, 40); + + LayoutContainer.SetPosition(window, initialPos); + Assert.That(window.Position, Is.EqualTo(initialPos)); + + _userInterfaceManager.WindowRoot.InvalidateArrange(); + _userInterfaceManager.RootControl.Arrange(new UIBox2(0, 0, 200, 200)); + + // 40,40 x 2 + 10 + Assert.That(window.Position, Is.EqualTo(new Vector2(90, 90))); + + window.Orphan(); + } + + private sealed class TestWindow : BaseWindow + { + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + return DragMode.None; + } + } } } diff --git a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs index 9154374bb50..6d9d24cee87 100644 --- a/Robust.Client/UserInterface/CustomControls/BaseWindow.cs +++ b/Robust.Client/UserInterface/CustomControls/BaseWindow.cs @@ -19,6 +19,8 @@ public abstract class BaseWindow : Control private DragMode CurrentDrag = DragMode.None; private Vector2 DragOffsetTopLeft; private Vector2 DragOffsetBottomRight; + private Control? _trackedParent; + private Vector2? _lastParentSize; public bool Resizable { get; set; } = true; public bool IsOpen => Parent != null; @@ -41,6 +43,18 @@ public virtual void Close() OnClose?.Invoke(); } + protected override void EnteredTree() + { + base.EnteredTree(); + TrackParentResize(); + } + + protected override void ExitedTree() + { + UntrackParentResize(); + base.ExitedTree(); + } + protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) { base.KeyBindDown(args); @@ -291,6 +305,55 @@ protected virtual DragMode GetDragModeFor(Vector2 relativeMousePos) return DragMode.None; } + private void TrackParentResize() + { + if (Parent == null) + return; + + _trackedParent = Parent; + _trackedParent.OnResized += ParentResized; + _lastParentSize = _trackedParent.Size; + } + + private void UntrackParentResize() + { + if (_trackedParent != null) + _trackedParent.OnResized -= ParentResized; + + _trackedParent = null; + _lastParentSize = null; + } + + /// + /// Re-locates this window if the parent re-sizes. Useful if display size changes. + /// + private void ParentResized() + { + // Sanity check + if (_trackedParent == null || Parent != _trackedParent) + return; + + var newParentSize = _trackedParent.Size; + + if (_lastParentSize is not { } oldParentSize || + oldParentSize.X <= 0 || oldParentSize.Y <= 0 || + newParentSize.X <= 0 || newParentSize.Y <= 0) + { + _lastParentSize = newParentSize; + return; + } + + if (oldParentSize.EqualsApprox(newParentSize)) + return; + + var relativeCenter = (Position + Size / 2) / oldParentSize; + var newPosition = relativeCenter * newParentSize - Size / 2; + var maxPosition = Vector2.Max(newParentSize - Size, Vector2.Zero); + + _lastParentSize = newParentSize; + LayoutContainer.SetPosition(this, Vector2.Clamp(newPosition, Vector2.Zero, maxPosition)); + } + [Flags] protected enum DragMode : byte { From 72ae628a5c98960db150764e480df57298e40c70 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Sun, 7 Jun 2026 00:00:18 +1000 Subject: [PATCH 002/178] Optimise sprite sorts Quick and dirty but shaved off 10% on debug, would be less on release. --- Robust.Client/Graphics/Clyde/Clyde.Sprite.cs | 58 +++++++++++++------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs index c30b639b8be..5489624667b 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -29,16 +29,31 @@ private void GetSprites(MapId map, Viewport view, IEye eye, Box2Rotated worldBou { ProcessSpriteEntities(map, view, eye, worldBounds, _drawingSpriteList); - // We use a separate list for indexing sprites so that the sort is faster. - indexList = ArrayPool.Shared.Rent(_drawingSpriteList.Count); + var count = _drawingSpriteList.Count; - // populate index list - for (var i = 0; i < _drawingSpriteList.Count; i++) - indexList[i] = i; + indexList = ArrayPool.Shared.Rent(count); + var sortItems = ArrayPool.Shared.Rent(count); + + for (var i = 0; i < count; i++) + { + ref var data = ref _drawingSpriteList[i]; + sortItems[i] = new SpriteSortItem( + i, + data.Sprite.DrawDepth, + data.Sprite.RenderOrder, + data.SpriteScreenBB.Top, + data.Uid); + } - // sort index list // TODO better sorting? parallel merge sort? - Array.Sort(indexList, 0, _drawingSpriteList.Count, new SpriteDrawingOrderComparer(_drawingSpriteList)); + Array.Sort(sortItems, 0, count); + + for (var i = 0; i < count; i++) + { + indexList[i] = sortItems[i].Index; + } + + ArrayPool.Shared.Return(sortItems); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -216,36 +231,41 @@ private readonly struct BatchData public float Cos { get; init; } } - private sealed class SpriteDrawingOrderComparer : IComparer + private readonly struct SpriteSortItem : IComparable { - private readonly RefList _drawList; + public readonly int Index; + private readonly int _drawDepth; + private readonly uint _renderOrder; + private readonly float _ySort; + private readonly EntityUid _uid; - public SpriteDrawingOrderComparer(RefList drawList) + public SpriteSortItem(int index, int drawDepth, uint renderOrder, float ySort, EntityUid uid) { - _drawList = drawList; + Index = index; + _drawDepth = drawDepth; + _renderOrder = renderOrder; + _ySort = ySort; + _uid = uid; } - public int Compare(int x, int y) + public int CompareTo(SpriteSortItem other) { - var a = _drawList[x]; - var b = _drawList[y]; - - var cmp = a.Sprite.DrawDepth.CompareTo(b.Sprite.DrawDepth); + var cmp = _drawDepth.CompareTo(other._drawDepth); if (cmp != 0) return cmp; - cmp = a.Sprite.RenderOrder.CompareTo(b.Sprite.RenderOrder); + cmp = _renderOrder.CompareTo(other._renderOrder); if (cmp != 0) return cmp; // compare the top of the sprite's BB for y-sorting. Because screen coordinates are flipped, the "top" of the BB is actually the "bottom". - cmp = a.SpriteScreenBB.Top.CompareTo(b.SpriteScreenBB.Top); + cmp = _ySort.CompareTo(other._ySort); if (cmp != 0) return cmp; - return a.Uid.CompareTo(b.Uid); + return _uid.CompareTo(other._uid); } } } From 338825c8e734f8e79096c5d6079faea0e8dbf42d Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:54:31 +1000 Subject: [PATCH 003/178] Add Pure attr to EntityLookup bounds methods (#6622) --- RELEASE-NOTES.md | 2 +- Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e60a7b1ed48..94dd7e357c2 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,7 +47,7 @@ END TEMPLATE--> ### Other -*None yet* +* Add Pure attributes to the EntityLookup bounds methods ### Internal diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs index 7d7d2ead228..9eee6955726 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Collections; @@ -803,18 +804,21 @@ public void FindLookupsIntersecting(MapId mapId, Box2Rotated worldBounds, Compon #region Bounds + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Box2 GetLocalBounds(Vector2i gridIndices, ushort tileSize) { return new Box2(gridIndices * tileSize, (gridIndices + 1) * tileSize); } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public Box2 GetLocalBounds(TileRef tileRef, ushort tileSize) { return GetLocalBounds(tileRef.GridIndices, tileSize); } + [Pure] public Box2Rotated GetWorldBounds(TileRef tileRef, Matrix3x2? worldMatrix = null, Angle? angle = null) { var grid = _gridQuery.GetComponent(tileRef.GridUid); From 9d677d22775fda9db1b27d003b16933326f86dc6 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Thu, 11 Jun 2026 18:55:43 +1000 Subject: [PATCH 004/178] Reduce per-frame allocs for openGL logs Skip allocating entirely if we log-level not enabled. This is no.2 on debug allocations on client. --- Robust.Client/Graphics/Clyde/Clyde.cs | 76 ++++++++++----------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index 03f929c536d..fc03dd5ed80 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -476,60 +476,38 @@ private unsafe void SetupDebugCallback() private void DebugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length, IntPtr message, IntPtr userParam) { - var contents = $"{source}: " + Marshal.PtrToStringAnsi(message, length); - - var category = "ogl.debug"; - switch (type) + var category = type switch { - case DebugType.DebugTypePerformance: - category += ".performance"; - break; - case DebugType.DebugTypeOther: - category += ".other"; - break; - case DebugType.DebugTypeError: - category += ".error"; - break; - case DebugType.DebugTypeDeprecatedBehavior: - category += ".deprecated"; - break; - case DebugType.DebugTypeUndefinedBehavior: - category += ".ub"; - break; - case DebugType.DebugTypePortability: - category += ".portability"; - break; - case DebugType.DebugTypeMarker: - case DebugType.DebugTypePushGroup: - case DebugType.DebugTypePopGroup: + DebugType.DebugTypePerformance => "ogl.debug.performance", + DebugType.DebugTypeOther => "ogl.debug.other", + DebugType.DebugTypeError => "ogl.debug.error", + DebugType.DebugTypeDeprecatedBehavior => "ogl.debug.deprecated", + DebugType.DebugTypeUndefinedBehavior => "ogl.debug.ub", + DebugType.DebugTypePortability => "ogl.debug.portability", + DebugType.DebugTypeMarker or DebugType.DebugTypePushGroup or DebugType.DebugTypePopGroup => // These are inserted by our own code so I imagine they're not necessary to log? - return; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + null, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; - var sawmill = _logManager.GetSawmill(category); + if (category == null) + return; - switch (severity) + var sawmill = _logManager.GetSawmill(category); + var level = severity switch { - case DebugSeverity.DontCare: - sawmill.Info(contents); - break; - case DebugSeverity.DebugSeverityNotification: - sawmill.Info(contents); - break; - case DebugSeverity.DebugSeverityHigh: - sawmill.Error(contents); - break; - case DebugSeverity.DebugSeverityMedium: - sawmill.Error(contents); - break; - case DebugSeverity.DebugSeverityLow: - sawmill.Warning(contents); - break; - default: - throw new ArgumentOutOfRangeException(nameof(severity), severity, null); - } + DebugSeverity.DontCare => LogLevel.Info, + DebugSeverity.DebugSeverityNotification => LogLevel.Info, + DebugSeverity.DebugSeverityHigh => LogLevel.Error, + DebugSeverity.DebugSeverityMedium => LogLevel.Error, + DebugSeverity.DebugSeverityLow => LogLevel.Warning, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; + + if (!sawmill.IsLogLevelEnabled(level)) + return; + + sawmill.Log(level, "{0}: {1}", source, Marshal.PtrToStringAnsi(message, length)); } private static DebugProc? _debugMessageCallbackInstance; From 8e98d5479ff9d4f1f62b9efce006f6f4fe4140d9 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Thu, 11 Jun 2026 19:49:58 +1000 Subject: [PATCH 005/178] Also this one --- Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs b/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs index 1f474590539..ee3addaa154 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GLFeatures.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using OpenToolkit.Graphics.OpenGL4; namespace Robust.Client.Graphics.Clyde @@ -201,17 +202,17 @@ private HashSet GetGLExtensions() if (!_isGLES) { var extensions = new HashSet(); - var extensionsText = ""; + var extensionsText = new StringBuilder(); // Desktop OpenGL uses this API to discourage static buffers var count = GL.GetInteger(GetPName.NumExtensions); for (var i = 0; i < count; i++) { if (i != 0) { - extensionsText += " "; + extensionsText.Append(' '); } var extension = GL.GetString(StringNameIndexed.Extensions, i); - extensionsText += extension; + extensionsText.Append(extension); extensions.Add(extension); } _sawmillOgl.Debug("OpenGL Extensions: {0}", extensionsText); From e0496a3ca54eeaca65d71022c7d665e75cc6e089 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Thu, 11 Jun 2026 20:24:21 +1000 Subject: [PATCH 006/178] Reduce TryParseEnum string allocs Added length --- Robust.Shared/Reflection/ReflectionManager.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Robust.Shared/Reflection/ReflectionManager.cs b/Robust.Shared/Reflection/ReflectionManager.cs index 5e947ec7904..b1ce6ec669e 100644 --- a/Robust.Shared/Reflection/ReflectionManager.cs +++ b/Robust.Shared/Reflection/ReflectionManager.cs @@ -293,10 +293,7 @@ public bool TryParseEnumReference(string reference, [NotNullWhen(true)] out Enum { foreach (var type in assembly.DefinedTypes) { - if (!type.IsEnum || !( - type.FullName!.Equals(typeName) || - type.FullName!.EndsWith("." + typeName) || - type.FullName!.EndsWith("+" + typeName))) + if (!type.IsEnum || !TypeNameMatchesEnumReference(type.FullName!, typeName)) { continue; } @@ -316,6 +313,21 @@ public bool TryParseEnumReference(string reference, [NotNullWhen(true)] out Enum return false; } + private static bool TypeNameMatchesEnumReference(string fullName, string typeName) + { + if (fullName.Equals(typeName)) + return true; + + if (fullName.Length <= typeName.Length) + return false; + + var prefixIndex = fullName.Length - typeName.Length - 1; + var separator = fullName[prefixIndex]; + + return (separator == '.' || separator == '+') + && fullName.AsSpan(prefixIndex + 1).SequenceEqual(typeName); + } + public Type? YamlTypeTagLookup(Type baseType, string typeName) { using (_yamlTypeTagCacheLock.ReadGuard()) From ee6e7994e1c72d0a4f1127d0e382d3cd38530329 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Sat, 13 Jun 2026 00:24:32 +1000 Subject: [PATCH 007/178] ResPath allocs fix --- Robust.Shared/Utility/ResPath.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Utility/ResPath.cs b/Robust.Shared/Utility/ResPath.cs index d5a8822eaa5..1dc2126f07d 100644 --- a/Robust.Shared/Utility/ResPath.cs +++ b/Robust.Shared/Utility/ResPath.cs @@ -439,9 +439,20 @@ public bool TryRelativeTo(ResPath basePath, [NotNullWhen(true)] out ResPath? rel if (CanonPath.StartsWith(basePath.CanonPath)) { - var x = CanonPath[basePath.CanonPath.Length..] - .Trim('/'); - relative = x == "" ? Self : new ResPath(x); + var start = basePath.CanonPath.Length; + var end = CanonPath.Length; + + while (start < end && CanonPath[start] == '/') + { + start++; + } + + while (end > start && CanonPath[end - 1] == '/') + { + end--; + } + + relative = start == end ? Self : new ResPath(CanonPath[start..end]); return true; } From ff67da37bb592d18ab99d612ffd6b25be558a460 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:55:17 +1000 Subject: [PATCH 008/178] Cleanup Robust integration test boilerplate (#8) --- RELEASE-NOTES.md | 2 +- .../GameStates/DefaultEntityTest.cs | 43 ++-------- .../GameStates/DetachedParentTest.cs | 74 +++-------------- .../GameStates/MissingParentTest.cs | 49 ++--------- .../GameStates/PvsChunkTest.cs | 37 ++------- .../GameStates/PvsPauseTest.cs | 18 +--- .../GameStates/PvsReEntryTest.cs | 61 +++----------- .../GameStates/PvsResetTest.cs | 34 ++------ .../GameStates/PvsSystemTests.cs | 43 ++-------- Robust.UnitTesting/RobustIntegrationTest.cs | 83 +++++++++++++++++++ 10 files changed, 145 insertions(+), 299 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 94dd7e357c2..4e63fc26c20 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -51,7 +51,7 @@ END TEMPLATE--> ### Internal -*None yet* +* Added several test helpers to avoid boilerplate in integration tests around client connection / disconnection. ## 277.0.0 diff --git a/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs b/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs index 60f2a63eee5..92e454506f6 100644 --- a/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/DefaultEntityTest.cs @@ -6,7 +6,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -19,35 +18,22 @@ public sealed class DefaultEntityTest : RobustIntegrationTest [Test] public async Task TestSpawnDefaultEntity() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var playerMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); - client.SetConnectTarget(server); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, false)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var session = playerMan.Sessions.First(); await server.WaitPost(() => playerMan.JoinGame(session)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Spawn a default unmodified entity. NetEntity ent = default; @@ -56,11 +42,7 @@ await server.WaitPost(() => ent = sEntMan.GetNetEntity(sEntMan.Spawn()); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that server & client both think the entity exists. Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(ent))); @@ -81,11 +63,7 @@ await server.WaitPost(() => server.PlayerMan.SetAttachedEntity(session, playerUid); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(player))); Assert.That(cEntMan.EntityExists(cEntMan.GetEntity(player))); @@ -95,18 +73,11 @@ await server.WaitPost(() => ent = sEntMan.GetNetEntity(sEntMan.SpawnAtPosition(null, coords)); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(sEntMan.EntityExists(sEntMan.GetEntity(ent))); Assert.That(cEntMan.EntityExists(cEntMan.GetEntity(ent))); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs index 4ea2a2dd9e8..2570fee2d5e 100644 --- a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -22,10 +21,8 @@ public sealed class DetachedParentTest : RobustIntegrationTest [Test] public async Task TestDetachedParent() { - var server = StartServer(new() {Pool = false}); - var client = StartClient(new() {Pool = false}); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(new() {Pool = false}, new() {Pool = false}); + var (client, server) = pair; var mapSys = server.System(); var xformSys = server.System(); @@ -33,17 +30,9 @@ public async Task TestDetachedParent() var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); - var netMan = client.ResolveDependency(); - - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -99,11 +88,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that transforms are as expected. var childX = server.Transform(child); @@ -160,11 +145,7 @@ await server.WaitPost(() => // Move the player into pvs range of the child, which will move them outside of the grid & parent's PVS range. await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // the client now knows about the child. cChild = client.EntMan.GetEntity(server.EntMan.GetNetEntity(child)); @@ -192,11 +173,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parentCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parentCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check that server-side transforms are as expected Assert.That(childX.ParentUid, Is.EqualTo(parent)); @@ -227,11 +204,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, mapCoords)); await server.WaitPost(() => xformSys.SetCoordinates(child, mapCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Child transform has updated. Assert.That(childX.MapID, Is.EqualTo(mapId)); @@ -253,11 +226,7 @@ await server.WaitPost(() => EntityUid parent2 = default; await server.WaitPost(() => parent2 = sEntMan.SpawnEntity(null, gridCoords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var parent2X = server.Transform(parent2); Assert.That(parent2X.MapID, Is.EqualTo(mapId)); @@ -273,11 +242,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parent2Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent2Coords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms cParent2 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent2)); @@ -310,11 +275,7 @@ await server.WaitPost(() => parent3 = sEntMan.SpawnEntity(null, grid2Coords); }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check server-side transforms var grid2X = server.Transform(grid2); @@ -342,11 +303,7 @@ await server.WaitPost(() => await server.WaitPost(() => xformSys.SetCoordinates(player, parent3Coords)); await server.WaitPost(() => xformSys.SetCoordinates(child, parent3Coords)); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms cParent3 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent3)); @@ -400,11 +357,7 @@ await server.WaitPost(() => }); - for (var i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check all the transforms var cParent4 = client.EntMan.GetEntity(server.EntMan.GetNetEntity(parent4)); @@ -442,9 +395,6 @@ await server.WaitPost(() => Assert.That(cParent4X.MapUid, Is.EqualTo(cMap3)); Assert.That(cGrid3X.MapUid, Is.EqualTo(cMap3)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs index 470ee32d37e..069001bcae9 100644 --- a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs @@ -5,7 +5,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -18,10 +17,8 @@ public sealed class MissingParentTest : RobustIntegrationTest [Test] public async Task TestMissingParent() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); @@ -29,28 +26,17 @@ public async Task TestMissingParent() var sPlayerMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); var cConfMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Limit client to receiving at most 1 entity per tick. cConfMan.SetCVar(CVars.NetPVSEntityBudget, 1); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -92,11 +78,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid)); Assert.That(entity, Is.Not.EqualTo(NetEntity.Invalid)); @@ -124,11 +106,7 @@ await server.WaitPost(() => }); // Wait for the client to receive some, but not all, of the entities - for (int i = 0; i < 8; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 8); Assert.That(cEntMan.TryGetEntity(first, out _), Is.True); Assert.That(cEntMan.TryGetEntity(last, out _), Is.False); @@ -140,11 +118,7 @@ await server.WaitPost(() => }); // Wait a few more ticks - for (int i = 0; i < 8; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 8); // Client should still not have received the new parent, however this shouldn't cause any issues. // The already known entity should just have been moved to nullspace. @@ -153,11 +127,7 @@ await server.WaitPost(() => Assert.That(client.MetaData(entity).Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); // Wait untill the client receives the parent entity - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // now that the parent was received the entity should no longer be in nullspace. Assert.That(cEntMan.TryGetEntity(last, out var newParent), Is.True); @@ -165,9 +135,6 @@ await server.WaitPost(() => Assert.That(client.Transform(entity).ParentUid, Is.EqualTo(newParent)); Assert.That(client.MetaData(entity).Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs index 6e7cb201f78..3a94114343f 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -17,10 +16,8 @@ public sealed class PvsChunkTest : RobustIntegrationTest [Test] public async Task TestGridMapChange() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); @@ -30,17 +27,10 @@ public async Task TestGridMapChange() var mapSys = sEntMan.System(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -87,11 +77,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); var nEntity = sEntMan.GetNetEntity(entity); var nGrid = sEntMan.GetNetEntity(grid); @@ -110,11 +96,7 @@ await server.WaitPost(() => // Teleport grid to new map await server.WaitPost(() => xforms.SetCoordinates(grid, mapCoords)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(xform.ParentUid, Is.EqualTo(grid)); Assert.That(xform.GridUid, Is.EqualTo(grid)); @@ -127,11 +109,7 @@ await server.WaitPost(() => // Delete the original map. await server.WaitPost(() => sEntMan.DeleteEntity(map2)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(xform.ParentUid, Is.EqualTo(grid)); Assert.That(xform.GridUid, Is.EqualTo(grid)); @@ -142,9 +120,6 @@ await server.WaitPost(() => Assert.That(!cEntMan.TryGetEntity(nMap2, out _)); Assert.That(cEntMan.TryGetEntity(nGrid, out _)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs index 12e4d6759ad..3ba8f3a214a 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsPauseTest.cs @@ -7,7 +7,6 @@ using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -19,10 +18,8 @@ public sealed class PvsPauseTest : RobustIntegrationTest [Test] public async Task PauseTest() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.EntMan; var confMan = server.CfgMan; @@ -33,19 +30,12 @@ public async Task PauseTest() var cEntMan = client.EntMan; var cPlayerMan = client.PlayerMan; - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); async Task RunTicks() { - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); } await RunTicks(); @@ -166,8 +156,6 @@ void AssertEnt(bool paused, bool detached, bool clientPaused) AssertEnt(paused: true, detached: false, clientPaused: true); } - client.Post(() => netMan.ClientDisconnect("")); - await RunTicks(); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs index 9ab6e1fa025..d92aa95fbcd 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs @@ -8,7 +8,6 @@ using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -23,10 +22,8 @@ public sealed class PvsReEntryTest : RobustIntegrationTest [Test] public async Task TestLossyReEntry() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); @@ -36,18 +33,11 @@ public async Task TestLossyReEntry() var stateMan = (ClientGameStateManager) client.ResolveDependency(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Ensure client & server ticks are synced. // Client runs 1 tick ahead @@ -88,11 +78,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); Assert.That(player, Is.Not.EqualTo(NetEntity.Invalid)); Assert.That(entity, Is.Not.EqualTo(NetEntity.Invalid)); @@ -115,11 +101,7 @@ await client.WaitPost(() => // note that we move the PLAYER not the entity, as we don't want to dirty the entity. var farAway = new EntityCoordinates(map, new Vector2(100, 100)); await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Client should have detached the entity to null space. Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.Detached)); @@ -127,11 +109,7 @@ await client.WaitPost(() => // Move the player back into range await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Entity is back in pvs range Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); @@ -142,11 +120,7 @@ await client.WaitPost(() => var timing = client.ResolveDependency(); var lastRealTick = timing.LastRealTick; - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Even though the client is receiving no new states, it will still have applied some from the state buffer. Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick)); @@ -156,11 +130,7 @@ await client.WaitPost(() => Assert.That(meta!.Flags & MetaDataFlags.Detached, Is.EqualTo(MetaDataFlags.None)); await server.WaitPost(() => xforms.SetCoordinates(sEntMan.GetEntity(player), farAway)); - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Client should have exhausted the buffer -- client has not been applying any states. Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick)); @@ -173,11 +143,7 @@ await client.WaitPost(() => // Move the entity back into range await server.WaitPost( () => xforms.SetCoordinates(sEntMan.GetEntity(player), coords)); - for (int i = 0; i < 5; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 5); // Still hasn't been applying states. Assert.That(timing.LastRealTick, Is.EqualTo(lastRealTick)); @@ -189,11 +155,7 @@ await client.WaitPost(() => // Client clears the tunnel, starts receiving states again. stateMan.DropStates = false; - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Entity should be in PVS range, client should know about it: Assert.That(timing.LastRealTick, Is.GreaterThan(lastRealTick)); @@ -204,9 +166,6 @@ await client.WaitPost(() => // If the test moves the entity instead of the player, then the test doesn't actually work. Assert.That(meta.LastModifiedTick, Is.EqualTo(lastDirty)); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } #endif } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs index 868994e6e46..f7e458632bc 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsResetTest.cs @@ -5,7 +5,6 @@ using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Map; -using Robust.Shared.Network; namespace Robust.UnitTesting.Server.GameStates; @@ -17,10 +16,8 @@ public sealed class PvsResetTest : RobustIntegrationTest [Test] public async Task ResetTest() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var sEntMan = server.EntMan; var confMan = server.CfgMan; @@ -29,22 +26,10 @@ public async Task ResetTest() var cEntMan = client.EntMan; var cPlayerMan = client.PlayerMan; - var netMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - async Task RunTicks() - { - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } - } - - await RunTicks(); + await RunTicksSync(server, client, 10); // Set up map and spawn player EntityUid sMap = default; @@ -64,7 +49,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - await RunTicks(); + await RunTicksSync(server, client, 10); var farAway = new EntityCoordinates(sMap, new Vector2(100, 100)); var netEnt = sEntMan.GetNetEntity(sEnt); var player = sEntMan.GetNetEntity(playerUid); @@ -106,29 +91,26 @@ void AssertDetached(bool detached) // Move the player out of the entity's PVS range await server.WaitPost(() => xforms.SetCoordinates(playerUid, farAway)); - await RunTicks(); + await RunTicksSync(server, client, 10); // Client should now have detached the entity, moving it into nullspace AssertDetached(true); // Marking the entity as dirty due to client-side prediction should have effect await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt))); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(true); // Move the player back into range await server.WaitPost( () => xforms.SetCoordinates(playerUid, coords)); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(false); // Marking the entity as dirty due to client-side prediction should have no real effect await client.WaitPost(() => client.EntMan.Dirty(cEnt, client.Transform(cEnt))); - await RunTicks(); + await RunTicksSync(server, client, 10); AssertDetached(false); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs index 6e4dc30273e..a6fcb607f46 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs @@ -7,7 +7,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Network; using Robust.Shared.Player; namespace Robust.UnitTesting.Server.GameStates; @@ -20,10 +19,8 @@ public sealed class PvsSystemTests : RobustIntegrationTest [Test] public async Task TestMultipleIndexChange() { - var server = StartServer(); - var client = StartClient(); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + await using var pair = await StartConnectedPair(); + var (client, server) = pair; var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); @@ -33,18 +30,11 @@ public async Task TestMultipleIndexChange() var maps = sEntMan.System(); var cEntMan = client.ResolveDependency(); - var netMan = client.ResolveDependency(); var cPlayerMan = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Set up map and grid EntityUid grid = default; @@ -78,11 +68,7 @@ await server.WaitPost(() => sPlayerMan.JoinGame(session); }); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Check player got properly attached await client.WaitPost(() => @@ -96,35 +82,20 @@ await client.WaitPost(() => xforms.SetCoordinates(other, otherXform, gridCoords); // Run for a few ticks. The test just checks that no PVS asserts/errors happen. - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Repeat but in the opposite direction ( map -> grid -> map ) // first move to map and wait a bit. xforms.SetCoordinates(other, otherXform, mapCoords); - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); // Move to and off grid in the same tick xforms.SetCoordinates(other, otherXform, gridCoords); xforms.SetCoordinates(other, otherXform, mapCoords); // wait for errors. - for (int i = 0; i < 10; i++) - { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } + await RunTicksSync(server, client, 10); - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); } } diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 48be5149599..78aa6842f55 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -182,6 +182,89 @@ protected virtual ClientIntegrationInstance StartClient(ClientIntegrationOptions return instance; } + /// + /// Connects a client integration instance to a server integration instance. + /// + protected static async Task ConnectClient( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + string? userName = null) + { + await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); + Assert.DoesNotThrow(() => client.SetConnectTarget(server)); + await client.WaitPost(() => ((IClientNetManager) client.NetMan).ClientConnect(null!, 0, userName!)); + } + + /// + /// Starts a connected client/server pair. + /// + protected async Task StartConnectedPair( + ServerIntegrationOptions? serverOptions = null, + ClientIntegrationOptions? clientOptions = null, + string? userName = null) + { + var server = StartServer(serverOptions); + var client = StartClient(clientOptions); + await ConnectClient(server, client, userName); + return new ConnectedIntegrationPair(server, client); + } + + /// + /// Runs the server and client in lockstep. + /// + protected static async Task RunTicksSync( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + int ticks) + { + for (var i = 0; i < ticks; i++) + { + await server.WaitRunTicks(1); + await client.WaitRunTicks(1); + } + } + + /// + /// Disconnects a client integration instance from its server and runs both sides long enough to process it. + /// + protected static async Task DisconnectClient( + ServerIntegrationInstance server, + ClientIntegrationInstance client, + string reason = "") + { + await client.WaitPost(() => ((IClientNetManager) client.NetMan).ClientDisconnect(reason)); + await RunTicksSync(server, client, 5); + } + + protected sealed class ConnectedIntegrationPair : IAsyncDisposable + { + public ServerIntegrationInstance Server { get; } + public ClientIntegrationInstance Client { get; } + + private bool _disposed; + + public ConnectedIntegrationPair(ServerIntegrationInstance server, ClientIntegrationInstance client) + { + Server = server; + Client = client; + } + + public void Deconstruct(out ClientIntegrationInstance client, out ServerIntegrationInstance server) + { + client = Client; + server = Server; + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _disposed = true; + await DisconnectClient(Server, Client); + } + } + private bool ShouldPool(IntegrationOptions? options) { // If no options are provided, we assume we should pool From ae38b3a5669e7a56842e9f3037c1f6cc3c863417 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Mon, 15 Jun 2026 21:04:24 -0700 Subject: [PATCH 009/178] Update Lidgren submodule to fix DOS attack, fix by Darkrell from Starlight --- .gitmodules | 2 +- Lidgren.Network/Lidgren.Network | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 30c23aed2df..249406255fb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/space-wizards/netserializer [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network - url = https://github.com/space-wizards/lidgren-network-gen3.git + url = https://github.com/Space-Wizards-Federation/SpaceWizards.Lidgren.Network [submodule "XamlX"] path = XamlX url = https://github.com/space-wizards/XamlX diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index 1d85b82e058..805b6e1ce51 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit 1d85b82e058101b7ebd60cc8883af5359e4c263a +Subproject commit 805b6e1ce516aba3a73b479ad282466b46b3d9fa From bae997f8b95b9269ec13b3c2936ede6f5a18bd04 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Tue, 16 Jun 2026 08:23:18 -0400 Subject: [PATCH 010/178] Build and test against our own content repo (#2) --- .github/workflows/test-content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index f8014cc945a..67f3b38e2e4 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -14,7 +14,7 @@ jobs: - name: Check out content uses: actions/checkout@v4.2.2 with: - repository: space-wizards/space-station-14 + repository: Space-Wizards-Federation/space-station-14 submodules: recursive - name: Setup .NET Core From b9e96ce8e8f8f36ff062deb491a66920b6e5cd48 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Tue, 16 Jun 2026 12:50:59 -0400 Subject: [PATCH 011/178] Fix wrong submodules being used in "Test content master against engine" workflow (#18) --- .github/workflows/test-content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index 67f3b38e2e4..b42fdc0947c 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v4.2.2 with: repository: Space-Wizards-Federation/space-station-14 - submodules: recursive + submodules: true - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 From 3e72678d1d145b34697a486f595c30b84b6f77a8 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Tue, 16 Jun 2026 17:35:29 -0400 Subject: [PATCH 012/178] Release notes --- RELEASE-NOTES.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4e63fc26c20..af81e110c98 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,19 +39,30 @@ END TEMPLATE--> ### New features -*None yet* +* Added `IsHardCollidable` to `SharedPhysicsSystem`. +* Added `GetFilledTileCount` to `SharedMapSystem`. +* Changed the cursors on interactive controls. +* Added new `StyleProperty` `track` to `ScrollBar` that takes a `StyleBox` and displays it as a backing track for the whole height of the `ScrollBar`. +* Scroll Lock is now a bindable key. ### Bugfixes -*None yet* +* Fixed override properties in `WrapContainer` not actually overriding the Style Properties. +* Fixed `BoxContainer`'s `SeparationOverride` not overriding the Style Properties. +* Fixed `SeparationOverride` not invalidating measure. +* Fixed swapped parameters in `MapManager`'s `FindGridsIntersecting` methods. ### Other -* Add Pure attributes to the EntityLookup bounds methods +* Added Pure attributes to the `EntityLookup` bounds methods. +* Improved performance of collision filter test. +* Removed an outdated xmldoc comment regarding dependency injection. +* Audio resources now use `AsSpan` when checking signatures. ### Internal * Added several test helpers to avoid boilerplate in integration tests around client connection / disconnection. +* Added `.lscache` files to `.gitignore`. ## 277.0.0 From fe368d63fa45b01bc035b79a0618c96f6ec48ab4 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Tue, 16 Jun 2026 17:39:35 -0400 Subject: [PATCH 013/178] Version: 277.1.0 --- MSBuild/Robust.Engine.Version.props | 8 ++++---- RELEASE-NOTES.md | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index bac96a1d378..fcfd5b39855 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - - - 277.0.0 - + + + 277.1.0 + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index af81e110c98..8a0624cdf16 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,25 @@ END TEMPLATE--> ### New features +*None yet* + +### Bugfixes + +*None yet* + +### Other + +*None yet* + +### Internal + +*None yet* + + +## 277.1.0 + +### New features + * Added `IsHardCollidable` to `SharedPhysicsSystem`. * Added `GetFilledTileCount` to `SharedMapSystem`. * Changed the cursors on interactive controls. From e42f9f692d39f6796da56c23416b3c5ebf492394 Mon Sep 17 00:00:00 2001 From: Connor Huffine Date: Tue, 16 Jun 2026 23:06:30 -0400 Subject: [PATCH 014/178] Remove broken GHAs --- .github/workflows/benchmarks.yml | 41 ---------------- .github/workflows/build-docfx.yml | 34 ------------- .github/workflows/codeql-analysis.yml | 71 --------------------------- RobustToolbox.slnx | 3 -- 4 files changed, 149 deletions(-) delete mode 100644 .github/workflows/benchmarks.yml delete mode 100644 .github/workflows/build-docfx.yml delete mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml deleted file mode 100644 index 827cc9c64eb..00000000000 --- a/.github/workflows/benchmarks.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Benchmarks -on: - workflow_dispatch: - schedule: - - cron: '0 5 * * *' - push: - tags: - - 'v*' - -concurrency: benchmarks - -jobs: - benchmark: - name: Run Benchmarks - runs-on: ubuntu-latest - steps: - - name: Run script on centcomm - uses: appleboy/ssh-action@master - with: - host: centcomm.spacestation14.io - username: robust-benchmark-runner - key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }} - command_timeout: 100000m - script: | - mkdir benchmark_run_${{ github.sha }} - cd benchmark_run_${{ github.sha }} - git clone https://github.com/space-wizards/RobustToolbox.git repo_dir --recursive - cd repo_dir - git checkout ${{ github.sha }} - cd Robust.Benchmarks - dotnet restore - export ROBUST_BENCHMARKS_ENABLE_SQL=1 - export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}" - export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}" - export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}" - export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD }}" - export ROBUST_BENCHMARKS_SQL_DATABASE="benchmarks" - export GITHUB_SHA="${{ github.sha }}" - dotnet run --filter '*' --configuration Release - cd ../../.. - rm -rf benchmark_run_${{ github.sha }} diff --git a/.github/workflows/build-docfx.yml b/.github/workflows/build-docfx.yml deleted file mode 100644 index 46f443ac23b..00000000000 --- a/.github/workflows/build-docfx.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Build & Publish DocFX - -on: - schedule: - - cron: "0 0 * * 0" -jobs: - docfx: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.2.2 - with: - submodules: true - - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 10.0.x - - - name: Install dependencies - run: dotnet restore - - - name: Build Project - run: dotnet build --no-restore /p:WarningsAsErrors=nullable - - - name: Build DocFX - uses: nikeee/docfx-action@v1.0.0 - with: - args: Robust.Docfx/docfx.json - - - name: Publish Docfx Documentation on GitHub Pages - uses: maxheld83/ghpages@master - env: - BUILD_DIR: Robust.Docfx/_robust-site - GH_PAT: ${{ secrets.GH_PAT }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index ae600a3be6d..00000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,71 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - workflow_dispatch - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: ["csharp"] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v4.2.2 - with: - submodules: true - - - name: Setup .NET Core - uses: actions/setup-dotnet@v4.1.0 - with: - dotnet-version: 7.0.x - - - name: Build - run: dotnet build - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/RobustToolbox.slnx b/RobustToolbox.slnx index 70f64dbc92e..49ea4914b00 100644 --- a/RobustToolbox.slnx +++ b/RobustToolbox.slnx @@ -7,11 +7,8 @@ - - - From d09a9b8a2686f16830dfa1f45c91bb5e5cd446e2 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Tue, 16 Jun 2026 21:13:07 -0700 Subject: [PATCH 015/178] Fix .gitmodules urls to use the new repositories --- .gitmodules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index 249406255fb..d83f9c8b743 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,15 @@ [submodule "NetSerializer"] path = NetSerializer - url = https://github.com/space-wizards/netserializer + url = https://github.com/Space-Wizards-Federation/netserializer [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network url = https://github.com/Space-Wizards-Federation/SpaceWizards.Lidgren.Network [submodule "XamlX"] path = XamlX - url = https://github.com/space-wizards/XamlX + url = https://github.com/Space-Wizards-Federation/XamlX [submodule "Robust.LoaderApi"] path = Robust.LoaderApi - url = https://github.com/space-wizards/Robust.LoaderApi.git + url = https://github.com/Space-Wizards-Federation/Robust.LoaderApi.git [submodule "cefglue"] path = cefglue - url = https://github.com/space-wizards/cefglue.git + url = https://github.com/Space-Wizards-Federation/cefglue.git From 2d8d5c3038c0f7362456081c1ad67357f536f983 Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sat, 24 Jan 2026 14:28:31 +0000 Subject: [PATCH 016/178] fix not giving prototype class that wasnt registered --- Robust.Shared/Prototypes/PrototypeManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 527ae7ea2cf..e809192bc02 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -949,7 +949,7 @@ public FrozenDictionary GetInstances() where T : IPrototype if (TryGetInstances(out var dict)) return dict; - throw new Exception($"Failed to fetch instances for kind {nameof(T)}"); + throw new Exception($"Failed to fetch instances for kind {typeof(T).Name}"); } public bool TryGetInstances([NotNullWhen(true)] out FrozenDictionary? instances) From c9d5f31fbf1c59e33bade3540a4842e4d3d9d1ce Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sat, 24 Jan 2026 14:29:03 +0000 Subject: [PATCH 017/178] add Proto field to EntitySystem for proxy and all systems to use --- Robust.Shared/GameObjects/EntitySystem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index e4f7e94480c..bb256bd4f70 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -9,6 +9,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Replays; @@ -24,6 +25,7 @@ namespace Robust.Shared.GameObjects public abstract partial class EntitySystem : IEntitySystem, IPostInjectInit { [Dependency] protected EntityManager EntityManager = default!; + [Dependency] protected IPrototypeManager Proto = default!; [Dependency] protected ILogManager LogManager = default!; [Dependency] private ISharedPlayerManager _playerMan = default!; [Dependency] private IReplayRecordingManager _replayMan = default!; From 1be04a58820724f4a750af29387af7bfca46d10a Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sat, 24 Jan 2026 14:29:29 +0000 Subject: [PATCH 018/178] add HasComp methods to EntityPrototype --- Robust.Shared/Prototypes/EntityPrototype.cs | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b5be7c3702b..12aeb67317d 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Robust.Shared.EntitySerialization; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -202,6 +203,33 @@ public bool TryGetComponent([NotNullWhen(true)] out T? component) return true; } + /// + /// Returns true if this prototype contains a component. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp(IComponentFactory factory) where T : IComponent, new() + => HasComp(factory.GetComponentName()); + + /// + /// Returns true if this prototype contains a component with a given type. + /// It is a programmer error if the name does not belong to a component. + /// This is not caught by a debug assert, so only use it with types from a + /// component registry, typeof(SomeComponent), etc. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp(Type type, IComponentFactory factory) + => HasComp(factory.GetComponentName(type)); + + /// + /// Returns true if this prototype contains a component with a given name. + /// It is a programmer error if the name does not belong to a component. + /// This is not caught by a debug assert so if you use this method, + /// make sure you got the name from . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasComp([ForbidLiteral] string name) + => Components.ContainsKey(name); + internal static void LoadEntity( Entity ent, IComponentFactory factory, From c12e4e7acc874c2e5017714d987f4a0c10736e1e Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sat, 24 Jan 2026 14:30:28 +0000 Subject: [PATCH 019/178] add HasComp proxy methods for EntProtoId and EntityPrototype --- .../GameObjects/EntitySystem.Proxy.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 8a264e68720..72ced10ab1f 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -8,6 +8,7 @@ using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; @@ -692,6 +693,54 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) return EntityManager.HasComponent(uid, type); } + /// + /// Returns true if an entity prototype has a component. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp([ForbidLiteral] EntProtoId id) where T : IComponent, new() + => HasComp(id, Factory.GetComponentName()); + + /// + /// Returns true if an entity prototype has a component with a given type. + /// Will throw if the type does not belong to a component. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp([ForbidLiteral] EntProtoId id, Type type) + => HasComp(id, Factory.GetComponentName(type)); + + /// + /// Returns true if an entity prototype contains a component with a given name. + /// Logs errors in case of the prototype not existing, debug asserts if the name is invalid. + /// If you call this a lot on the same prototype, resolve it first and use the -using HasComp variants. + /// + protected bool HasComp([ForbidLiteral] EntProtoId id, string name) + => Proto.Resolve(id, out var proto) && proto.HasComp(name); + + /// + /// Returns true if a resolved entity prototype has a component. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp(EntityPrototype proto) where T : IComponent, new() + => proto.HasComp(Factory); + + /// + /// Returns true if a resolved entity prototype has a component with a given type. + /// Will throw if the type does not belong to a component. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected bool HasComp(EntityPrototype proto, Type type) + => proto.HasComp(type, Factory); + + /// + /// Returns true if a resolved entity prototype contains a component with a given name. + /// Debug asserts that the name is for a valid component. + /// + protected bool HasComp(EntityPrototype proto, string name) + { + DebugTools.Assert(Factory.TryGetRegistration(name, out _), $"Tried to check entity prototype {proto.ID} for unregistered component '{name}'"); + return proto.HasComp(name); + } + #endregion #region Component Add From 0c4a40c6ab838a90ba37caedda0fce5ccf3d7a0f Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sat, 24 Jan 2026 14:32:28 +0000 Subject: [PATCH 020/178] add tests for EntityPrototype.HasComp methods --- .../Prototypes/PrototypeHasCompTest.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs new file mode 100644 index 00000000000..895de1da5d1 --- /dev/null +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -0,0 +1,107 @@ +using JetBrains.Annotations; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; + +namespace Robust.UnitTesting.Shared.Prototypes; + +/// +/// Tests the HasComp family of methods. +/// +[UsedImplicitly] +[TestFixture] +internal sealed class PrototypeHasCompTest : OurRobustUnitTest +{ + private IComponentFactory _factory = default!; + private IPrototypeManager _proto = default!; + + protected override Type[] ExtraComponents => + [ + typeof(TestDefinedComponent), + typeof(TestInheritedComponent), + typeof(TestMissingComponent) + ]; + + // TestEntity is expected to have TestDefinedComponent, TestInheritedComponent but not TestMissingComponent + const string TestEntity = "TestEntity"; + + [OneTimeSetUp] + public void Setup() + { + IoCManager.Resolve().Initialize(); + _factory = IoCManager.Resolve(); + _proto = IoCManager.Resolve(); + + _proto.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _proto.LoadString(TestPrototypes); + _proto.ResolveResults(); + } + + [Test] + public void TestHasCompGeneric() + { + var proto = _proto.Index(TestEntity); + Assert.That(proto.HasComp(_factory)); + Assert.That(proto.HasComp(_factory)); + Assert.That(!proto.HasComp(_factory)); + } + + [Test] + public void TestHasCompType() + { + var proto = _proto.Index(TestEntity); + + // first test with GetRegistration + var defined = _factory.GetRegistration().Type; + var inherited = _factory.GetRegistration().Type; + var missing = _factory.GetRegistration().Type; + + void TestThem() + { + Assert.That(proto.HasComp(defined, _factory)); + Assert.That(proto.HasComp(inherited, _factory)); + Assert.That(!proto.HasComp(missing, _factory)); + } + + TestThem(); + + // then test with typeof + defined = typeof(TestDefinedComponent); + inherited = typeof(TestInheritedComponent); + missing = typeof(TestMissingComponent); + TestThem(); + } + + [Test] + public void TestHasCompString() + { + var proto = _proto.Index(TestEntity); + // intentionally skirting ForbidLiteral here + var defined = "TestDefined"; + var inherited = "TestInherited"; + var missing = "TestMissing"; + Assert.That(proto.HasComp(defined)); + Assert.That(proto.HasComp(inherited)); + Assert.That(!proto.HasComp(missing)); + } + + const string TestPrototypes = $@" +- type: entity + parent: TestEntityParent # making sure inheritance wouldn't break it + id: {TestEntity} + components: + - type: TestDefined + +- type: entity + abstract: true + id: TestEntityParent + components: + - type: TestInherited +"; +} + +internal sealed partial class TestDefinedComponent : Component; +internal sealed partial class TestInheritedComponent : Component; +internal sealed partial class TestMissingComponent : Component; From a86e33ccb6bd2f3c5b083c8ace97d800a71d46d8 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Sun, 17 May 2026 19:54:33 +0100 Subject: [PATCH 021/178] a Co-authored-by: Tayrtahn --- Robust.Shared/GameObjects/EntitySystem.Proxy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 72ced10ab1f..d98b030c2fa 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -717,7 +717,7 @@ protected bool HasComp([ForbidLiteral] EntProtoId id, string name) => Proto.Resolve(id, out var proto) && proto.HasComp(name); /// - /// Returns true if a resolved entity prototype has a component. + /// Returns true if a resolved entity prototype has a . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp(EntityPrototype proto) where T : IComponent, new() From 6522ff42eca63c8006b2266b63494658ac983769 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Sun, 17 May 2026 19:57:03 +0100 Subject: [PATCH 022/178] Apply suggestions from code review Co-authored-by: Tayrtahn --- .../Prototypes/PrototypeHasCompTest.cs | 3 ++- Robust.Shared/GameObjects/EntitySystem.Proxy.cs | 2 +- Robust.Shared/Prototypes/EntityPrototype.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs index 895de1da5d1..28c8217c38e 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -11,7 +11,8 @@ namespace Robust.UnitTesting.Shared.Prototypes; /// Tests the HasComp family of methods. /// [UsedImplicitly] -[TestFixture] +[TestOf(typeof(EntityPrototype))] +[Description($"Tests the {nameof(EntityPrototype)} HasComp family of methods"] internal sealed class PrototypeHasCompTest : OurRobustUnitTest { private IComponentFactory _factory = default!; diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index d98b030c2fa..d77397a0e61 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -694,7 +694,7 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) } /// - /// Returns true if an entity prototype has a component. + /// Returns true if an entity prototype has a . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp([ForbidLiteral] EntProtoId id) where T : IComponent, new() diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 12aeb67317d..b71b826887f 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -204,7 +204,7 @@ public bool TryGetComponent([NotNullWhen(true)] out T? component) } /// - /// Returns true if this prototype contains a component. + /// Returns true if this prototype contains a . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(IComponentFactory factory) where T : IComponent, new() From f9285784f019429f8504deffc5f24c8d5c13ebb5 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sun, 17 May 2026 15:49:06 -0400 Subject: [PATCH 023/178] Update Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs --- .../Prototypes/PrototypeHasCompTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs index 28c8217c38e..336e162a9e8 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -12,7 +12,7 @@ namespace Robust.UnitTesting.Shared.Prototypes; /// [UsedImplicitly] [TestOf(typeof(EntityPrototype))] -[Description($"Tests the {nameof(EntityPrototype)} HasComp family of methods"] +[Description($"Tests the {nameof(EntityPrototype)} HasComp family of methods")] internal sealed class PrototypeHasCompTest : OurRobustUnitTest { private IComponentFactory _factory = default!; From dc2cbd952fda8b05b49aef4206794aaa4b24f66e Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sun, 17 May 2026 21:56:26 +0100 Subject: [PATCH 024/178] CompName gaming --- .../Prototypes/PrototypeHasCompTest.cs | 54 +++++++++++++-- .../Audio/Systems/SharedAudioSystem.cs | 1 - Robust.Shared/GameObjects/CompName.cs | 67 +++++++++++++++++++ Robust.Shared/GameObjects/ComponentFactory.cs | 11 +++ .../GameObjects/EntitySystem.Proxy.cs | 32 +++------ Robust.Shared/GameObjects/EntitySystem.cs | 2 +- .../GameObjects/IComponentFactory.cs | 19 ++++++ Robust.Shared/Prototypes/EntityPrototype.cs | 51 ++++++++------ .../Implementations/CompNameSerializer.cs | 39 +++++++++++ 9 files changed, 226 insertions(+), 50 deletions(-) create mode 100644 Robust.Shared/GameObjects/CompName.cs create mode 100644 Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs index 336e162a9e8..9871a6a1f71 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -4,11 +4,12 @@ using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; namespace Robust.UnitTesting.Shared.Prototypes; /// -/// Tests the HasComp family of methods. +/// Tests the HasComp family of methods as well as serialization. /// [UsedImplicitly] [TestOf(typeof(EntityPrototype))] @@ -22,11 +23,14 @@ internal sealed class PrototypeHasCompTest : OurRobustUnitTest [ typeof(TestDefinedComponent), typeof(TestInheritedComponent), - typeof(TestMissingComponent) + typeof(TestMissingComponent), + typeof(TestCompNameComponent) ]; // TestEntity is expected to have TestDefinedComponent, TestInheritedComponent but not TestMissingComponent const string TestEntity = "TestEntity"; + // expected to have TestCompNameComponent + const string TestInception = "TestInception"; [OneTimeSetUp] public void Setup() @@ -35,7 +39,7 @@ public void Setup() _factory = IoCManager.Resolve(); _proto = IoCManager.Resolve(); - _proto.RegisterKind(typeof(EntityPrototype), typeof(EntityCategoryPrototype)); + _proto.Initialize(); _proto.LoadString(TestPrototypes); _proto.ResolveResults(); } @@ -47,6 +51,11 @@ public void TestHasCompGeneric() Assert.That(proto.HasComp(_factory)); Assert.That(proto.HasComp(_factory)); Assert.That(!proto.HasComp(_factory)); + + // and with CompName separately + Assert.That(proto.HasComp(_factory.CompName())); + Assert.That(proto.HasComp(_factory.CompName())); + Assert.That(!proto.HasComp(_factory.CompName())); } [Test] @@ -79,15 +88,28 @@ void TestThem() public void TestHasCompString() { var proto = _proto.Index(TestEntity); - // intentionally skirting ForbidLiteral here - var defined = "TestDefined"; - var inherited = "TestInherited"; - var missing = "TestMissing"; + var defined = new CompName("TestDefined", _factory); + var inherited = new CompName("TestInherited", _factory); + var missing = new CompName("TestMissing", _factory); Assert.That(proto.HasComp(defined)); Assert.That(proto.HasComp(inherited)); Assert.That(!proto.HasComp(missing)); } + [Test] + public void TestCompNameSerialization() + { + var proto = _proto.Index(TestInception); + Assert.That(proto.TryComp(out var inception, _factory)); + var name = inception!.Comp; + Assert.That(name, Is.EqualTo(_factory.CompName())); // inception + Assert.That(proto.HasComp(name)); + Assert.That(_factory.HasRegistration(name)); + + // gibberish component should prevent it from loading + Assert.That(!_proto.HasIndex("TestFail")); + } + const string TestPrototypes = $@" - type: entity parent: TestEntityParent # making sure inheritance wouldn't break it @@ -100,9 +122,27 @@ public void TestHasCompString() id: TestEntityParent components: - type: TestInherited + +- type: entity + id: TestInception + components: + - type: TestCompName + comp: TestCompName # inception + +- type: entity + id: TestFail + components: + - type: TestCompName + comp: Afnuaghdjngbjda # this prevents this prototype from loading, hopefully nobody adds this component to rt :godo: "; } internal sealed partial class TestDefinedComponent : Component; internal sealed partial class TestInheritedComponent : Component; internal sealed partial class TestMissingComponent : Component; + +internal sealed partial class TestCompNameComponent : Component +{ + [DataField(required: true)] + public CompName Comp; +} diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 747b973562f..4a22cebb0ed 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -32,7 +32,6 @@ public abstract partial class SharedAudioSystem : EntitySystem [Dependency] protected IConfigurationManager CfgManager = default!; [Dependency] protected IGameTiming Timing = default!; [Dependency] private INetManager _netManager = default!; - [Dependency] protected IPrototypeManager ProtoMan = default!; [Dependency] protected IRobustRandom RandMan = default!; [Dependency] protected MetaDataSystem MetadataSys = default!; [Dependency] protected SharedTransformSystem XformSystem = default!; diff --git a/Robust.Shared/GameObjects/CompName.cs b/Robust.Shared/GameObjects/CompName.cs new file mode 100644 index 00000000000..67ff2e4043b --- /dev/null +++ b/Robust.Shared/GameObjects/CompName.cs @@ -0,0 +1,67 @@ +using System; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Robust.Shared.GameObjects; + +/// +/// Wrapper type for the name of a component. +/// All methods of creating it are checked to be valid, so the string value is also guaranteed to be valid. +/// +/// +/// This will be automatically validated by if used in data fields. +/// Doing so however will NOT +/// +public readonly record struct CompName : + IEquatable, + IComparable, + IAsType +{ + /// + /// Wrap a component name string, throwing an exception if it is not registered. + /// + public CompName(string name, IComponentFactory factory) + { + if (!factory.HasRegistration(name)) + throw new UnknownComponentException($"Tried to create CompName with unregistered component name '{name}'"); + Name = name; + } + + private CompName([ForbidLiteral] string name) + { + // no checking, only used by Get methods below which are guaranteed to be valid + Name = name; + } + + public static implicit operator string(CompName name) + => name.Name; + + /// + /// Get the name of a given component using a and . + /// + public static CompName Get(IComponentFactory factory) where T : IComponent, new() + => new CompName(factory.GetComponentName()); + + /// + /// Get the name of a given component using a and a component's . + /// Will throw for non-component types. + /// + public static CompName Get(Type type, IComponentFactory factory) + => new CompName(factory.GetComponentName(type)); + + /// + /// The underlying string for this component name. + /// E.g. for TransformComponent this is "Transform". + /// + public string Name { get; private init; } + + public bool Equals(string? other) + => Name == other; + + public int CompareTo(CompName other) + => string.Compare(Name, other.Name, StringComparison.Ordinal); + + public string AsType() => Name; + + public override string ToString() => Name; +} diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index 20bedc1d21e..aaab8e16b07 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -349,6 +349,14 @@ public string GetComponentName(ushort netID) return GetRegistration(netID).Name; } + [Pure] + public CompName CompName() where T : IComponent, new() + => GameObjects.CompName.Get(this); + + [Pure] + public CompName CompName(Type type) + => GameObjects.CompName.Get(type, this); + public ComponentRegistration GetRegistration(ushort netID) { if (_networkedComponents is null) @@ -407,6 +415,9 @@ public bool TryGetRegistration(string componentName, [NotNullWhen(true)] out Com return false; } + public bool HasRegistration(string componentName) + => _names.ContainsKey(componentName); + public bool TryGetRegistration(Type reference, [NotNullWhen(true)] out ComponentRegistration? registration) { if (_types.TryGetValue(reference, out var tempRegistration)) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index d77397a0e61..5fb526c6ecf 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -698,7 +698,7 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp([ForbidLiteral] EntProtoId id) where T : IComponent, new() - => HasComp(id, Factory.GetComponentName()); + => HasComp(id, Factory.CompName()); /// /// Returns true if an entity prototype has a component with a given type. @@ -706,40 +706,30 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp([ForbidLiteral] EntProtoId id, Type type) - => HasComp(id, Factory.GetComponentName(type)); + => HasComp(id, Factory.CompName(type)); /// - /// Returns true if an entity prototype contains a component with a given name. - /// Logs errors in case of the prototype not existing, debug asserts if the name is invalid. - /// If you call this a lot on the same prototype, resolve it first and use the -using HasComp variants. + /// Returns true if a entity prototype contains a component with a given name. + /// Logs errors in case of the prototype not existing. + /// If you call this a lot on the same prototype, resolve it first and call the -using HasComp variants. /// - protected bool HasComp([ForbidLiteral] EntProtoId id, string name) - => Proto.Resolve(id, out var proto) && proto.HasComp(name); + protected bool HasComp([ForbidLiteral] EntProtoId id, CompName name) + => ProtoMan.Resolve(id, out var proto) && proto.HasComp(name); /// - /// Returns true if a resolved entity prototype has a . + /// Returns true if an already-resolved entity prototype has a component of type . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp(EntityPrototype proto) where T : IComponent, new() - => proto.HasComp(Factory); + => proto.HasComp(Factory.CompName()); /// - /// Returns true if a resolved entity prototype has a component with a given type. + /// Returns true if an already-resolved entity prototype has a component with a given type. /// Will throw if the type does not belong to a component. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp(EntityPrototype proto, Type type) - => proto.HasComp(type, Factory); - - /// - /// Returns true if a resolved entity prototype contains a component with a given name. - /// Debug asserts that the name is for a valid component. - /// - protected bool HasComp(EntityPrototype proto, string name) - { - DebugTools.Assert(Factory.TryGetRegistration(name, out _), $"Tried to check entity prototype {proto.ID} for unregistered component '{name}'"); - return proto.HasComp(name); - } + => proto.HasComp(Factory.CompName(type)); #endregion diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index bb256bd4f70..00254839d37 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -25,7 +25,7 @@ namespace Robust.Shared.GameObjects public abstract partial class EntitySystem : IEntitySystem, IPostInjectInit { [Dependency] protected EntityManager EntityManager = default!; - [Dependency] protected IPrototypeManager Proto = default!; + [Dependency] protected IPrototypeManager ProtoMan = default!; [Dependency] protected ILogManager LogManager = default!; [Dependency] private ISharedPlayerManager _playerMan = default!; [Dependency] private IReplayRecordingManager _replayMan = default!; diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index 219826c6dbf..e2cb49ab509 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -193,6 +193,19 @@ public interface IComponentFactory [Pure] string GetComponentName(ushort netID); + /// + /// Get a wrapped for a given component type. + /// + [Pure] + CompName CompName() where T : IComponent, new(); + + /// + /// Get a wrapped for a component's . + /// Throws for non-component types. + /// + [Pure] + CompName CompName(Type type); + /// /// Gets the registration belonging to a component, throwing an exception if it does not exist. /// @@ -250,6 +263,12 @@ public interface IComponentFactory /// bool IsIgnored(string componentName); + /// + /// Returns true if a given name has a component registration associated with it. + /// This check is always case sensitive. + /// + bool HasRegistration(string componentName); + /// /// Tries to get the registration belonging to a component. /// diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index b71b826887f..bcd53362c3f 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -170,29 +170,43 @@ void ISerializationHooks.AfterDeserialization() } [Obsolete("Pass in IComponentFactory")] - public bool TryGetComponent([NotNullWhen(true)] out T? component) - where T : IComponent, new() - { - var compName = IoCManager.Resolve().GetComponentName(); - return TryGetComponent(compName, out component); - } + public bool TryGetComponent([NotNullWhen(true)] out T? component) where T : IComponent, new() + => TryGetComponent(out component, IoCManager.Resolve()); + [Obsolete("TryComp is shorter, use it instead")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetComponent([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new() - { - var compName = factory.GetComponentName(); - return TryGetComponent(compName, out component); - } + => TryComp(out component, factory); + [Obsolete("Use TryComp with a CompName instead")] public bool TryGetComponent(string name, [NotNullWhen(true)] out T? component) where T : IComponent, new() { - DebugTools.AssertEqual(IoCManager.Resolve().GetComponentName(), name); + // please do not use this method ever ^_^ + var factory = IoCManager.Resolve(); + return TryComp(factory.CompName(), out component); + } + + /// + /// Tries to get and cast a component from this prototype with . + /// Returns false if the component is missing or is not assignable to . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryComp(out T? component, IComponentFactory factory) where T : IComponent, new() + => TryComp(factory.CompName(), out component); - if (!Components.TryGetValue(name, out var componentUnCast)) + /// + /// Tries to get and cast a component from this prototype with a associated with it. + /// Returns false if the component is missing or is not assignable to . + /// + public bool TryComp(CompName name, [NotNullWhen(true)] out T? component) where T : IComponent, new() + { + if (!Components.TryGetValue(name.Name, out var componentUnCast)) { component = default; return false; } + // TODO: should this throw? might break some crazy shitcode though if (componentUnCast.Component is not T cast) { component = default; @@ -208,7 +222,7 @@ public bool TryGetComponent([NotNullWhen(true)] out T? component) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(IComponentFactory factory) where T : IComponent, new() - => HasComp(factory.GetComponentName()); + => HasComp(factory.CompName()); /// /// Returns true if this prototype contains a component with a given type. @@ -218,17 +232,14 @@ public bool TryGetComponent([NotNullWhen(true)] out T? component) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(Type type, IComponentFactory factory) - => HasComp(factory.GetComponentName(type)); + => HasComp(factory.CompName(type)); /// - /// Returns true if this prototype contains a component with a given name. - /// It is a programmer error if the name does not belong to a component. - /// This is not caught by a debug assert so if you use this method, - /// make sure you got the name from . + /// Returns true if this prototype contains a component with a given . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasComp([ForbidLiteral] string name) - => Components.ContainsKey(name); + public bool HasComp(CompName name) + => Components.ContainsKey(name.Name); internal static void LoadEntity( Entity ent, diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs new file mode 100644 index 00000000000..bd1a51ab8b0 --- /dev/null +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/CompNameSerializer.cs @@ -0,0 +1,39 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using static Robust.Shared.Serialization.Manager.ISerializationManager; + +namespace Robust.Shared.Serialization.TypeSerializers.Implementations; + +/// +/// Serializer used automatically for types. +/// +[TypeSerializer] +public sealed class CompNameSerializer : ITypeSerializer, ITypeCopyCreator +{ + private IComponentFactory? _factory; + + public ValidationNode Validate(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, ISerializationContext? context = null) + { + var name = node.Value; + _factory ??= dependencies.Resolve(); + if (_factory.HasRegistration(name)) + return new ValidatedValueNode(node); + + return new ErrorNode(node, $"No component found with name {name}"); + } + + public CompName Read(ISerializationManager serialization, ValueDataNode node, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null, InstantiationDelegate? instanceProvider = null) + => new CompName(node.Value, _factory ??= dependencies.Resolve()); + + public DataNode Write(ISerializationManager serialization, CompName value, IDependencyCollection dependencies, bool alwaysWrite = false, ISerializationContext? context = null) + => new ValueDataNode(value.Name); + + public CompName CreateCopy(ISerializationManager serializationManager, CompName source, IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null) + => source; +} From 9160f48608bbeee51aee9b4e9aabde03a04736f8 Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:kde.org> Date: Sun, 17 May 2026 22:40:35 +0100 Subject: [PATCH 025/178] fix remark on no ignored components --- Robust.Shared/GameObjects/CompName.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/GameObjects/CompName.cs b/Robust.Shared/GameObjects/CompName.cs index 67ff2e4043b..3dd84f58ce0 100644 --- a/Robust.Shared/GameObjects/CompName.cs +++ b/Robust.Shared/GameObjects/CompName.cs @@ -10,7 +10,7 @@ namespace Robust.Shared.GameObjects; /// /// /// This will be automatically validated by if used in data fields. -/// Doing so however will NOT +/// Doing so however will NOT skip ignored components, move your shitcode to shared! /// public readonly record struct CompName : IEquatable, From cb768d2aa5ffe3e4658941c9c40eb531e67adc52 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Thu, 18 Jun 2026 17:21:47 +1000 Subject: [PATCH 026/178] Raise a BUI message on any input The bound wrapper is internal only and I don't want these filtered out by UI validation. --- Robust.Server/GameObjects/EntitySystems/InputSystem.cs | 4 ++-- .../Components/UserInterface/UserInterfaceComponent.cs | 9 +++++++++ .../GameObjects/Systems/SharedUserInterfaceSystem.cs | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs index 57a5ab2a15a..5c844ba1c18 100644 --- a/Robust.Server/GameObjects/EntitySystems/InputSystem.cs +++ b/Robust.Server/GameObjects/EntitySystems/InputSystem.cs @@ -38,7 +38,7 @@ public override void Shutdown() private void InputMessageHandler(InputCmdMessage message, EntitySessionEventArgs eventArgs) { - if (!(message is FullInputCmdMessage msg)) + if (message is not FullInputCmdMessage msg) return; //Client Sanitization: out of bounds functionID @@ -46,7 +46,7 @@ private void InputMessageHandler(InputCmdMessage message, EntitySessionEventArgs return; //Client Sanitization: bad enum key state value - if (!Enum.IsDefined(typeof(BoundKeyState), msg.State)) + if (!Enum.IsDefined(msg.State)) return; var session = eventArgs.SenderSession; diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs index b44986238ec..1e6f890be29 100644 --- a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs +++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs @@ -160,6 +160,15 @@ public sealed class BoundUserInterfaceMessageAttempt( public readonly BoundUserInterfaceMessage Message = message; } + /// + /// Raised whenever the server receives a BUI wrap message from a client for a valid interface. + /// + [ByRefEvent] + public readonly record struct BoundUserInterfaceMessageReceivedEvent( + EntityUid Actor, + EntityUid Target, + Enum UiKey); + [NetSerializable, Serializable] public abstract class BoundUserInterfaceState { diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 7012d65c729..5b07899cced 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -107,6 +107,9 @@ private void OnMessageReceived(BoundUIWrapMessage msg, EntityUid sender) return; } + var received = new BoundUserInterfaceMessageReceivedEvent(sender, uid, msg.UiKey); + RaiseLocalEvent(ref received); + // If it's not an open message check we're even a subscriber. if (msg.Message is not OpenBoundInterfaceMessage && (!uiComp.Actors.TryGetValue(msg.UiKey, out var actors) || From 51cdfb6b7ec9db6ff3bf3f9a3109ff1ac7328740 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 18 Jun 2026 10:39:35 +0200 Subject: [PATCH 027/178] Add "hidden" console commands --- RELEASE-NOTES.md | 2 +- Robust.Client/Console/ClientConsoleHost.Completions.cs | 2 +- Robust.Shared/Console/Commands/ListCommand.cs | 2 +- Robust.Shared/Console/ConsoleHost.cs | 5 +++++ Robust.Shared/Console/IConsoleCommand.cs | 3 +++ Robust.Shared/Console/IConsoleHost.cs | 6 ++++++ 6 files changed, 17 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 94dd7e357c2..bd3d89b9a34 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Console commands can now be "hidden" by prefixing them with `_`. ### Bugfixes diff --git a/Robust.Client/Console/ClientConsoleHost.Completions.cs b/Robust.Client/Console/ClientConsoleHost.Completions.cs index 7bda1199f44..6195a952b2e 100644 --- a/Robust.Client/Console/ClientConsoleHost.Completions.cs +++ b/Robust.Client/Console/ClientConsoleHost.Completions.cs @@ -34,7 +34,7 @@ private Task CalcCompletions(List args, string argStr, // Typing out command name, handle this ourselves. var cmdOptions = CompletionResult.FromOptions( AvailableCommands.Values - .Where(c => CanExecute(c.Command)) + .Where(c => CanExecute(c.Command) && !IsCommandHidden(c)) .OrderBy(c => c.Command) .Select(c => new CompletionOption(c.Command, c.Description))); diff --git a/Robust.Shared/Console/Commands/ListCommand.cs b/Robust.Shared/Console/Commands/ListCommand.cs index 53df9233f26..6b9c7f5ded4 100644 --- a/Robust.Shared/Console/Commands/ListCommand.cs +++ b/Robust.Shared/Console/Commands/ListCommand.cs @@ -18,7 +18,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) var builder = new StringBuilder(Loc.GetString("cmd-list-heading")); foreach (var command in host.AvailableCommands.Values - .Where(p => p.Command.Contains(filter)) + .Where(p => p.Command.Contains(filter) && !host.IsCommandHidden(p)) .OrderBy(c => c.Command)) { //TODO: Make this actually check permissions. diff --git a/Robust.Shared/Console/ConsoleHost.cs b/Robust.Shared/Console/ConsoleHost.cs index 5b6c239ec1a..58fd66fa270 100644 --- a/Robust.Shared/Console/ConsoleHost.cs +++ b/Robust.Shared/Console/ConsoleHost.cs @@ -81,6 +81,11 @@ public void LoadConsoleCommands() } } + public bool IsCommandHidden(IConsoleCommand command) + { + return command.Command.StartsWith('_'); + } + protected virtual void UpdateAvailableCommands() { } diff --git a/Robust.Shared/Console/IConsoleCommand.cs b/Robust.Shared/Console/IConsoleCommand.cs index b59d220729e..9f6544d25d7 100644 --- a/Robust.Shared/Console/IConsoleCommand.cs +++ b/Robust.Shared/Console/IConsoleCommand.cs @@ -21,6 +21,9 @@ public interface IConsoleCommand /// /// A string as identifier for this command. /// + /// + /// Commands starting with '_' are treated as "hidden". They will not be shown in listings or completions. + /// string Command { get; } /// diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index 66b86419260..6ee2f4ea897 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -66,6 +66,12 @@ public interface IConsoleHost /// void LoadConsoleCommands(); + /// + /// Check whether a console command is hidden. + /// + /// + bool IsCommandHidden(IConsoleCommand command); + #region RegisterCommand /// /// Registers a console command into the console system. This is an alternative to From a008dda4fa1f568c15b5975a02ef372ab621879d Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:goida.zip> Date: Thu, 18 Jun 2026 15:15:42 +0100 Subject: [PATCH 028/178] add pure --- Robust.Shared/GameObjects/EntitySystem.Proxy.cs | 6 ++++++ Robust.Shared/Prototypes/EntityPrototype.cs | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 5fb526c6ecf..3d6ca5f3874 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using JetBrains.Annotations; using Robust.Shared.Containers; @@ -696,6 +697,7 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) /// /// Returns true if an entity prototype has a . /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp([ForbidLiteral] EntProtoId id) where T : IComponent, new() => HasComp(id, Factory.CompName()); @@ -704,6 +706,7 @@ protected bool HasComp([NotNullWhen(true)] EntityUid? uid, Type type) /// Returns true if an entity prototype has a component with a given type. /// Will throw if the type does not belong to a component. /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp([ForbidLiteral] EntProtoId id, Type type) => HasComp(id, Factory.CompName(type)); @@ -713,12 +716,14 @@ protected bool HasComp([ForbidLiteral] EntProtoId id, Type type) /// Logs errors in case of the prototype not existing. /// If you call this a lot on the same prototype, resolve it first and call the -using HasComp variants. /// + [Pure] protected bool HasComp([ForbidLiteral] EntProtoId id, CompName name) => ProtoMan.Resolve(id, out var proto) && proto.HasComp(name); /// /// Returns true if an already-resolved entity prototype has a component of type . /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp(EntityPrototype proto) where T : IComponent, new() => proto.HasComp(Factory.CompName()); @@ -727,6 +732,7 @@ protected bool HasComp([ForbidLiteral] EntProtoId id, CompName name) /// Returns true if an already-resolved entity prototype has a component with a given type. /// Will throw if the type does not belong to a component. /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool HasComp(EntityPrototype proto, Type type) => proto.HasComp(Factory.CompName(type)); diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index bcd53362c3f..bb73abca2b5 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using Robust.Shared.EntitySerialization; using Robust.Shared.GameObjects; @@ -173,6 +174,7 @@ void ISerializationHooks.AfterDeserialization() public bool TryGetComponent([NotNullWhen(true)] out T? component) where T : IComponent, new() => TryGetComponent(out component, IoCManager.Resolve()); + [Pure] [Obsolete("TryComp is shorter, use it instead")] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetComponent([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new() @@ -190,6 +192,7 @@ void ISerializationHooks.AfterDeserialization() /// Tries to get and cast a component from this prototype with . /// Returns false if the component is missing or is not assignable to . /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryComp(out T? component, IComponentFactory factory) where T : IComponent, new() => TryComp(factory.CompName(), out component); @@ -198,6 +201,7 @@ void ISerializationHooks.AfterDeserialization() /// Tries to get and cast a component from this prototype with a associated with it. /// Returns false if the component is missing or is not assignable to . /// + [Pure] public bool TryComp(CompName name, [NotNullWhen(true)] out T? component) where T : IComponent, new() { if (!Components.TryGetValue(name.Name, out var componentUnCast)) @@ -220,6 +224,7 @@ void ISerializationHooks.AfterDeserialization() /// /// Returns true if this prototype contains a . /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(IComponentFactory factory) where T : IComponent, new() => HasComp(factory.CompName()); @@ -230,6 +235,7 @@ void ISerializationHooks.AfterDeserialization() /// This is not caught by a debug assert, so only use it with types from a /// component registry, typeof(SomeComponent), etc. /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(Type type, IComponentFactory factory) => HasComp(factory.CompName(type)); @@ -237,6 +243,7 @@ public bool HasComp(Type type, IComponentFactory factory) /// /// Returns true if this prototype contains a component with a given . /// + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasComp(CompName name) => Components.ContainsKey(name.Name); From e1ebbd0acb939281b5ad38032f60a04177e249e5 Mon Sep 17 00:00:00 2001 From: deltanedas <@deltanedas:goida.zip> Date: Thu, 18 Jun 2026 15:17:41 +0100 Subject: [PATCH 029/178] ! --- Robust.Shared/GameObjects/EntitySystem.Proxy.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 3d6ca5f3874..9c4c055044d 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using JetBrains.Annotations; using Robust.Shared.Containers; From dc7bb35cabae42b0f385d340a3a816608b3bff82 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Fri, 19 Jun 2026 10:00:05 +1000 Subject: [PATCH 030/178] Make Box2.Contains faster Turns out being fancy is slower at least on my machine. --- .../NumericsHelpers/Box2Benchmark.cs | 24 +++++++++++++++---- Robust.Shared.Maths/Box2.cs | 19 +++++++++------ 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs index 4fc4f9f6157..e40cb2c9ca3 100644 --- a/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs +++ b/Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs @@ -8,12 +8,28 @@ namespace Robust.Benchmarks.NumericsHelpers; [Virtual, DisassemblyDiagnoser] public class Box2Benchmark { - public Box2 Box = new(); - public Matrix3x2 Matrix = new(); + public Box2 Box = new(-1, -2, 3, 4); + public Vector2 Point = new(0.75f, 1.25f); + public Matrix3x2 Matrix = Matrix3x2.CreateRotation(0.5f) * Matrix3x2.CreateTranslation(3, -1); + private Box2 _boxResult; [Benchmark] - public Box2 Transform() + public void Transform() { - return Matrix.TransformBox(Box); + _boxResult = Matrix.TransformBox(Box); + } + + [Benchmark(Baseline = true)] + public bool ContainsOld() + { + var xOk = Point.X >= Box.Left ^ Point.X > Box.Right; + var yOk = Point.Y >= Box.Bottom ^ Point.Y > Box.Top; + return xOk && yOk; + } + + [Benchmark] + public bool ContainsCurrent() + { + return Box.Contains(Point); } } diff --git a/Robust.Shared.Maths/Box2.cs b/Robust.Shared.Maths/Box2.cs index f7c8db88a28..81c1c43c855 100644 --- a/Robust.Shared.Maths/Box2.cs +++ b/Robust.Shared.Maths/Box2.cs @@ -309,13 +309,18 @@ public readonly bool Contains(float x, float y) [Pure] public readonly bool Contains(Vector2 point, bool closedRegion = true) { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Bottom ^ point.Y > Top - : point.Y > Bottom ^ point.Y >= Top; - return xOk && yOk; + if (closedRegion) + { + return point.X >= Left + && point.X <= Right + && point.Y >= Bottom + && point.Y <= Top; + } + + return point.X > Left + && point.X < Right + && point.Y > Bottom + && point.Y < Top; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From bfaccfced21aa19fabc90701f113259cc357b569 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 18 Jun 2026 20:45:36 -0400 Subject: [PATCH 031/178] Test building content master against engine in release configuration (#35) --- .github/workflows/test-content.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index b42fdc0947c..87cb7d59d0b 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -34,7 +34,9 @@ jobs: run: cp RobustToolbox/global.json . - name: Install dependencies run: dotnet restore - - name: Build + - name: Build (Release) + run: dotnet build --configuration Release --no-restore /m + - name: Build (Debug) run: dotnet build --configuration DebugOpt --no-restore /m - name: Content.Tests shell: pwsh From faad9098fea5ed8bc53d50b7c94ce4438dce0457 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Fri, 19 Jun 2026 19:32:21 +1000 Subject: [PATCH 032/178] Update release notes Separate commit to make it easier for history. --- RELEASE-NOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8a0624cdf16..05ebb366629 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,7 +43,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* Windows will stay at relative position not absolute pixel position on window resize. ### Other @@ -51,7 +51,9 @@ END TEMPLATE--> ### Internal -*None yet* +* Reduce TryParseEnum string allocations. +* Reduce TryRelativeTo string allocations. +* Reduce OpenGL logging string allocations on debug for the client. ## 277.1.0 From 703cf3293dbfac36ae360d75c43ad378cac299c0 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Fri, 19 Jun 2026 19:35:00 +1000 Subject: [PATCH 033/178] RN --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 05ebb366629..500f65ff2b4 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -54,6 +54,7 @@ END TEMPLATE--> * Reduce TryParseEnum string allocations. * Reduce TryRelativeTo string allocations. * Reduce OpenGL logging string allocations on debug for the client. +* Optimise sprite sorting slightly. ## 277.1.0 From f48ac8c01b79286212d8346ebf893c213c084566 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Fri, 19 Jun 2026 20:21:39 +1000 Subject: [PATCH 034/178] RN --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 500f65ff2b4..b08d18f6572 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -55,6 +55,7 @@ END TEMPLATE--> * Reduce TryRelativeTo string allocations. * Reduce OpenGL logging string allocations on debug for the client. * Optimise sprite sorting slightly. +* Simplify and optimise Box2.Contains(Vector2) ## 277.1.0 From 32237cf719a2fb993dfbca377b3e9df6816c2c7a Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Fri, 19 Jun 2026 20:35:02 +1000 Subject: [PATCH 035/178] RN --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b08d18f6572..3faf304606f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Add a BoundUserInterfaceMessageReceivedEvent that will be raised whenever a BoundUserInterfaceMessage is received regardless of validation. ### Bugfixes From 8f6ebad5b97f16789e77068bafb3682427945f67 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Fri, 19 Jun 2026 21:55:44 +1000 Subject: [PATCH 036/178] Reduce game memory usage a crumb (#30) --- RELEASE-NOTES.md | 3 +- .../ComponentRegistrySerializerTest.cs | 2 +- .../EntitySerialization/EntityDeserializer.cs | 5 +- Robust.Shared/Prototypes/EntityPrototype.cs | 85 +++---------------- Robust.Shared/Prototypes/IPrototypeManager.cs | 3 + Robust.Shared/Prototypes/PrototypeManager.cs | 38 +++++---- .../ComponentRegistrySerializer.cs | 72 +++++++--------- 7 files changed, 75 insertions(+), 133 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3faf304606f..0f7aef49442 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. ### New features @@ -56,6 +56,7 @@ END TEMPLATE--> * Reduce OpenGL logging string allocations on debug for the client. * Optimise sprite sorting slightly. * Simplify and optimise Box2.Contains(Vector2) +* Optimise ComponentRegistry deserialization slightly. ## 277.1.0 diff --git a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs index 8c3eed5acaa..62c8845bf35 100644 --- a/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -25,7 +25,7 @@ internal sealed class ComponentRegistrySerializerTest : OurSerializationTest public void SerializationTest() { var component = new TestComponent(); - var registry = new ComponentRegistry {{"Test", new ComponentRegistryEntry(component, new MappingDataNode())}}; + var registry = new ComponentRegistry {{"Test", new ComponentRegistryEntry(component)}}; var node = Serialization.WriteValueAs(registry); Assert.That(node.Sequence.Count, Is.EqualTo(1)); diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index a5be87519cb..f563c49081a 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -617,8 +617,9 @@ private void LoadEntity( } var datanode = compData; - if (proto != null && proto.Components.TryGetValue(name, out var protoData)) - datanode = _seriMan.CombineMappings(compData, protoData.Mapping); + + if (proto != null && _proto.GetPrototypeData(proto).TryGetValue(name, out var protoData)) + datanode = _seriMan.CombineMappings(compData, protoData); _components.Add(name, datanode); } diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index bb73abca2b5..46494b44c46 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -12,7 +12,6 @@ using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; @@ -153,16 +152,16 @@ public sealed partial class EntityPrototype : IPrototype, IInheritingPrototype, /// /// A dictionary mapping the component type list to the YAML mapping containing their settings. /// - [DataField("components")] + [DataField] [AlwaysPushInheritance] public ComponentRegistry Components = new(); public EntityPrototype() { // Everybody gets a transform component! - Components.Add("Transform", new ComponentRegistryEntry(new TransformComponent(), new MappingDataNode())); + Components.Add("Transform", new ComponentRegistryEntry(new TransformComponent())); // And a metadata component too! - Components.Add("MetaData", new ComponentRegistryEntry(new MetaDataComponent(), new MappingDataNode())); + Components.Add("MetaData", new ComponentRegistryEntry(new MetaDataComponent())); } void ISerializationHooks.AfterDeserialization() @@ -331,8 +330,15 @@ public override string ToString() return $"EntityPrototype({ID})"; } - [DataRecord] - public partial record ComponentRegistryEntry(IComponent Component, MappingDataNode Mapping); + public sealed class ComponentRegistryEntry + { + public IComponent Component { get; } + + public ComponentRegistryEntry(IComponent component) + { + Component = component; + } + } [DataDefinition] public sealed partial class EntityPlacementProperties @@ -380,73 +386,6 @@ public HashSet SnapFlags } } } - /*private class PrototypeSerializationContext : YamlObjectSerializer.Context - { - readonly EntityPrototype? prototype; - - public PrototypeSerializationContext(EntityPrototype? owner) - { - prototype = owner; - } - - public override void SetCachedField(string field, T value) - { - if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null) - { - base.SetCachedField(field, value); - return; - } - - if (!prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var fieldList)) - { - fieldList = new Dictionary<(string, Type), object?>(); - prototype.FieldCache[prototype.CurrentDeserializingComponent] = fieldList; - } - - fieldList[(field, typeof(T))] = value; - } - - public override bool TryGetCachedField(string field, [MaybeNullWhen(false)] out T value) - { - if (StackDepth != 0 || prototype?.CurrentDeserializingComponent == null) - { - return base.TryGetCachedField(field, out value); - } - - if (prototype.FieldCache.TryGetValue(prototype.CurrentDeserializingComponent, out var dict)) - { - if (dict.TryGetValue((field, typeof(T)), out var theValue)) - { - value = (T) theValue!; - return true; - } - } - - value = default!; - return false; - } - - public override void SetDataCache(string field, object value) - { - if (StackDepth != 0 || prototype == null) - { - base.SetDataCache(field, value); - return; - } - - prototype.DataCache[field] = value; - } - - public override bool TryGetDataCache(string field, out object? value) - { - if (StackDepth != 0 || prototype == null) - { - return base.TryGetDataCache(field, out value); - } - - return prototype.DataCache.TryGetValue(field, out value); - } - }*/ } public sealed class ComponentRegistry : Dictionary, IEntityLoadContext diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index fcb5cedb18e..c0d292ec659 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -413,8 +413,11 @@ bool TryIndex( // ReSharper restore MethodOverloadWithOptionalParameter bool HasMapping(string id); + bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingDataNode? mappings); + bool TryGetMapping(string id, [NotNullWhen(true)] out MappingDataNode? mappings); + /// /// Returns whether a prototype kind exists. /// diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index e809192bc02..ac0b24efdc4 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -20,6 +20,7 @@ using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -30,7 +31,6 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal { [Dependency] private IReflectionManager _reflectionManager = default!; [Dependency] protected IResourceManager Resources = default!; - [Dependency] protected ITaskManager TaskManager = default!; [Dependency] private ISerializationManager _serializationManager = default!; [Dependency] private ILogManager _logManager = default!; [Dependency] private ILocalizationManager _locMan = default!; @@ -38,8 +38,9 @@ public abstract partial class PrototypeManager : IPrototypeManagerInternal [Dependency] private IEntityManager _entMan = default!; [Dependency] private IRobustRandom _random = default!; - private readonly Dictionary> _prototypeDataCache = new(); - private EntityDiffContext _context = new(); + private readonly Dictionary> _prototypeDataCache = new(); + + private readonly Dictionary _tempMappingData = new(); private readonly Dictionary _kindNames = new(); private readonly Dictionary _kindPriorities = new(); @@ -919,6 +920,11 @@ public bool TryGetMapping(Type kind, string id, [NotNullWhen(true)] out MappingD return _kinds[kind].Results.TryGetValue(id, out mappings); } + public bool TryGetMapping(string id, [NotNullWhen(true)] out MappingDataNode? mappings) + { + return _kinds[typeof(T)].Results.TryGetValue(id, out mappings); + } + public bool HasKind(string kind) { return _kindNames.ContainsKey(kind); @@ -1215,28 +1221,26 @@ public IReadOnlyDictionary GetPrototypeData(EntityProto if (_prototypeDataCache.TryGetValue(prototype.ID, out var data)) return data; - _context.WritingReadingPrototypes = true; - data = new(); + _tempMappingData.Clear(); - var xform = _factory.GetRegistration(typeof(TransformComponent)).Name; - try + if (TryGetMapping(prototype.ID, out var mapping) + && mapping.TryGet("components", out var components)) { - foreach (var (compType, comp) in prototype.Components) + foreach (var component in components) { - if (compType == xform) + if (component is not MappingDataNode componentMapping + || !componentMapping.TryGet("type", out var type)) + { continue; + } - var node = _serializationManager.WriteValueAs(comp.Component.GetType(), comp.Component, - alwaysWrite: true, context: _context); - data.Add(compType, node); + var copy = componentMapping.Copy(); + copy.Remove("type"); + _tempMappingData[type.Value] = copy; } } - catch (Exception e) - { - Sawmill.Error($"Failed to convert prototype {prototype.ID} into yaml. Exception: {e.Message}"); - } - _context.WritingReadingPrototypes = false; + data = _tempMappingData.ToFrozenDictionary(); _prototypeDataCache[prototype.ID] = data; return data; } diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs index 8101c34a8fe..b7e4dae9b44 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Robust.Shared.Collections; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; @@ -29,6 +30,8 @@ public ComponentRegistry Read(ISerializationManager serializationManager, { var factory = dependencies.Resolve(); var components = instanceProvider != null ? instanceProvider() : new ComponentRegistry(); + var referenceTypes = node.Count <= 1024 ? stackalloc CompIdx[node.Count] : new CompIdx[node.Count]; + var refIdx = 0; foreach (var sequenceEntry in node.Sequence) { @@ -61,29 +64,24 @@ public ComponentRegistry Read(ISerializationManager serializationManager, continue; } - var copy = componentMapping.Copy()!; - copy.Remove("type"); - - var type = factory.GetRegistration(compType).Type; - var read = (IComponent)serializationManager.Read(type, copy, hookCtx, context)!; - - components[compType] = new ComponentRegistryEntry(read, copy); - } - - var referenceTypes = new List(); - // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) - { - var registration = factory.GetRegistration(componentName); - var compType = registration.Idx; + var registration = factory.GetRegistration(compType); + var compIdx = registration.Idx; - if (referenceTypes.Contains(compType)) + if (referenceTypes[..refIdx].Contains(compIdx)) { throw new InvalidOperationException( - $"Duplicate component reference in prototype: '{compType}'"); + $"Duplicate component reference in prototype: '{compIdx}'"); } - referenceTypes.Add(compType); + referenceTypes[refIdx++] = compIdx; + + var copy = componentMapping.Copy()!; + copy.Remove("type"); + + var read = (IComponent)serializationManager.Read(registration.Type, copy, hookCtx, context)!; + + // The full YAML mapping is already retained by PrototypeManager. + components[compType] = new ComponentRegistryEntry(read); } return components; @@ -95,8 +93,10 @@ public ValidationNode Validate(ISerializationManager serializationManager, ISerializationContext? context = null) { var factory = dependencies.Resolve(); - var components = new ComponentRegistry(); + var componentNames = new HashSet(); var list = new List(); + var referenceTypes = node.Count <= 1024 ? stackalloc CompIdx[node.Count] : new CompIdx[node.Count]; + var refIdx = 0; foreach (var sequenceEntry in node.Sequence) { @@ -122,34 +122,27 @@ public ValidationNode Validate(ISerializationManager serializationManager, } // Has this type already been added? - if (components.ContainsKey(compType)) + if (!componentNames.Add(compType)) { list.Add(new ErrorNode(componentMapping, "Duplicate Component.")); continue; } - var copy = componentMapping.Copy()!; - copy.Remove("type"); - - var type = factory.GetRegistration(compType).Type; - - list.Add(serializationManager.ValidateNode(type, copy, context)); - } - - var referenceTypes = new List(); + var registration = factory.GetRegistration(compType); + var compIdx = registration.Idx; - // Assert that there are no conflicting component references. - foreach (var componentName in components.Keys) - { - var registration = factory.GetRegistration(componentName); - var compType = registration.Idx; - - if (referenceTypes.Contains(compType)) + if (referenceTypes[..refIdx].Contains(compIdx)) { - return new ErrorNode(node, "Duplicate ComponentReference."); + list.Add(new ErrorNode(componentMapping, "Duplicate ComponentReference.")); + continue; } - referenceTypes.Add(compType); + referenceTypes[refIdx++] = compIdx; + + var copy = componentMapping.Copy(); + copy.Remove("type"); + + list.Add(serializationManager.ValidateNode(registration.Type, copy, context)); } return new ValidatedSequenceNode(list); @@ -186,7 +179,8 @@ public void CopyTo(ISerializationManager serializationManager, ComponentRegistry foreach (var (id, component) in source) { - target.Add(id, serializationManager.CreateCopy(component, context, notNullableOverride: true)); + var copy = serializationManager.CreateCopy(component.Component, context, notNullableOverride: true); + target.Add(id, new ComponentRegistryEntry(copy)); } } From fcf255e11fa86713616b004db5b046cc250a8388 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 20 Jun 2026 00:51:43 +1000 Subject: [PATCH 037/178] Make Box2Rotated Transform faster (#34) --- RELEASE-NOTES.md | 1 + .../NumericsHelpers/Box2RotatedBenchmark.cs | 21 ++++++++++++++++--- Robust.Shared.Maths/Matrix3Helpers.cs | 13 ++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0f7aef49442..97198b19ce1 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -57,6 +57,7 @@ END TEMPLATE--> * Optimise sprite sorting slightly. * Simplify and optimise Box2.Contains(Vector2) * Optimise ComponentRegistry deserialization slightly. +* Optimise Box2Rotated.TransformBox slightly. ## 277.1.0 diff --git a/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs index 3515af67032..09d6d0bf302 100644 --- a/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs +++ b/Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs @@ -8,11 +8,26 @@ namespace Robust.Benchmarks.NumericsHelpers; [Virtual, DisassemblyDiagnoser] public class Box2RotatedBenchmark { - public Box2Rotated Box = new(); + public Box2Rotated Box = new(Box2.UnitCentered.Translated(new Vector2(1, 2)), Angle.FromDegrees(37), new Vector2(1, 2)); + public Matrix3x2 Matrix = Matrix3x2.CreateScale(1.5f, 0.5f) * Matrix3x2.CreateRotation(0.5f) * Matrix3x2.CreateTranslation(3, -1); + private Matrix3x2 _matrixResult; + private Box2 _boxResult; [Benchmark] - public Matrix3x2 GetTransform() + public void GetTransform() { - return Box.Transform; + _matrixResult = Box.Transform; + } + + [Benchmark(Baseline = true)] + public void TransformBoxOld() + { + _boxResult = (Box.Transform * Matrix).TransformBox(Box.Box); + } + + [Benchmark] + public void TransformBox() + { + _boxResult = Matrix.TransformBox(Box); } } diff --git a/Robust.Shared.Maths/Matrix3Helpers.cs b/Robust.Shared.Maths/Matrix3Helpers.cs index 8df6f1c2b22..0fc00780315 100644 --- a/Robust.Shared.Maths/Matrix3Helpers.cs +++ b/Robust.Shared.Maths/Matrix3Helpers.cs @@ -34,7 +34,9 @@ public static Box2Rotated TransformBounds(this Matrix3x2 refFromBox, Box2Rotated [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2Rotated box) { - return (box.Transform * refFromBox).TransformBox(box.Box); + refFromBox.TransformBox(box, out var x, out var y); + var aabb = SimdHelpers.GetAABB(x, y); + return Unsafe.As, Box2>(ref aabb); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -44,7 +46,14 @@ internal static void TransformBox( out Vector128 x, out Vector128 y) { - (box.Transform * refFromBox).TransformBox(box.Box, out x, out y); + box.GetVertices(out var boxX, out var boxY); + + x = Vector128.Create(refFromBox.M31) + + boxX * Vector128.Create(refFromBox.M11) + + boxY * Vector128.Create(refFromBox.M21); + y = Vector128.Create(refFromBox.M32) + + boxX * Vector128.Create(refFromBox.M12) + + boxY * Vector128.Create(refFromBox.M22); } public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2 box) From ead6018a6a1243f9e8b6a5f4aa56bc220c48a3c3 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 19 Jun 2026 22:49:29 +0200 Subject: [PATCH 038/178] Add markup escaping Fluent functions --- RELEASE-NOTES.md | 1 + .../LocalizationManager.Functions.cs | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index bd3d89b9a34..87a59447815 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -40,6 +40,7 @@ END TEMPLATE--> ### New features * Console commands can now be "hidden" by prefixing them with `_`. +* Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. ### Bugfixes diff --git a/Robust.Shared/Localization/LocalizationManager.Functions.cs b/Robust.Shared/Localization/LocalizationManager.Functions.cs index 82e3f292bb0..354db76939f 100644 --- a/Robust.Shared/Localization/LocalizationManager.Functions.cs +++ b/Robust.Shared/Localization/LocalizationManager.Functions.cs @@ -9,6 +9,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Maths; +using Robust.Shared.Utility; namespace Robust.Shared.Localization { @@ -42,6 +43,10 @@ private void AddBuiltInFunctions(FluentBundle bundle) AddCtxFunction(bundle, "ATTRIB", args => FuncAttrib(bundle, args)); AddCtxFunction(bundle, "CAPITALIZE", FuncCapitalize); AddCtxFunction(bundle, "INDEFINITE", FuncIndefinite); + + // Rich text + AddCtxFunction(bundle, "ESCAPE", FuncEscape); + AddCtxFunction(bundle, "ESCAPE-PARAM", FuncEscapeParam); } /// @@ -370,6 +375,24 @@ public void AddFunction(CultureInfo culture, string name, LocFunction function) bundle.AddFunctionOverriding(name, (args, options) => CallFunction(function, bundle, args, options)); } + + /// + /// Escape the provided string argument for insertion among rich text markup. + /// + private static ILocValue FuncEscape(LocArgs args) + { + var input = args.Args[0].Format(new LocContext()); + return new LocValueString(FormattedMessage.EscapeText(input)); + } + + /// + /// Escape the provided string argument for insertion as a string markup parameter. + /// + private static ILocValue FuncEscapeParam(LocArgs args) + { + var input = args.Args[0].Format(new LocContext()); + return new LocValueString(FormattedMessage.EscapeStringParameter(input)); + } } internal sealed class FluentLocWrapperType : IFluentType From a1a03d9fa1ece48604f7115102bbf106de93bc6e Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Fri, 19 Jun 2026 22:50:15 +0200 Subject: [PATCH 039/178] Allow specifying tooltip in cmdlink tag --- RELEASE-NOTES.md | 1 + Robust.Client/UserInterface/RichText/CommandLinkTag.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 87a59447815..cefc0c94cc6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -41,6 +41,7 @@ END TEMPLATE--> * Console commands can now be "hidden" by prefixing them with `_`. * Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. +* The `[cmdlink /]` tag can now have a tooltip specified with the optional "title" attribute. ### Bugfixes diff --git a/Robust.Client/UserInterface/RichText/CommandLinkTag.cs b/Robust.Client/UserInterface/RichText/CommandLinkTag.cs index 743ab15f889..78c213f3fad 100644 --- a/Robust.Client/UserInterface/RichText/CommandLinkTag.cs +++ b/Robust.Client/UserInterface/RichText/CommandLinkTag.cs @@ -36,6 +36,9 @@ public bool TryCreateControl(MarkupNode node, [NotNullWhen(true)] out Control? c label.OnMouseExited += _ => label.FontColorOverride = Color.LightBlue; label.OnKeyBindDown += args => OnKeybindDown(args, command); + if (node.Attributes.TryGetValue("title", out var titleArg)) + label.ToolTip = titleArg.StringValue; + control = label; return true; } From 30cac6ec2aeda014af9b4ec50b3669238ef84073 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 00:10:04 +0200 Subject: [PATCH 040/178] Add CommandParsing.EscapeCommand --- RELEASE-NOTES.md | 1 + .../Utility/CommandParsing_Test.cs | 17 +++++++++ Robust.Shared/Utility/CommandParsing.cs | 38 ++++++++++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index cefc0c94cc6..a863a3eb35b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -42,6 +42,7 @@ END TEMPLATE--> * Console commands can now be "hidden" by prefixing them with `_`. * Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. * The `[cmdlink /]` tag can now have a tooltip specified with the optional "title" attribute. +* Added `CommandParsing.EscapeCommand()` for *formatting* command strings easily. ### Bugfixes diff --git a/Robust.Shared.Tests/Utility/CommandParsing_Test.cs b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs index b99089f2bbe..3a9d7c213ac 100644 --- a/Robust.Shared.Tests/Utility/CommandParsing_Test.cs +++ b/Robust.Shared.Tests/Utility/CommandParsing_Test.cs @@ -33,5 +33,22 @@ public void TestEscape(string source, string expected) Assert.That(escaped, Is.EqualTo(expected)); } + + [TestCase("foo;bar")] + [TestCase("\"foo;bar")] + [TestCase("f oo;bar")] + [TestCase("f\\ oo;bar")] + public void TestEscapeCommand(string source) + { + var args = source.Split(';'); + + var result = new List(); + var escapedCommand = CommandParsing.EscapeCommand(args); + TestContext.Out.WriteLine($"Escaped command: {escapedCommand}"); + + CommandParsing.ParseArguments(escapedCommand, result); + + Assert.That(result, Is.EquivalentTo(args)); + } } } diff --git a/Robust.Shared/Utility/CommandParsing.cs b/Robust.Shared/Utility/CommandParsing.cs index a6e5e16a92c..1fd48dd5baf 100644 --- a/Robust.Shared/Utility/CommandParsing.cs +++ b/Robust.Shared/Utility/CommandParsing.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Text; using Robust.Shared.Collections; @@ -7,6 +8,8 @@ namespace Robust.Shared.Utility { public static class CommandParsing { + private static readonly SearchValues CommandArgumentSeparator = SearchValues.Create(" "); + /// /// Parses a full console command into a list of arguments. /// @@ -63,7 +66,7 @@ internal static void ParseArguments( continue; } - if (chr == ' ' && !inQuotes) + if (CommandArgumentSeparator.Contains(chr) && !inQuotes) { if (sb.Length != 0) { @@ -93,5 +96,38 @@ public static string Escape(string text) { return text.Replace("\\", "\\\\").Replace("\"", "\\\""); } + + /// + /// Split a set of arguments into a string that can be parsed round-trip. + /// + /// + /// + /// This is effectively the inverse of . + /// + /// + public static string EscapeCommand(params string[] arguments) + { + var sb = new StringBuilder(); + + var first = true; + + foreach (var entry in arguments) + { + if (!first) + sb.Append(' '); + first = false; + + var quoted = entry.ContainsAny(CommandArgumentSeparator); + if (quoted) + sb.Append('"'); + + sb.Append(Escape(entry)); + + if (quoted) + sb.Append('"'); + } + + return sb.ToString(); + } } } From a4f42755727580ed9fafa04c3a09bbb5a956706e Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 00:21:09 +0200 Subject: [PATCH 041/178] Add IUserInterfaceManager.GetRootForMouse and Popup.OpenAtCursor --- RELEASE-NOTES.md | 2 ++ Robust.Client/UserInterface/Controls/Popup.cs | 29 +++++++++++++++++++ .../UserInterface/IUserInterfaceManager.cs | 8 +++++ .../UserInterface/UserInterfaceManager.cs | 15 ++++++++++ 4 files changed, 54 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a863a3eb35b..00d92d34ef3 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,6 +43,8 @@ END TEMPLATE--> * Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. * The `[cmdlink /]` tag can now have a tooltip specified with the optional "title" attribute. * Added `CommandParsing.EscapeCommand()` for *formatting* command strings easily. +* Added `IUserInterfaceManager.GetRootForMouse()`. +* Added `Popup.OpenAtCursor()`. ### Bugfixes diff --git a/Robust.Client/UserInterface/Controls/Popup.cs b/Robust.Client/UserInterface/Controls/Popup.cs index 2b4f363ea7e..a2223b3d40d 100644 --- a/Robust.Client/UserInterface/Controls/Popup.cs +++ b/Robust.Client/UserInterface/Controls/Popup.cs @@ -30,6 +30,29 @@ public Popup() public bool CloseOnEscape { get; set; } = true; + private bool _autoOrphan; + + /// + /// Opens the popup at the location of the mouse. + /// + /// + /// + /// The popup is placed in the modal root, and is automatically sized. + /// + /// + /// The popup *automatically* gets removed from the popup root when it is hidden again. + /// Do not remove it manually! + /// + /// + public void OpenAtMouse() + { + _autoOrphan = true; + var root = UserInterfaceManager.GetRootForMouse(); + root.ModalRoot.AddChild(this); + + Open(UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position, Vector2.One)); + } + public virtual void Open(UIBox2? box = null, Vector2? altPos = null, Vector2? altPosUp = null) { if (Visible) @@ -72,6 +95,12 @@ protected internal override void ModalRemoved() Visible = false; OnPopupHide?.Invoke(); + + if (_autoOrphan) + { + Orphan(); + _autoOrphan = false; + } } protected override Vector2 MeasureOverride(Vector2 availableSize) diff --git a/Robust.Client/UserInterface/IUserInterfaceManager.cs b/Robust.Client/UserInterface/IUserInterfaceManager.cs index 0ad4eb29997..271b2df7485 100644 --- a/Robust.Client/UserInterface/IUserInterfaceManager.cs +++ b/Robust.Client/UserInterface/IUserInterfaceManager.cs @@ -167,6 +167,14 @@ public partial interface IUserInterfaceManager /// Exists so that control don't have to inject dependencies or otherwise obtain an instance just to log errors. /// ISawmill ControlSawmill { get; } + + /// + /// Get the UI root responsible for the current mouse position. + /// + /// + /// This is useful to open popups or similar on the current active window. + /// + UIRoot GetRootForMouse(); } public readonly struct PostDrawUIRootEventArgs diff --git a/Robust.Client/UserInterface/UserInterfaceManager.cs b/Robust.Client/UserInterface/UserInterfaceManager.cs index 942e2f5ffef..57c86c35185 100644 --- a/Robust.Client/UserInterface/UserInterfaceManager.cs +++ b/Robust.Client/UserInterface/UserInterfaceManager.cs @@ -492,5 +492,20 @@ public void HoverSound() { ClearWindows(); } + + public UIRoot GetRootForMouse() + { + var pos = _inputManager.MouseScreenPosition; + + foreach (var root in _roots) + { + if (root.Window.Id == pos.Window) + { + return root; + } + } + + return RootControl; + } } } From fbab67805807787c13695a96ca746ba5dfaf0d30 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 03:36:49 +0200 Subject: [PATCH 042/178] Add `FormattedStringBuilder` for safely constructing markup with code. --- RELEASE-NOTES.md | 1 + .../RichText/FormattedStringBuilderTest.cs | 255 ++++++++++++ .../RichText/FormattedStringBuilder.cs | 367 ++++++++++++++++++ .../FormattedStringBuilderExtensions.cs | 28 ++ 4 files changed, 651 insertions(+) create mode 100644 Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs create mode 100644 Robust.Shared/RichText/FormattedStringBuilder.cs create mode 100644 Robust.Shared/RichText/FormattedStringBuilderExtensions.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 00d92d34ef3..2dd80c8ec5f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -45,6 +45,7 @@ END TEMPLATE--> * Added `CommandParsing.EscapeCommand()` for *formatting* command strings easily. * Added `IUserInterfaceManager.GetRootForMouse()`. * Added `Popup.OpenAtCursor()`. +* Added `FormattedStringBuilder` for safely constructing markup with code. ### Bugfixes diff --git a/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs b/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs new file mode 100644 index 00000000000..431cd925222 --- /dev/null +++ b/Robust.Shared.Tests/RichText/FormattedStringBuilderTest.cs @@ -0,0 +1,255 @@ +using System.Text; +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.RichText; +using Robust.Shared.Utility; + +namespace Robust.UnitTesting.Shared.RichText; + +[Parallelizable(ParallelScope.All)] +[TestFixture, TestOf(typeof(FormattedStringBuilder))] +public static class FormattedStringBuilderTest +{ + [Test] + public static void TestPlainText() + { + var fsb = new FormattedStringBuilder(); + fsb.AppendText("Foobar"); + fsb.AppendLine(); + fsb.AppendMarkup("Wawa"); + + AssertMarkup(fsb, "Foobar\nWawa"); + } + + [Test] + public static void TestPlainTextExistingBuilder() + { + var sb = new StringBuilder(); + sb.Append("Guh"); + var fsb = new FormattedStringBuilder(sb); + fsb.AppendText("Foobar"); + fsb.AppendLine(); + fsb.AppendMarkup("Wawa"); + + AssertMarkup(fsb, "GuhFoobar\nWawa"); + } + + [Test] + public static void TestBasicTag() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagOpen(); + + fsb.AppendText("bar"); + fsb.PopTag(); + + AssertMarkup(fsb, "[bold]bar[/bold]"); + } + + [Test] + public static void TestBasicTagFormattedString() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagOpen(); + + fsb.AppendText("bar"); + fsb.PopTag(); + + Assert.That((FormattedMessage)fsb.ToFormattedString(), Is.EqualTo(FormattedMessage.FromMarkupOrThrow("[bold]bar[/bold]"))); + } + + [Test] + public static void TestSelfClosingTag() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold"); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold /]"); + } + + [Test] + public static void TestTagValueLong() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", 10); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=10 /]"); + } + + [Test] + public static void TestTagValueString() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", "wawa"); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=\"wawa\" /]"); + } + + [Test] + public static void TestTagValueColor() + { + var fsb = new FormattedStringBuilder(); + fsb.BeginTag("bold", Color.FromHex("#AAA")); + fsb.FinishTagSelfClosed(); + + AssertMarkup(fsb, "[bold=#AAA /]"); + } + + [Test] + public static void TestTagAttributeString() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", "b").FinishTagSelfClosed(), + "[bold a=\"b\" /]"); + } + + [Test] + public static void TestTagAttributeLong() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", 10).FinishTagSelfClosed(), + "[bold a=10 /]"); + } + + [Test] + public static void TestTagAttributeColor() + { + AssertMarkup( + Fsb().BeginTag("bold").TagAttribute("a", Color.FromHex("#AAA")).FinishTagSelfClosed(), + "[bold a=#AAA /]"); + } + + [Test] + public static void TestAppendMarkup() + { + AssertMarkup( + Fsb().AppendMarkup("[bold /]"), + "[bold /]"); + } + + [Test] + public static void TestAppendMarkupLine() + { + AssertMarkup( + Fsb().AppendMarkupLine("[bold /]"), + "[bold /]\n"); + } + + [Test] + public static void TestBeginInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.BeginTag("a"), Throws.TypeOf()); + } + + [Test] + public static void TestBeginValueInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.BeginTag("a", "b"), Throws.TypeOf()); + } + + [Test] + public static void TestTagAttributeInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.TagAttribute("a", "b"), Throws.TypeOf()); + } + + [Test] + public static void TestFinishTagSelfClosedInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.FinishTagSelfClosed(), Throws.TypeOf()); + } + + [Test] + public static void TestFinishTagOpenInvalid() + { + var fsb = Fsb(); + + Assert.That(() => fsb.FinishTagOpen(), Throws.TypeOf()); + } + + [Test] + public static void TestPopTagEmpty() + { + var fsb = Fsb(); + + Assert.That(() => fsb.PopTag(), Throws.TypeOf()); + } + + [Test] + public static void TestPopTagInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.PopTag(), Throws.TypeOf()); + } + + [Test] + public static void TestAppendTextInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendText("A"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendMarkup("A"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupInvalidMarkup() + { + var fsb = Fsb(); + + Assert.That(() => fsb.AppendMarkup("[wawa"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendLineInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendLine(), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupLineInvalid() + { + var fsb = Fsb().BeginTag("a"); + + Assert.That(() => fsb.AppendMarkupLine("guh"), Throws.TypeOf()); + } + + [Test] + public static void TestAppendMarkupLineInvalidMarkup() + { + var fsb = Fsb(); + + Assert.That(() => fsb.AppendMarkupLine("[guh"), Throws.TypeOf()); + } + + private static void AssertMarkup(FormattedStringBuilder fsb, string expected) + { + Assert.That( + FormattedMessage.FromMarkupOrThrow(fsb.ToString()), + Is.EqualTo(FormattedMessage.FromMarkupOrThrow(expected))); + } + + private static FormattedStringBuilder Fsb() => new FormattedStringBuilder(); +} diff --git a/Robust.Shared/RichText/FormattedStringBuilder.cs b/Robust.Shared/RichText/FormattedStringBuilder.cs new file mode 100644 index 00000000000..0f059dd8b26 --- /dev/null +++ b/Robust.Shared/RichText/FormattedStringBuilder.cs @@ -0,0 +1,367 @@ +using System; +using System.Text; +using Robust.Shared.Collections; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Shared.RichText; + +/// +/// A wrapper around , with convenience methods for safely constructing rich text markup. +/// +/// +/// +/// Tags are written with multiple consecutive calls. Functions may throw if not in the right state, +/// and this can be checked with . +/// It should go without saying that care must be taken to use the underlying +/// while this is the case. +/// +/// +/// While the underlying is accessible, you are of course responsible for writing valid +/// markup and escaping if necessary. +/// +/// +public sealed class FormattedStringBuilder +{ + private ValueList _tagStack; + + /// + /// The underlying used by this instance. + /// + /// + /// you are responsible for writing valid markup and escaping where necessary, if you access this property. + /// + public StringBuilder Builder { get; } + + /// + /// If true, we are currently writing a tag. + /// + /// + /// This can be ended through or . + /// + public bool IsInsideTag { get; private set; } = true; + + /// + /// Create a new builder with an empty underlying . + /// + public FormattedStringBuilder() : this(new StringBuilder()) + { + + } + + /// + /// Create a new builder wrapping an existing . + /// + /// + /// The provided instance is not initially mutated. + /// + public FormattedStringBuilder(StringBuilder builder) + { + Builder = builder; + } + + /// + /// Begin a new tag with the specified name. + /// + /// The name of the tag to begin. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName) + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot begin tag: we're already in a tag"); + + _tagStack.Push(tagName); + IsInsideTag = false; + Builder.Append($"[{tagName}"); + + return this; + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, MarkupParameter value) + { + BeginTag(tagName); + + Builder.Append(value.ToString()); + + return this; + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, string value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, long value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Begin a new tag with the specified name and a value. + /// + /// The name of the tag to begin. + /// The value of the markup tag. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're already inside a tag. + /// + public FormattedStringBuilder BeginTag(string tagName, Color value) + { + return BeginTag(tagName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, MarkupParameter value) + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot write attribute: we aren't in a tag!"); + + Builder.Append($" {attributeName}{value}"); + return this; + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, string value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, long value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Specify an attribute for the tag currently being written. + /// + /// + /// This does not check for duplicates. + /// + /// The name of the attribute to write. + /// The value of the attribute. + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder TagAttribute(string attributeName, Color value) + { + return TagAttribute(attributeName, new MarkupParameter(value)); + } + + /// + /// Finish writing the current tag as self-closed. + /// + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder FinishTagSelfClosed() + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot finish tag: we aren't in a tag!"); + + _tagStack.Pop(); + Builder.Append("/]"); + IsInsideTag = true; + return this; + } + + /// + /// Finish writing the current tag as open. You will have to close it with . + /// + /// The current instance, to enable easy method call chaining. + /// + /// Thrown if we're not currently inside a tag. + /// + public FormattedStringBuilder FinishTagOpen() + { + if (IsInsideTag) + throw new InvalidOperationException("Cannot finish tag: we aren't in a tag!"); + + Builder.Append(']'); + IsInsideTag = true; + return this; + } + + /// + /// Write a closing tag for the most recent open tag. + /// + /// + /// The stack of open tags (from ) is automatically tracked. + /// + /// The current instance, to enable easy method call chaining. + /// + public FormattedStringBuilder PopTag() + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot begin tag: we're already in a tag"); + + Builder.Append($"[/{_tagStack.Pop()}]"); + return this; + } + + /// + /// Append plain text. + /// + /// The text to append without interpreting formatting. + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendText(string text) + { + CheckSafe(); + Builder.Append(FormattedMessage.EscapeText(text)); + return this; + } + + /// + /// Append markup. + /// + /// The text to append as markup. + /// + /// Thrown if is not valid markup. + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendMarkup(string markup) + { + CheckSafe(); + + if (!FormattedMessage.ValidMarkup(markup)) + throw new ArgumentException("Not valid markup!", nameof(markup)); + + Builder.Append(markup); + return this; + } + + /// + /// Append markup, followed by a newline. + /// + /// + /// The added line is always a single Line Feed (LF), not . + /// + /// The text to append as markup. + /// + /// Thrown if is not valid markup. + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendMarkupLine(string markup) + { + AppendMarkup(markup); + AppendLine(); + return this; + } + + /// + /// Append a newline. + /// + /// + /// The added line is always a single Line Feed (LF), not . + /// + /// + /// Thrown if we're currently inside a tag. + /// + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder AppendLine() + { + CheckSafe(); + Builder.Append('\n'); + return this; + } + + /// + /// Returns the internal value as a string. + /// + public override string ToString() + { + return Builder.ToString(); + } + + /// + /// Returns the internal value as a . + /// + /// + /// Thrown if the contained string is not valid markup + /// (e.g. if you manually messed with the underlying .) + /// + public FormattedString ToFormattedString() + { + return FormattedString.FromMarkup(ToString()); + } + + private void CheckSafe() + { + if (!IsInsideTag) + throw new InvalidOperationException("Cannot append: we are currently writing a tag."); + } +} diff --git a/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs b/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs new file mode 100644 index 00000000000..6449f9c054f --- /dev/null +++ b/Robust.Shared/RichText/FormattedStringBuilderExtensions.cs @@ -0,0 +1,28 @@ +namespace Robust.Shared.RichText; + +/// +/// Extension methods for . +/// +public static class FormattedStringBuilderExtensions +{ + extension(FormattedStringBuilder builder) + { + /// + /// Write a cmdlink tag. + /// + /// The user-visible tag for the link. + /// The command executed when the user clicks. + /// The tooltip (title) when the user hovers over the link. + /// The current instance, to enable easy method call chaining. + public FormattedStringBuilder MakeCommandLinkTag(string text, string command, string? title = null) + { + builder.BeginTag("cmdlink", text); + builder.TagAttribute("command", command); + if (title != null) + builder.TagAttribute("title", title); + builder.FinishTagSelfClosed(); + + return builder; + } + } +} From 1a09f17dadd98610217503bb8a83b3dc612c0228 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Thu, 7 May 2026 17:05:38 +0200 Subject: [PATCH 043/178] Add HBox and VBox convenience types. --- RELEASE-NOTES.md | 1 + Robust.Client/UserInterface/Controls/HBox.cs | 6 ++++++ Robust.Client/UserInterface/Controls/VBox.cs | 12 ++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 Robust.Client/UserInterface/Controls/HBox.cs create mode 100644 Robust.Client/UserInterface/Controls/VBox.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 2dd80c8ec5f..b51b1c725b3 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -46,6 +46,7 @@ END TEMPLATE--> * Added `IUserInterfaceManager.GetRootForMouse()`. * Added `Popup.OpenAtCursor()`. * Added `FormattedStringBuilder` for safely constructing markup with code. +* Added `VBox` and `HBox` convenience types for more concisely construct `BoxContainer`s. ### Bugfixes diff --git a/Robust.Client/UserInterface/Controls/HBox.cs b/Robust.Client/UserInterface/Controls/HBox.cs new file mode 100644 index 00000000000..c9ce4110c07 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/HBox.cs @@ -0,0 +1,6 @@ +namespace Robust.Client.UserInterface.Controls; + +/// +/// Convenience type to describe a horizontal . +/// +public sealed class HBox : BoxContainer; diff --git a/Robust.Client/UserInterface/Controls/VBox.cs b/Robust.Client/UserInterface/Controls/VBox.cs new file mode 100644 index 00000000000..1476f749b65 --- /dev/null +++ b/Robust.Client/UserInterface/Controls/VBox.cs @@ -0,0 +1,12 @@ +namespace Robust.Client.UserInterface.Controls; + +/// +/// Convenience type to describe a vertical . +/// +public sealed class VBox : BoxContainer +{ + public VBox() + { + Orientation = LayoutOrientation.Vertical; + } +} From c1919263f4604ad306a95a11f190d0e8ce3c7b4f Mon Sep 17 00:00:00 2001 From: Axionyx Date: Sat, 20 Jun 2026 20:00:38 +0200 Subject: [PATCH 044/178] Track isLocal in user data (#6641) --- Robust.Shared/Network/NetManager.ServerAuth.cs | 6 ++++-- Robust.Shared/Network/NetUserData.cs | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index f11458d669c..175fbfe6f69 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -179,7 +179,8 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) HWId = legacyHwid, ModernHWIds = modernHWIds, Trust = joinedRespJson.ConnectionData!.Trust, - CreatedTime = joinedRespJson.UserData.CreatedTime + CreatedTime = joinedRespJson.UserData.CreatedTime, + IsLocal = isLocal }; padSuccessMessage = false; type = LoginType.LoggedIn; @@ -223,7 +224,8 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) userData = new NetUserData(userId, name) { HWId = [], - ModernHWIds = [] + ModernHWIds = [], + IsLocal = isLocal }; } diff --git a/Robust.Shared/Network/NetUserData.cs b/Robust.Shared/Network/NetUserData.cs index 7ee61388e9a..1aa7aa25a87 100644 --- a/Robust.Shared/Network/NetUserData.cs +++ b/Robust.Shared/Network/NetUserData.cs @@ -41,6 +41,11 @@ public sealed record NetUserData /// public float Trust { get; init; } + /// + /// True if the player is connecting from a local address. + /// + public bool IsLocal { get; init; } + public NetUserData(NetUserId userId, string userName) { UserId = userId; From f41b2d5faf3ce6ff0b0ab06cafcf46a47dc753c6 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 20:01:18 +0200 Subject: [PATCH 045/178] bump natives to 0.2.5 --- Directory.Packages.props | 2 +- RELEASE-NOTES.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 69000ce2613..92e4a7258df 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -48,7 +48,7 @@ - + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b51b1c725b3..055f19eb795 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -55,6 +55,7 @@ END TEMPLATE--> ### Other * Add Pure attributes to the EntityLookup bounds methods +* Bump `Robust.Natives` to `0.2.5`. ### Internal From 24da77408e250d9ac67f0b08cfcf204189b655de Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 20:06:10 +0200 Subject: [PATCH 046/178] Release notes for c1919263f4604ad306a95a11f190d0e8ce3c7b4f --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 055f19eb795..db5e6f6ced6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,6 +47,7 @@ END TEMPLATE--> * Added `Popup.OpenAtCursor()`. * Added `FormattedStringBuilder` for safely constructing markup with code. * Added `VBox` and `HBox` convenience types for more concisely construct `BoxContainer`s. +* Added `IsLocal` to `NetUserData`. ### Bugfixes From 8af60684da330d5abc83685097f6b074eec967a0 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 20:34:27 +0200 Subject: [PATCH 047/178] Update release notes --- RELEASE-NOTES.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index db5e6f6ced6..66742c6e5a1 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -48,19 +48,29 @@ END TEMPLATE--> * Added `FormattedStringBuilder` for safely constructing markup with code. * Added `VBox` and `HBox` convenience types for more concisely construct `BoxContainer`s. * Added `IsLocal` to `NetUserData`. +* Add `SharedMapSystem.GetFilledTileCount()` +* Add a `track` style property for scroll bars. +* Added `ScrollLock` key. ### Bugfixes -*None yet* +* Fixes override properties in `WrapContainer` not being respected properly. +* Fix `BoxContainer.SeparationOverride` not being respected properly and not invalidating layout. +* Fixed swapped arguments being passed through in various `FindGridsIntersecting` overloads. +* Fixed a doc comment in `LocalizationManager`. ### Other * Add Pure attributes to the EntityLookup bounds methods * Bump `Robust.Natives` to `0.2.5`. +* Minor performance improvement in `IsHardCollidable()`. +* Remove an outdated paragraph from `[DependencyAttribute]` documentation related to `readonly` fields. +* More stock controls use alternative cursor shapes where appropriate. +* Minor performance improvement to audio loading. ### Internal -*None yet* +* Added `.lscache` to `.gitignore`. ## 277.0.0 From 7cfce436333f185333c64dcabb2529e40f1c9f50 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Sat, 20 Jun 2026 20:34:37 +0200 Subject: [PATCH 048/178] Version: 277.1.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index bac96a1d378..e366ed6a67f 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.0.0 + 277.1.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 66742c6e5a1..255d6b4afb0 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,25 @@ END TEMPLATE--> ### New features +*None yet* + +### Bugfixes + +*None yet* + +### Other + +*None yet* + +### Internal + +*None yet* + + +## 277.1.0 + +### New features + * Console commands can now be "hidden" by prefixing them with `_`. * Add `ESCAPE()` and `ESCAPE-PARAM()` localization functions, for escaping text for markup formatting. * The `[cmdlink /]` tag can now have a tooltip specified with the optional "title" attribute. From 4f15631623404e6b2a9013970bcc44fb7f396528 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:06:19 +0200 Subject: [PATCH 049/178] fix publish-client.yml web edit go --- .github/workflows/publish-client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index 4767740a2ff..6708205b296 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -36,7 +36,7 @@ jobs: - name: Upload files to Suns uses: appleboy/scp-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} source: "release/${{ steps.parse_version.outputs.version }}" From d0decda59d355104f54966db8fff6ca78e0d47a6 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:07:58 +0200 Subject: [PATCH 050/178] fix it actually this time my hubris --- .github/workflows/publish-client.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index 6708205b296..f9b36cb903a 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -46,7 +46,7 @@ jobs: - name: Update manifest JSON uses: appleboy/ssh-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }} From a80e7869238a801c175aec05fc4e55a12fd3d816 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sat, 20 Jun 2026 13:13:36 -0700 Subject: [PATCH 051/178] Replace default Auth as well as README and other links (#49) Co-authored-by: Tayrtahn --- .github/workflows/publish-client.yml | 2 +- README.md | 14 +++++++------- Robust.Client.WebView/TestBrowseWindow.cs | 2 +- Robust.Server/server_config.toml | 4 ++-- Robust.Shared/CVars.cs | 2 +- Robust.Shared/Network/AuthManager.cs | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index f9b36cb903a..5319e4bec30 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -33,7 +33,7 @@ jobs: mkdir "release/${{ steps.parse_version.outputs.version }}" mv release/*.zip "release/${{ steps.parse_version.outputs.version }}" - - name: Upload files to Suns + - name: Upload files to Dropwig uses: appleboy/scp-action@master with: host: dropwig.playss14.com diff --git a/README.md b/README.md index a8d9fc8c08e..dc52da21198 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -![Robust Toolbox](https://raw.githubusercontent.com/space-wizards/asset-dump/3dd3078e49e3a7e06709a6e0fc6e3223d8d44ca2/robust.png) +![Robust Toolbox](https://raw.githubusercontent.com/Space-Wizards-Federation/asset-dump/3dd3078e49e3a7e06709a6e0fc6e3223d8d44ca2/robust.png) -Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/space-wizards/space-station-14), although we're working on making it usable for both [singleplayer](https://github.com/space-wizards/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/space-wizards/RobustToolboxTemplate) projects. +Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/Space-Wizards-Federation/space-station-14/), although we're working on making it usable for both [singleplayer](https://github.com/Space-Wizards-Federation/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/Space-Wizards-Federation/RobustToolboxTemplate/) projects. -Use the [content repo](https://github.com/space-wizards/space-station-14) for actual development, even if you're modifying the engine itself. +Use the [content repo](https://github.com/Space-Wizards-Federation/space-station-14) for actual development, even if you're modifying the engine itself. ## Project Links -[Website](https://spacestation14.io/) | [Discord](https://discord.gg/t2jac3p) | [Forum](https://forum.spacestation14.io/) | [Steam](https://store.steampowered.com/app/1255460/Space_Station_14/) | [Standalone Download](https://spacestation14.io/about/nightlies/) +[Website](https://playss14.com//) | [Discord](https://discord.gg/ss14) | [Standalone Download](https://playss14.com/about/download/) ## Documentation/Wiki @@ -14,12 +14,12 @@ The [wiki](https://docs.spacestation14.io/) has documentation on SS14s content, ## Contributing -We are happy to accept contributions from anybody. Get in Discord or IRC if you want to help. We've got a [list of issues](https://github.com/space-wizards/RobustToolbox/issues) that need to be done and anybody can pick them up. Don't be afraid to ask for help either! +We are happy to accept contributions from anybody. Get in Discord or IRC if you want to help. We've got a [list of issues](https://github.com/Space-Wizards-Federation/RobustToolbox/issues/) that need to be done and anybody can pick them up. Don't be afraid to ask for help either! ## Building -This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Think of Robust Toolbox as BYOND in the context of Space Station 13. +This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/Space-Wizards-Federation/space-station-14). Think of Robust Toolbox as BYOND in the context of Space Station 13. ## Legal Info -See [legal.md](https://github.com/space-wizards/RobustToolbox/blob/master/legal.md) for licenses and copyright. +See [legal.md](https://github.com/Space-Wizards-Federation/RobustToolbox/blob/master/legal.md) for licenses and copyright. diff --git a/Robust.Client.WebView/TestBrowseWindow.cs b/Robust.Client.WebView/TestBrowseWindow.cs index 3c73754ef76..4cab70562bc 100644 --- a/Robust.Client.WebView/TestBrowseWindow.cs +++ b/Robust.Client.WebView/TestBrowseWindow.cs @@ -23,7 +23,7 @@ internal sealed class TestBrowseWindowCommand : LocalizedCommands public override void Execute(IConsoleShell shell, string argStr, string[] args) { - var url = args.Length > 0 ? args[0] : "https://spacestation14.com"; + var url = args.Length > 0 ? args[0] : "https://playss14.com"; new TestBrowseWindow(url).Open(); } diff --git a/Robust.Server/server_config.toml b/Robust.Server/server_config.toml index 44a50e58729..f68797bed3a 100644 --- a/Robust.Server/server_config.toml +++ b/Robust.Server/server_config.toml @@ -52,7 +52,7 @@ tags = "" # Must be in the form of an ss14:// or ss14s:// URI pointing to the status API. server_url = "" # Comma-separated list of URLs of hub servers to advertise to. -hub_urls = "https://hub.spacestation14.com/" +hub_urls = "https://hub.playss14.com/" [build] # *Absolutely all of these can be supplied using a "build.json" file* @@ -98,5 +98,5 @@ hub_urls = "https://hub.spacestation14.com/" # You should probably never EVER need to touch this, but if you need a custom auth server, # (the auth server being the one which manages Space Station 14 accounts), you change it here. -# server = https://auth.spacestation14.com/ +# server = "https://auth.playss14.com/" diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index fa1608e43ba..b1eaac8f204 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -1613,7 +1613,7 @@ protected CVars() /// Comma-separated list of URLs of hub servers to advertise to. /// public static readonly CVarDef HubUrls = - CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY); + CVarDef.Create("hub.hub_urls", "https://hub.playss14.com/", CVar.SERVERONLY); /// /// URL of this server to advertise. diff --git a/Robust.Shared/Network/AuthManager.cs b/Robust.Shared/Network/AuthManager.cs index ea2250e891f..3fd92993177 100644 --- a/Robust.Shared/Network/AuthManager.cs +++ b/Robust.Shared/Network/AuthManager.cs @@ -25,7 +25,7 @@ internal interface IAuthManager internal sealed class AuthManager : IAuthManager { - public const string DefaultAuthServer = "https://auth.spacestation14.com/"; + public const string DefaultAuthServer = "https://auth.playss14.com/"; public NetUserId? UserId { get; set; } public string? Server { get; set; } = DefaultAuthServer; From 5718e9e3853510adb0a28a532b9306bbc54539e2 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sat, 20 Jun 2026 16:19:33 -0400 Subject: [PATCH 052/178] Release notes --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 97198b19ce1..0884f6585c6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,7 +47,7 @@ END TEMPLATE--> ### Other -*None yet* +* Changed default auth server URL. ### Internal From 3c77bfde67f2f8c719a7589dcb0d378deb8f4dca Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sat, 20 Jun 2026 16:20:44 -0400 Subject: [PATCH 053/178] Version: 277.2.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index fcfd5b39855..bcc58d4ae73 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.1.0 + 277.2.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0884f6585c6..38b2d75884e 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,6 +35,29 @@ END TEMPLATE--> ### Breaking changes +*None yet* + +### New features + +*None yet* + +### Bugfixes + +*None yet* + +### Other + +*None yet* + +### Internal + +*None yet* + + +## 277.2.0 + +### Breaking changes + * Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. ### New features From e568c7ba7801c3d001f9d059cdc93dbc96482db7 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:08:30 +1000 Subject: [PATCH 054/178] LocalRotation normalize + obsolete setter (#125) --- RELEASE-NOTES.md | 1 + .../GameObjects/Components/Transform_Test.cs | 15 ++++++++ .../Transform/TransformComponent.cs | 3 ++ .../SharedTransformSystem.Component.cs | 34 ++++++++++++++++--- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 38b2d75884e..c6a5fa90c2b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -59,6 +59,7 @@ END TEMPLATE--> ### Breaking changes * Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. +* Obsolete LocalRotation in favor of the system method. The angle is now also normalized to 2PI and no longer grows indefinitely. ### New features diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs index 95b857bb95e..14f54d46477 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs @@ -439,6 +439,21 @@ public void WorldRotationTest() Assert.That(result, new ApproxEqualityConstraint(Angle.FromDegrees(225))); } + [Test] + public void LocalRotationNormalizesTest() + { + var entity = EntityManager.SpawnEntity(null, InitialPos); + var transform = EntityManager.GetComponent(entity); + + XformSystem.SetLocalRotation(entity, Angle.FromDegrees(90), transform); + + Assert.That(transform.LocalRotation, NUnit.Framework.Is.EqualTo(Angle.FromDegrees(90))); + + XformSystem.SetWorldRotation(transform, Angle.FromDegrees(810)); + + Assert.That(transform.LocalRotation, NUnit.Framework.Is.EqualTo(Angle.FromDegrees(90))); + } + /// /// Test that, in a chain A -> B -> C, if A is moved C's world position correctly updates. /// diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index f2e132d06cd..bb0d20aa20b 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -153,11 +153,14 @@ public bool NoLocalRotation public Angle LocalRotation { get => _localRotation; + [Obsolete("Use SharedTransformSystem.SetLocalRotation")] set { if(_noLocalRotation) return; + value = SharedTransformSystem.NormalizeRotation(value); + if (_localRotation.EqualsApprox(value)) return; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 2fa10c984a6..cdeeaf29c81 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -45,7 +45,7 @@ internal void ReAnchor( var oldRot = xform._localRotation; var oldMap = xform.MapUid; xform._localPosition = tilePos + newGrid.TileSizeHalfVector; - xform._localRotation += rotation; + xform._localRotation = NormalizeRotation(xform._localRotation + rotation); var meta = MetaData(uid); SetGridId((uid, xform, meta), newGridUid); @@ -437,6 +437,11 @@ public void SetLocalPositionNoLerp(EntityUid uid, Vector2 value, TransformCompon #region Local Rotation + internal static Angle NormalizeRotation(Angle rotation) + { + return rotation.Reduced(); + } + public void SetLocalRotationNoLerp(EntityUid uid, Angle value, TransformComponent? xform = null) { if (!XformQuery.Resolve(uid, ref xform)) @@ -457,6 +462,27 @@ public void SetLocalRotation(TransformComponent xform, Angle value) #endregion + #region No Local Rotation + + public void SetNoLocalRotation(EntityUid uid, bool value, TransformComponent? xform = null) + { + if (!XformQuery.Resolve(uid, ref xform)) + return; + + SetNoLocalRotation((uid, xform), value); + } + + public void SetNoLocalRotation(Entity entity, bool value) + { + if (value) + SetLocalRotation(entity.Owner, Angle.Zero, entity.Comp); + + entity.Comp._noLocalRotation = value; + Dirty(entity); + } + + #endregion + #region Coordinates public void SetCoordinates(EntityUid uid, EntityCoordinates value) @@ -519,7 +545,7 @@ public void SetCoordinates( xform._localPosition = value.Position; if (rotation != null && !xform.NoLocalRotation) - xform._localRotation = rotation.Value; + xform._localRotation = NormalizeRotation(rotation.Value); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); @@ -621,7 +647,7 @@ public void SetCoordinates( { // preserve world rotation if (rotation == null && oldParent != null && newParent != null && !xform.NoLocalRotation) - xform._localRotation += GetWorldRotation(oldParent) - GetWorldRotation(newParent); + xform._localRotation = NormalizeRotation(xform._localRotation + GetWorldRotation(oldParent) - GetWorldRotation(newParent)); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); } @@ -1277,7 +1303,7 @@ public virtual void SetLocalPositionRotation(EntityUid uid, Vector2 pos, Angle r xform._localPosition = pos; if (!xform.NoLocalRotation) - xform._localRotation = rot; + xform._localRotation = NormalizeRotation(rot); DebugTools.Assert(!xform.NoLocalRotation || xform.LocalRotation == 0); From bec88c87a77b3051d920c66a0cc76fb3ad43eec6 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:08:51 +0000 Subject: [PATCH 055/178] Add NotNullWhenTrue to EntityPrototype.TryComp factory overload (#132) Co-authored-by: deltanedas <@deltanedas:goida.zip> --- Robust.Shared/Prototypes/EntityPrototype.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 46494b44c46..8cf4d7098f9 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -193,7 +193,7 @@ void ISerializationHooks.AfterDeserialization() /// [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryComp(out T? component, IComponentFactory factory) where T : IComponent, new() + public bool TryComp([NotNullWhen(true)] out T? component, IComponentFactory factory) where T : IComponent, new() => TryComp(factory.CompName(), out component); /// From 931b2a0989f08ba18274f371fde5cd6b611bef2a Mon Sep 17 00:00:00 2001 From: Axionyx Date: Sun, 21 Jun 2026 22:05:22 +0200 Subject: [PATCH 056/178] Add trust scores for localhost@ and guest@ connections (#6642) Add trust scores for localhost@ and guest@ connections, configurable via cvar --- Robust.Shared/CVars.cs | 12 ++++++++++++ Robust.Shared/Network/NetManager.ServerAuth.cs | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index fa1608e43ba..d4c67e3998d 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -996,6 +996,18 @@ protected CVars() public static readonly CVarDef AuthServer = CVarDef.Create("auth.server", AuthManager.DefaultAuthServer, CVar.SERVERONLY); + /// + /// Trust score for unauthenticated localhost connections + /// + public static readonly CVarDef AuthLocalTrust = + CVarDef.Create("auth.localtrust", 1f, CVar.SERVERONLY); + + /// + /// Trust score for guest connections + /// + public static readonly CVarDef AuthGuestTrust = + CVarDef.Create("auth.guesttrust", 0f, CVar.SERVERONLY); + /* * RENDERING */ diff --git a/Robust.Shared/Network/NetManager.ServerAuth.cs b/Robust.Shared/Network/NetManager.ServerAuth.cs index 175fbfe6f69..7f1303d4b74 100644 --- a/Robust.Shared/Network/NetManager.ServerAuth.cs +++ b/Robust.Shared/Network/NetManager.ServerAuth.cs @@ -221,10 +221,14 @@ private async void HandleHandshake(NetPeerData peer, NetConnection connection) _logger.Verbose( $"{connection.RemoteEndPoint}: Assigned user ID: {userId}"); + var localTrust = _config.GetCVar(CVars.AuthLocalTrust); + var guestTrust = _config.GetCVar(CVars.AuthGuestTrust); + userData = new NetUserData(userId, name) { HWId = [], ModernHWIds = [], + Trust = isLocal ? localTrust : guestTrust, IsLocal = isLocal }; } From e164c94633a27462c5e828ec829df8c5a6cbb6e1 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:50:29 +1000 Subject: [PATCH 057/178] Direction optimisations (#135) --- RELEASE-NOTES.md | 2 +- Robust.Shared.Maths/Direction.cs | 326 +++++++++++++------------------ 2 files changed, 132 insertions(+), 196 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c6a5fa90c2b..a2c17daadd6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -47,7 +47,7 @@ END TEMPLATE--> ### Other -*None yet* +* Optimise Direction and DirectionFlag methods. ### Internal diff --git a/Robust.Shared.Maths/Direction.cs b/Robust.Shared.Maths/Direction.cs index d661e8ddc6f..188a45745e2 100644 --- a/Robust.Shared.Maths/Direction.cs +++ b/Robust.Shared.Maths/Direction.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics.Contracts; using System.Numerics; +using System.Runtime.CompilerServices; namespace Robust.Shared.Maths { @@ -55,136 +56,81 @@ public static class DirectionExtensions private const double Segment = 2 * Math.PI / 8.0; // Cut the circle into 8 pieces + // 1f / MathF.Sqrt(2) except we can't const that. + private const float DiagonalComponent = 0.7071067811865476f; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction AsDir(this DirectionFlag directionFlag) { - switch (directionFlag) + return directionFlag switch { - case DirectionFlag.South: - return Direction.South; - case DirectionFlag.SouthEast: - return Direction.SouthEast; - case DirectionFlag.East: - return Direction.East; - case DirectionFlag.NorthEast: - return Direction.NorthEast; - case DirectionFlag.North: - return Direction.North; - case DirectionFlag.NorthWest: - return Direction.NorthWest; - case DirectionFlag.West: - return Direction.West; - case DirectionFlag.SouthWest: - return Direction.SouthWest; - default: - throw new ArgumentOutOfRangeException(); - } + DirectionFlag.South => Direction.South, + DirectionFlag.SouthEast => Direction.SouthEast, + DirectionFlag.East => Direction.East, + DirectionFlag.NorthEast => Direction.NorthEast, + DirectionFlag.North => Direction.North, + DirectionFlag.NorthWest => Direction.NorthWest, + DirectionFlag.West => Direction.West, + DirectionFlag.SouthWest => Direction.SouthWest, + _ => throw new ArgumentOutOfRangeException(nameof(directionFlag), directionFlag, null) + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DirectionFlag AsFlag(this Direction direction) { - switch (direction) + return direction switch { - case Direction.South: - return DirectionFlag.South; - case Direction.SouthEast: - return DirectionFlag.SouthEast; - case Direction.East: - return DirectionFlag.East; - case Direction.NorthEast: - return DirectionFlag.NorthEast; - case Direction.North: - return DirectionFlag.North; - case Direction.NorthWest: - return DirectionFlag.NorthWest; - case Direction.West: - return DirectionFlag.West; - case Direction.SouthWest: - return DirectionFlag.SouthWest; - default: - throw new ArgumentOutOfRangeException(); - } + Direction.South => DirectionFlag.South, + Direction.SouthEast => DirectionFlag.SouthEast, + Direction.East => DirectionFlag.East, + Direction.NorthEast => DirectionFlag.NorthEast, + Direction.North => DirectionFlag.North, + Direction.NorthWest => DirectionFlag.NorthWest, + Direction.West => DirectionFlag.West, + Direction.SouthWest => DirectionFlag.SouthWest, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DirectionFlag AsDirectionFlag(this Vector2i indices) { - switch (indices.X) + return (indices.X, indices.Y) switch { - case -1: - switch (indices.Y) - { - case -1: - return DirectionFlag.SouthWest; - case 0: - return DirectionFlag.West; - case 1: - return DirectionFlag.NorthWest; - } - break; - case 0: - switch (indices.Y) - { - case -1: - return DirectionFlag.South; - case 1: - return DirectionFlag.North; - } - break; - case 1: - switch (indices.Y) - { - case -1: - return DirectionFlag.SouthEast; - case 0: - return DirectionFlag.East; - case 1: - return DirectionFlag.NorthEast; - } - break; - } - - throw new ArgumentOutOfRangeException( - $"Tried to use a non-supported Vector2i for conversion to direction flag"); + (-1, -1) => DirectionFlag.SouthWest, + (-1, 0) => DirectionFlag.West, + (-1, 1) => DirectionFlag.NorthWest, + (0, -1) => DirectionFlag.South, + (0, 1) => DirectionFlag.North, + (1, -1) => DirectionFlag.SouthEast, + (1, 0) => DirectionFlag.East, + (1, 1) => DirectionFlag.NorthEast, + _ => throw new ArgumentOutOfRangeException( + nameof(indices), + indices, + "Tried to use a non-supported Vector2i for conversion to direction flag") + }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction AsDirection(this Vector2i indices) { - switch (indices.X) + return (indices.X, indices.Y) switch { - case -1: - switch (indices.Y) - { - case -1: - return Direction.SouthWest; - case 0: - return Direction.West; - case 1: - return Direction.NorthWest; - } - break; - case 0: - switch (indices.Y) - { - case -1: - return Direction.South; - case 1: - return Direction.North; - } - break; - case 1: - switch (indices.Y) - { - case -1: - return Direction.SouthEast; - case 0: - return Direction.East; - case 1: - return Direction.NorthEast; - } - break; - } - - throw new ArgumentOutOfRangeException( - $"Tried to use a non-supported Vector2i for conversion to direction"); + (-1, -1) => Direction.SouthWest, + (-1, 0) => Direction.West, + (-1, 1) => Direction.NorthWest, + (0, -1) => Direction.South, + (0, 1) => Direction.North, + (1, -1) => Direction.SouthEast, + (1, 0) => Direction.East, + (1, 1) => Direction.NorthEast, + _ => throw new ArgumentOutOfRangeException( + nameof(indices), + indices, + "Tried to use a non-supported Vector2i for conversion to direction") + }; } @@ -193,6 +139,7 @@ public static Direction AsDirection(this Vector2i indices) /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetDir(this Vector2 vec) { return Angle.FromWorldVec(vec).GetDir(); @@ -203,6 +150,7 @@ public static Direction GetDir(this Vector2 vec) /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetDir(this Vector2i vec) { return new Angle(vec).GetDir(); @@ -213,103 +161,88 @@ public static Direction GetDir(this Vector2i vec) /// /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Direction GetCardinalDir(this Vector2i vec) { return new Angle(vec).GetCardinalDir(); } - public static Direction GetOpposite(this Direction direction) + /// + extension(Direction direction) { - return direction switch + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Direction GetOpposite() { - Direction.East => Direction.West, - Direction.West => Direction.East, - Direction.North => Direction.South, - Direction.South => Direction.North, - Direction.NorthEast => Direction.SouthWest, - Direction.SouthWest => Direction.NorthEast, - Direction.NorthWest => Direction.SouthEast, - Direction.SouthEast => Direction.NorthWest, - _ => throw new ArgumentOutOfRangeException(nameof(direction)) - }; - } + return (Direction) (((int) direction + 4) & 7); + } - public static Direction GetClockwise90Degrees(this Direction direction) - { - return direction switch + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Direction GetClockwise90Degrees() { - Direction.East => Direction.South, - Direction.West => Direction.North, - Direction.North => Direction.East, - Direction.South => Direction.West, - Direction.NorthEast => Direction.SouthEast, - Direction.SouthWest => Direction.NorthWest, - Direction.NorthWest => Direction.NorthEast, - Direction.SouthEast => Direction.SouthWest, - _ => throw new ArgumentOutOfRangeException(nameof(direction)) - }; - } - - /// - /// Converts a direction to an angle, where angle is -PI to +PI. - /// - /// - /// - [Pure] - public static Angle ToAngle(this Direction dir) - { - var ang = Segment * (int) dir; - - if (ang > Math.PI) // convert 0 > 2PI to -PI > +PI - ang -= 2 * Math.PI; + return (Direction) (((int) direction + 6) & 7); + } - return ang; - } + /// + /// Converts a direction to an angle, where angle is -PI to +PI. + /// + /// + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Angle ToAngle() + { + var ang = Segment * (int) direction; - private static readonly Vector2[] DirectionVectors = { - new (0, -1), - new Vector2(1, -1).Normalized(), - new (1, 0), - new Vector2(1, 1).Normalized(), - new (0, 1), - new Vector2(-1, 1).Normalized(), - new (-1, 0), - new Vector2(-1, -1).Normalized() - }; + if (ang > Math.PI) // convert 0 > 2PI to -PI > +PI + ang -= 2 * Math.PI; - private static readonly Vector2i[] IntDirectionVectors = { - new (0, -1), - new (1, -1), - new (1, 0), - new (1, 1), - new (0, 1), - new (-1, 1), - new (-1, 0), - new (-1, -1) - }; + return ang; + } - /// - /// Converts a Direction to a normalized Direction vector. - /// - /// - /// a normalized 2D Vector - /// if invalid Direction is used - /// - public static Vector2 ToVec(this Direction dir) - { - return DirectionVectors[(int) dir]; - } + /// + /// Converts a Direction to a normalized Direction vector. + /// + /// a normalized 2D Vector + /// if invalid Direction is used + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2 ToVec() + { + return direction switch + { + Direction.South => new Vector2(0, -1), + Direction.SouthEast => new Vector2(DiagonalComponent, -DiagonalComponent), + Direction.East => new Vector2(1, 0), + Direction.NorthEast => new Vector2(DiagonalComponent, DiagonalComponent), + Direction.North => new Vector2(0, 1), + Direction.NorthWest => new Vector2(-DiagonalComponent, DiagonalComponent), + Direction.West => new Vector2(-1, 0), + Direction.SouthWest => new Vector2(-DiagonalComponent, -DiagonalComponent), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } - /// - /// Converts a Direction to a Vector2i. Useful for getting adjacent tiles. - /// - /// Direction - /// an 2D int Vector - /// if invalid Direction is used - /// - public static Vector2i ToIntVec(this Direction dir) - { - return IntDirectionVectors[(int) dir]; + /// + /// Converts a Direction to a Vector2i. Useful for getting adjacent tiles. + /// + /// an 2D int Vector + /// if invalid Direction is used + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector2i ToIntVec() + { + return direction switch + { + Direction.South => new Vector2i(0, -1), + Direction.SouthEast => new Vector2i(1, -1), + Direction.East => new Vector2i(1, 0), + Direction.NorthEast => new Vector2i(1, 1), + Direction.North => new Vector2i(0, 1), + Direction.NorthWest => new Vector2i(-1, 1), + Direction.West => new Vector2i(-1, 0), + Direction.SouthWest => new Vector2i(-1, -1), + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } } /// @@ -319,6 +252,7 @@ public static Vector2i ToIntVec(this Direction dir) /// 2D integer vector /// Direction by which we offset /// a newly vector offset by the dir or exception if the direction is invalid + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector2i Offset(this Vector2i vec, Direction dir) { return vec + dir.ToIntVec(); @@ -329,11 +263,13 @@ public static Vector2i Offset(this Vector2i vec, Direction dir) /// /// Vector to get the angle from. /// Angle of the vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Angle ToAngle(this Vector2 vec) { return new(vec); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Angle ToWorldAngle(this Vector2 vec) { return Angle.FromWorldVec(vec); From 31053007fb3eb128ca35e225f70f4daf405ab7bd Mon Sep 17 00:00:00 2001 From: Aiden Date: Tue, 23 Jun 2026 18:25:21 -0500 Subject: [PATCH 058/178] fix websocket no dispose (#6646) fix pls --- Robust.Client/Network/Transfer/ClientTransferManager.cs | 7 +++++++ .../Network/Transfer/ServerTransferImplWebSocket.cs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/Robust.Client/Network/Transfer/ClientTransferManager.cs b/Robust.Client/Network/Transfer/ClientTransferManager.cs index 781679d9e63..4f3af82a39b 100644 --- a/Robust.Client/Network/Transfer/ClientTransferManager.cs +++ b/Robust.Client/Network/Transfer/ClientTransferManager.cs @@ -46,6 +46,13 @@ public void Initialize() _netManager.RegisterNetMessage(RxTransferInit, NetMessageAccept.Client | NetMessageAccept.Handshake); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(RxTransferData, NetMessageAccept.Client | NetMessageAccept.Handshake); + _netManager.Disconnect += OnNetDisconnect; + } + + private void OnNetDisconnect(object? sender, NetDisconnectedArgs e) + { + _transferImpl?.Dispose(); + _transferImpl = null; } private async void RxTransferInit(MsgTransferInit message) diff --git a/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs b/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs index 641b960f3a0..6541aa15374 100644 --- a/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs +++ b/Robust.Server/Network/Transfer/ServerTransferImplWebSocket.cs @@ -118,5 +118,7 @@ public async Task HandleApiRequest(NetUserId userId, IStatusHandlerContext conte public override void Dispose() { _connectTcs.TrySetCanceled(); + + base.Dispose(); } } From b8c946a274d7d6e0894ae70de4b953726ea0fa94 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Wed, 24 Jun 2026 02:54:57 +0200 Subject: [PATCH 059/178] Release notes --- RELEASE-NOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 255d6b4afb0..6eafd7f6175 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,11 +39,11 @@ END TEMPLATE--> ### New features -*None yet* +* Local and guest trust scores are now assigned via the `auth.localtrust` and `auth.guesttrust` CVars. ### Bugfixes -*None yet* +* Fixed exceptions related to WebSocket transfer system. ### Other From f600a84ad482608c75cbfa8992a9dc9cb569d175 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Wed, 24 Jun 2026 02:55:06 +0200 Subject: [PATCH 060/178] Version: 277.2.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index e366ed6a67f..932b9cd7a5f 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.1.0 + 277.2.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6eafd7f6175..466a43c216a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,11 +39,11 @@ END TEMPLATE--> ### New features -* Local and guest trust scores are now assigned via the `auth.localtrust` and `auth.guesttrust` CVars. +*None yet* ### Bugfixes -* Fixed exceptions related to WebSocket transfer system. +*None yet* ### Other @@ -54,6 +54,17 @@ END TEMPLATE--> *None yet* +## 277.2.0 + +### New features + +* Local and guest trust scores are now assigned via the `auth.localtrust` and `auth.guesttrust` CVars. + +### Bugfixes + +* Fixed exceptions related to WebSocket transfer system. + + ## 277.1.0 ### New features From d07c1af29bf8b27c2da155325deffccd999df99d Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:39:36 +1000 Subject: [PATCH 061/178] Early-out PVS updates on rotation (#133) --- RELEASE-NOTES.md | 2 +- Robust.Server/GameStates/PvsSystem.Entity.cs | 3 +++ .../GameObjects/Components/Transform/TransformComponent.cs | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a2c17daadd6..d73f2749078 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Add a `OnlyRotation` property to MoveEvent where the EntityCoordinates remain the same. ### Bugfixes diff --git a/Robust.Server/GameStates/PvsSystem.Entity.cs b/Robust.Server/GameStates/PvsSystem.Entity.cs index 70748a196cf..bca88aac336 100644 --- a/Robust.Server/GameStates/PvsSystem.Entity.cs +++ b/Robust.Server/GameStates/PvsSystem.Entity.cs @@ -11,6 +11,9 @@ internal sealed partial class PvsSystem { private void OnEntityMove(ref MoveEvent ev) { + if (ev.OnlyRotation) + return; + UpdatePosition(ev.Entity.Owner, ev.Entity.Comp1, ev.Entity.Comp2, ev.OldPosition.EntityId); } diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index bb0d20aa20b..b94210a3fb7 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -557,6 +557,7 @@ public readonly struct MoveEvent( public TransformComponent Component => Entity.Comp1; public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId; + public bool OnlyRotation => OldPosition.Equals(NewPosition); } public struct TransformChildrenEnumerator : IDisposable From 27c2f34870be1eaffdaffd9d11653e3c86870a7d Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:22:30 +1000 Subject: [PATCH 062/178] Implement IList for ValueList (#32) --- RELEASE-NOTES.md | 1 + .../Collections/ValueListTest.cs | 141 ++++++++++++++++++ Robust.Shared/Collections/ValueList.cs | 56 ++++++- 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 Robust.Shared.Tests/Collections/ValueListTest.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index d73f2749078..8b59da3b8d4 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -40,6 +40,7 @@ END TEMPLATE--> ### New features * Add a `OnlyRotation` property to MoveEvent where the EntityCoordinates remain the same. +* ValueList now implements IList and not just IEnumerable. ### Bugfixes diff --git a/Robust.Shared.Tests/Collections/ValueListTest.cs b/Robust.Shared.Tests/Collections/ValueListTest.cs new file mode 100644 index 00000000000..6e139dd3639 --- /dev/null +++ b/Robust.Shared.Tests/Collections/ValueListTest.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; +using Robust.Shared.Collections; + +namespace Robust.Shared.Tests.Collections; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture, TestOf(typeof(ValueList<>))] +internal sealed class ValueListTest +{ + [Test] + public void TryPopClearsRemovedReference() + { + var list = new ValueList(1); + var item = new object(); + list.Add(item); + + Assert.That(list.TryPop(out var popped), Is.True); + Assert.That(popped, Is.SameAs(item)); + + var itemsField = typeof(ValueList).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance); + var items = (object?[]?) itemsField!.GetValue(list); + + Assert.That(items, Is.Not.Null); + Assert.That(items![0], Is.Null); + } + + [Test] + public void IListMethodsMatchList() + { + IList expected = new List(); + IList actual = new ValueList(); + + AssertListEqual(expected, actual); + Assert.That(actual.IsReadOnly, Is.EqualTo(expected.IsReadOnly)); + + expected.Add(1); + actual.Add(1); + AssertListEqual(expected, actual); + + expected.Add(3); + actual.Add(3); + AssertListEqual(expected, actual); + + expected.Insert(1, 2); + actual.Insert(1, 2); + AssertListEqual(expected, actual); + + expected[2] = 4; + actual[2] = 4; + AssertListEqual(expected, actual); + + Assert.That(actual.IndexOf(2), Is.EqualTo(expected.IndexOf(2))); + Assert.That(actual.IndexOf(99), Is.EqualTo(expected.IndexOf(99))); + Assert.That(actual.Contains(4), Is.EqualTo(expected.Contains(4))); + Assert.That(actual.Contains(99), Is.EqualTo(expected.Contains(99))); + + var expectedCopy = new int[5]; + var actualCopy = new int[5]; + expected.CopyTo(expectedCopy, 1); + actual.CopyTo(actualCopy, 1); + Assert.That(actualCopy, Is.EqualTo(expectedCopy)); + + Assert.That(actual.Remove(2), Is.EqualTo(expected.Remove(2))); + AssertListEqual(expected, actual); + + expected.RemoveAt(1); + actual.RemoveAt(1); + AssertListEqual(expected, actual); + + expected.Clear(); + actual.Clear(); + AssertListEqual(expected, actual); + } + + [Test] + public void IListInsertGrowsDefaultList() + { + IList expected = new List(); + IList actual = new ValueList(); + + expected.Insert(0, 10); + actual.Insert(0, 10); + + AssertListEqual(expected, actual); + } + + [Test] + public void IListCopyToExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => expected.CopyTo(null!, 0), () => actual.CopyTo(null!, 0)); + AssertSameException(() => expected.CopyTo(new int[3], -1), () => actual.CopyTo(new int[3], -1)); + AssertSameException(() => expected.CopyTo(new int[3], 1), () => actual.CopyTo(new int[3], 1)); + AssertSameException(() => expected.CopyTo(new int[2], 0), () => actual.CopyTo(new int[2], 0)); + } + + [Test] + public void IListIndexExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => _ = expected[-1], () => _ = actual[-1]); + AssertSameException(() => _ = expected[3], () => _ = actual[3]); + AssertSameException(() => expected[-1] = 0, () => actual[-1] = 0); + AssertSameException(() => expected[3] = 0, () => actual[3] = 0); + } + + [Test] + public void IListMutationExceptionsMatchList() + { + IList expected = new List { 1, 2, 3 }; + IList actual = new ValueList(expected); + + AssertSameException(() => expected.Insert(-1, 0), () => actual.Insert(-1, 0)); + AssertSameException(() => expected.Insert(4, 0), () => actual.Insert(4, 0)); + AssertSameException(() => expected.RemoveAt(-1), () => actual.RemoveAt(-1)); + AssertSameException(() => expected.RemoveAt(3), () => actual.RemoveAt(3)); + } + + private static void AssertListEqual(IList expected, IList actual) + { + Assert.Multiple(() => + { + Assert.That(actual.Count, Is.EqualTo(expected.Count)); + Assert.That(actual, Is.EqualTo(expected)); + }); + } + + private static void AssertSameException(TestDelegate expected, TestDelegate actual) + { + var expectedException = Assert.Throws(Is.InstanceOf(), expected); + var actualException = Assert.Throws(Is.InstanceOf(), actual); + + Assert.That(actualException, Is.TypeOf(expectedException!.GetType())); + } +} diff --git a/Robust.Shared/Collections/ValueList.cs b/Robust.Shared/Collections/ValueList.cs index 4d45c1b4bd7..71f38bb1705 100644 --- a/Robust.Shared/Collections/ValueList.cs +++ b/Robust.Shared/Collections/ValueList.cs @@ -23,6 +23,9 @@ namespace Robust.Shared.Collections; /// public APIs probably shouldn't expose it unless you know what you're doing. /// /// +/// If you use this as an IList then make sure it's passed as a generic to avoid boxing. +/// +/// /// This implementation does not complain if you modify it during iteration. Be careful! /// /// @@ -34,7 +37,7 @@ namespace Robust.Shared.Collections; /// /// /// The type of item to store in the list. -public struct ValueList : IEnumerable +public struct ValueList : IList { private const int DefaultCapacity = 4; @@ -142,6 +145,7 @@ public static ValueList OwningArray(T[]? array, int count) } public int Count { get; private set; } + public readonly bool IsReadOnly => false; // Sets or Gets the element at the given index. public readonly ref T this[int index] @@ -280,6 +284,22 @@ public readonly bool Contains(T item) return IndexOf(item) >= 0; } + public readonly void CopyTo(T[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + + if (arrayIndex < 0) + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + if (array.Length - arrayIndex < Count) + throw new ArgumentException("Destination array is not long enough."); + + if (Count == 0) + return; + + Array.Copy(_items!, 0, array, arrayIndex, Count); + } + /// /// Ensures that the capacity of this list is at least the specified . /// If the current capacity of the list is less than specified , @@ -397,13 +417,16 @@ public void Insert(int index, T item) throw new ArgumentOutOfRangeException(); } - if (Count == _items!.Length) Grow(Count + 1); + if (Count == Capacity) + Grow(Count + 1); + + var items = _items!; if (index < Count) { - Array.Copy(_items, index, _items, index + 1, Count - index); + Array.Copy(items, index, items, index + 1, Count - index); } - _items[index] = item; + items[index] = item; Count++; } @@ -504,6 +527,24 @@ public void RemoveAt(int index) _items![Count] = default!; } + T IList.this[int index] + { + readonly get + { + if ((uint) index >= (uint) Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return _items![index]; + } + set + { + if ((uint) index >= (uint) Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _items![index] = value; + } + } + public void Sort() => Span.Sort(); public void Sort(IComparer? comparer) => Span.Sort(comparer); public void Sort(Comparison comparison) => Span.Sort(comparison); @@ -674,7 +715,12 @@ public bool TryPop([MaybeNullWhen(false)] out T value) return false; } - value = _items![--Count]; + var index = --Count; + value = _items![index]; + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + _items[index] = default!; + return true; } From b32ef31b03a0d2a25239645ee01fc0b5a4e51363 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:38:10 +1000 Subject: [PATCH 063/178] Fix ValueList TryPop holding references (#31) --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 8b59da3b8d4..cb28b9ac889 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -44,7 +44,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* Fix ValueList TryPop not clearing element references. ### Other From ea2529e1ce45345ff61310f92fe3ff280ab5fbb1 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 25 Jun 2026 18:34:17 +1000 Subject: [PATCH 064/178] Fix ValueList Peek (#145) --- RELEASE-NOTES.md | 1 + .../Collections/ValueListTest.cs | 21 +++++++++++++++++++ Robust.Shared/Collections/ValueList.cs | 2 +- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index cb28b9ac889..103ea0fe45a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -45,6 +45,7 @@ END TEMPLATE--> ### Bugfixes * Fix ValueList TryPop not clearing element references. +* Fix ValueList Peek always throwing by referencing the wrong index. ### Other diff --git a/Robust.Shared.Tests/Collections/ValueListTest.cs b/Robust.Shared.Tests/Collections/ValueListTest.cs index 6e139dd3639..e8f7024c04c 100644 --- a/Robust.Shared.Tests/Collections/ValueListTest.cs +++ b/Robust.Shared.Tests/Collections/ValueListTest.cs @@ -9,6 +9,27 @@ namespace Robust.Shared.Tests.Collections; [TestFixture, TestOf(typeof(ValueList<>))] internal sealed class ValueListTest { + [Test] + public void PeekCorrectReference() + { + var list = new ValueList(2) + { + false, + true, + }; + + Assert.That(list.TryPeek(out var popped), Is.True); + Assert.That(popped, Is.True); + + list.TryPop(out _); + + Assert.That(list.TryPeek(out popped), Is.True); + Assert.That(popped, Is.False); + + list.TryPop(out _); + Assert.That(list.TryPeek(out popped), Is.False); + } + [Test] public void TryPopClearsRemovedReference() { diff --git a/Robust.Shared/Collections/ValueList.cs b/Robust.Shared/Collections/ValueList.cs index 71f38bb1705..f115356c507 100644 --- a/Robust.Shared/Collections/ValueList.cs +++ b/Robust.Shared/Collections/ValueList.cs @@ -736,7 +736,7 @@ public bool TryPeek([MaybeNullWhen(false)] out T value) return false; } - value = _items![Count]; + value = _items![Count - 1]; return true; } } From 6712c4fd4ead566c7e6fd1e5e63922695cc54a01 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Thu, 25 Jun 2026 20:07:14 +0200 Subject: [PATCH 065/178] fix publish --- .github/workflows/publish-client.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-client.yml b/.github/workflows/publish-client.yml index 4767740a2ff..5319e4bec30 100644 --- a/.github/workflows/publish-client.yml +++ b/.github/workflows/publish-client.yml @@ -33,10 +33,10 @@ jobs: mkdir "release/${{ steps.parse_version.outputs.version }}" mv release/*.zip "release/${{ steps.parse_version.outputs.version }}" - - name: Upload files to Suns + - name: Upload files to Dropwig uses: appleboy/scp-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} source: "release/${{ steps.parse_version.outputs.version }}" @@ -46,7 +46,7 @@ jobs: - name: Update manifest JSON uses: appleboy/ssh-action@master with: - host: suns.spacestation14.com + host: dropwig.playss14.com username: robust-build-push key: ${{ secrets.CENTCOMM_ROBUST_BUILDS_PUSH_KEY }} script: /home/robust-build-push/push.ps1 ${{ steps.parse_version.outputs.version }} From 2b63bfdcc0ddde3b71435a4f01407bffe24a2751 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Thu, 25 Jun 2026 11:29:40 -0700 Subject: [PATCH 066/178] Update Lidgren.Network --- Lidgren.Network/Lidgren.Network | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index 1d85b82e058..726dd552a4b 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit 1d85b82e058101b7ebd60cc8883af5359e4c263a +Subproject commit 726dd552a4b104fb2b701848705f1250a073f060 From d71a1cc1b0bf0c29ded1de7cde22ed557c8adeaf Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Thu, 25 Jun 2026 11:32:20 -0700 Subject: [PATCH 067/178] Release notes --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 466a43c216a..a249406e42f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,7 +43,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* Fixed a bug in Lidgren.Network HandleReleasedFragment that could cause out of memory errors. ### Other From f47f6b02bf08bec9be36328208b728f9cff64aad Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Thu, 25 Jun 2026 12:36:13 -0700 Subject: [PATCH 068/178] Version: 277.2.1 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 932b9cd7a5f..22df1e03bc5 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.2.0 + 277.2.1 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a249406e42f..6887d3164b5 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,7 +43,7 @@ END TEMPLATE--> ### Bugfixes -* Fixed a bug in Lidgren.Network HandleReleasedFragment that could cause out of memory errors. +*None yet* ### Other @@ -54,6 +54,13 @@ END TEMPLATE--> *None yet* +## 277.2.1 + +### Bugfixes + +* Fixed a bug in Lidgren.Network HandleReleasedFragment that could cause out of memory errors. + + ## 277.2.0 ### New features From e519bcdc9d55dfaa6dad7ba7b56acd8006be3d67 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:21:17 +0200 Subject: [PATCH 069/178] undo some changes specific to the new repo --- .github/workflows/test-content.yml | 8 +++--- .gitmodules | 10 +++---- MSBuild/Robust.Engine.Version.props | 12 +++------ README.md | 14 +++++----- RELEASE-NOTES.md | 32 ++++++++++++++++++++--- Robust.Client.WebView/TestBrowseWindow.cs | 2 +- Robust.Server/server_config.toml | 4 +-- Robust.Shared/CVars.cs | 2 +- Robust.Shared/Network/AuthManager.cs | 2 +- 9 files changed, 52 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index 87cb7d59d0b..f8014cc945a 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -14,8 +14,8 @@ jobs: - name: Check out content uses: actions/checkout@v4.2.2 with: - repository: Space-Wizards-Federation/space-station-14 - submodules: true + repository: space-wizards/space-station-14 + submodules: recursive - name: Setup .NET Core uses: actions/setup-dotnet@v4.1.0 @@ -34,9 +34,7 @@ jobs: run: cp RobustToolbox/global.json . - name: Install dependencies run: dotnet restore - - name: Build (Release) - run: dotnet build --configuration Release --no-restore /m - - name: Build (Debug) + - name: Build run: dotnet build --configuration DebugOpt --no-restore /m - name: Content.Tests shell: pwsh diff --git a/.gitmodules b/.gitmodules index d83f9c8b743..30c23aed2df 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,15 @@ [submodule "NetSerializer"] path = NetSerializer - url = https://github.com/Space-Wizards-Federation/netserializer + url = https://github.com/space-wizards/netserializer [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network - url = https://github.com/Space-Wizards-Federation/SpaceWizards.Lidgren.Network + url = https://github.com/space-wizards/lidgren-network-gen3.git [submodule "XamlX"] path = XamlX - url = https://github.com/Space-Wizards-Federation/XamlX + url = https://github.com/space-wizards/XamlX [submodule "Robust.LoaderApi"] path = Robust.LoaderApi - url = https://github.com/Space-Wizards-Federation/Robust.LoaderApi.git + url = https://github.com/space-wizards/Robust.LoaderApi.git [submodule "cefglue"] path = cefglue - url = https://github.com/Space-Wizards-Federation/cefglue.git + url = https://github.com/space-wizards/cefglue.git diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 766b4cc3bc7..22df1e03bc5 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,8 +1,4 @@ - - - - - 277.2.1 - - - + + + 277.2.1 + diff --git a/README.md b/README.md index dc52da21198..a8d9fc8c08e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -![Robust Toolbox](https://raw.githubusercontent.com/Space-Wizards-Federation/asset-dump/3dd3078e49e3a7e06709a6e0fc6e3223d8d44ca2/robust.png) +![Robust Toolbox](https://raw.githubusercontent.com/space-wizards/asset-dump/3dd3078e49e3a7e06709a6e0fc6e3223d8d44ca2/robust.png) -Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/Space-Wizards-Federation/space-station-14/), although we're working on making it usable for both [singleplayer](https://github.com/Space-Wizards-Federation/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/Space-Wizards-Federation/RobustToolboxTemplate/) projects. +Robust Toolbox is an engine primarily being developed for [Space Station 14](https://github.com/space-wizards/space-station-14), although we're working on making it usable for both [singleplayer](https://github.com/space-wizards/RobustToolboxTemplateSingleplayer) and [multiplayer](https://github.com/space-wizards/RobustToolboxTemplate) projects. -Use the [content repo](https://github.com/Space-Wizards-Federation/space-station-14) for actual development, even if you're modifying the engine itself. +Use the [content repo](https://github.com/space-wizards/space-station-14) for actual development, even if you're modifying the engine itself. ## Project Links -[Website](https://playss14.com//) | [Discord](https://discord.gg/ss14) | [Standalone Download](https://playss14.com/about/download/) +[Website](https://spacestation14.io/) | [Discord](https://discord.gg/t2jac3p) | [Forum](https://forum.spacestation14.io/) | [Steam](https://store.steampowered.com/app/1255460/Space_Station_14/) | [Standalone Download](https://spacestation14.io/about/nightlies/) ## Documentation/Wiki @@ -14,12 +14,12 @@ The [wiki](https://docs.spacestation14.io/) has documentation on SS14s content, ## Contributing -We are happy to accept contributions from anybody. Get in Discord or IRC if you want to help. We've got a [list of issues](https://github.com/Space-Wizards-Federation/RobustToolbox/issues/) that need to be done and anybody can pick them up. Don't be afraid to ask for help either! +We are happy to accept contributions from anybody. Get in Discord or IRC if you want to help. We've got a [list of issues](https://github.com/space-wizards/RobustToolbox/issues) that need to be done and anybody can pick them up. Don't be afraid to ask for help either! ## Building -This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/Space-Wizards-Federation/space-station-14). Think of Robust Toolbox as BYOND in the context of Space Station 13. +This repository is the **engine** part of SS14. It's the base engine all SS14 servers will be built on. As such, it does not start on its own: it needs the [content repo](https://github.com/space-wizards/space-station-14). Think of Robust Toolbox as BYOND in the context of Space Station 13. ## Legal Info -See [legal.md](https://github.com/Space-Wizards-Federation/RobustToolbox/blob/master/legal.md) for licenses and copyright. +See [legal.md](https://github.com/space-wizards/RobustToolbox/blob/master/legal.md) for licenses and copyright. diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 335032b2685..6887d3164b5 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,13 +39,11 @@ END TEMPLATE--> ### New features -* Add a `OnlyRotation` property to MoveEvent where the EntityCoordinates remain the same. -* ValueList now implements IList and not just IEnumerable. +*None yet* ### Bugfixes -* Fix ValueList TryPop not clearing element references. -* Fix ValueList Peek always throwing by referencing the wrong index. +*None yet* ### Other @@ -119,6 +117,32 @@ END TEMPLATE--> * Statics on interfaces, and other cases of `static abstract` methods and properties, are now allowed by the sandbox. * `INumber` and all associated types in `System.Numerics` are now allowed by the sandbox. * `BigInteger` is now allowed by the sandbox. +* `ISpanFormattable` and `IUtf8SpanFormattable` are now allowed by the sandbox. +* `IParsable`, `ISpanParsable`, and `IUtf8SpanParsable` are now allowed by the sandbox. +* Added `MarkupNode.IsPlainText` helper property. +* Added an analyzer to detect and warn about `[Dependency]` fields with nullable types. These have never done anything special and are programming error. +* Added a `[Dependency]` source generator. This should reduce runtime codegen overhead amount and reduce reflection use. + * Existing code using dependency fields should be updated to be `partial` and not use `readonly`. Analyzers and code fixers exist for this. It is not yet an error, but will become one in the future. +* `IEntityManager` has a new family of 1-4 component methods for working with *singleton entities*, entities which only + one instance of exists at a time for state management. Consult `IEntityManager.Single.cs` and the documentation for + details. +* `INetManager` receive bandwidth stats are now tracked on release builds. +* OpenAL is now configured for HRTF support. This should make positional audio sound better on headphones. +* More fields on `AudioComponent` are now accessible to be modified by user code, in case you replace the built-in occlusion code or similar. +* Added `IDependencyCollection.Create()` and `.CreateFrom()`. +* Added a system for explicitly unloading `IResourceCache` resources. Use with care! +* `System.Numerics.Tensors.TensorPrimitives` is now available to sandbox. +* A new analyzer prevents `[Virtual]` from being used on `static`, `sealed` or `abstract` types. + +### Bugfixes + +* `ComponentNetworkGenerator` received some internal refactoring, and should now work on `internal` types. +* Matrix and shader state are now automatically cleaned up after each overlay draw. +* Fixed `ScrollContainer` not arranging child elements correctly, causing buggy behavior with `RichTextLabel` and other controls. +* Fix Happy Eyeballs HTTP connections being left alive in some cases. +* Fixed some doc comments. +* Fix VRAM leak related to grid chunk edges. + ### Other * Game server now informs the auth server of its address when confirming client connections. diff --git a/Robust.Client.WebView/TestBrowseWindow.cs b/Robust.Client.WebView/TestBrowseWindow.cs index 4cab70562bc..3c73754ef76 100644 --- a/Robust.Client.WebView/TestBrowseWindow.cs +++ b/Robust.Client.WebView/TestBrowseWindow.cs @@ -23,7 +23,7 @@ internal sealed class TestBrowseWindowCommand : LocalizedCommands public override void Execute(IConsoleShell shell, string argStr, string[] args) { - var url = args.Length > 0 ? args[0] : "https://playss14.com"; + var url = args.Length > 0 ? args[0] : "https://spacestation14.com"; new TestBrowseWindow(url).Open(); } diff --git a/Robust.Server/server_config.toml b/Robust.Server/server_config.toml index f68797bed3a..44a50e58729 100644 --- a/Robust.Server/server_config.toml +++ b/Robust.Server/server_config.toml @@ -52,7 +52,7 @@ tags = "" # Must be in the form of an ss14:// or ss14s:// URI pointing to the status API. server_url = "" # Comma-separated list of URLs of hub servers to advertise to. -hub_urls = "https://hub.playss14.com/" +hub_urls = "https://hub.spacestation14.com/" [build] # *Absolutely all of these can be supplied using a "build.json" file* @@ -98,5 +98,5 @@ hub_urls = "https://hub.playss14.com/" # You should probably never EVER need to touch this, but if you need a custom auth server, # (the auth server being the one which manages Space Station 14 accounts), you change it here. -# server = "https://auth.playss14.com/" +# server = https://auth.spacestation14.com/ diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 2c3819b44f0..d4c67e3998d 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -1625,7 +1625,7 @@ protected CVars() /// Comma-separated list of URLs of hub servers to advertise to. /// public static readonly CVarDef HubUrls = - CVarDef.Create("hub.hub_urls", "https://hub.playss14.com/", CVar.SERVERONLY); + CVarDef.Create("hub.hub_urls", "https://hub.spacestation14.com/", CVar.SERVERONLY); /// /// URL of this server to advertise. diff --git a/Robust.Shared/Network/AuthManager.cs b/Robust.Shared/Network/AuthManager.cs index 3fd92993177..ea2250e891f 100644 --- a/Robust.Shared/Network/AuthManager.cs +++ b/Robust.Shared/Network/AuthManager.cs @@ -25,7 +25,7 @@ internal interface IAuthManager internal sealed class AuthManager : IAuthManager { - public const string DefaultAuthServer = "https://auth.playss14.com/"; + public const string DefaultAuthServer = "https://auth.spacestation14.com/"; public NetUserId? UserId { get; set; } public string? Server { get; set; } = DefaultAuthServer; From f7b6a24e295b8036de6788e6d2268afe225f6879 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 26 Jun 2026 01:30:47 +0200 Subject: [PATCH 070/178] update release notes for merge from new to old repo --- RELEASE-NOTES.md | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6887d3164b5..c0be4da74e4 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,24 +35,48 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. +* Obsolete LocalRotation in favor of the system method. The angle is now also normalized to 2PI and no longer grows indefinitely. ### New features -*None yet* +* Add a `OnlyRotation` property to MoveEvent where the EntityCoordinates remain the same. +* ValueList now implements IList and not just IEnumerable. +* Add a BoundUserInterfaceMessageReceivedEvent that will be raised whenever a BoundUserInterfaceMessage is received regardless of validation. +* Added `IsHardCollidable` to `SharedPhysicsSystem`. +* Added `GetFilledTileCount` to `SharedMapSystem`. +* Changed the cursors on interactive controls. +* Added new `StyleProperty` `track` to `ScrollBar` that takes a `StyleBox` and displays it as a backing track for the whole height of the `ScrollBar`. +* Scroll Lock is now a bindable key. ### Bugfixes -*None yet* +* Fix ValueList TryPop not clearing element references. +* Fix ValueList Peek always throwing by referencing the wrong index. +* Windows will stay at relative position not absolute pixel position on window resize. +* Fixed override properties in `WrapContainer` not actually overriding the Style Properties. +* Fixed `BoxContainer`'s `SeparationOverride` not overriding the Style Properties. +* Fixed `SeparationOverride` not invalidating measure. +* Fixed swapped parameters in `MapManager`'s `FindGridsIntersecting` methods. ### Other -*None yet* +* Optimise Direction and DirectionFlag methods. +* Added Pure attributes to the `EntityLookup` bounds methods. +* Improved performance of collision filter test. +* Removed an outdated xmldoc comment regarding dependency injection. +* Audio resources now use `AsSpan` when checking signatures. ### Internal -*None yet* - +* Reduce TryParseEnum string allocations. +* Reduce TryRelativeTo string allocations. +* Reduce OpenGL logging string allocations on debug for the client. +* Optimise sprite sorting slightly. +* Simplify and optimise Box2.Contains(Vector2) +* Optimise ComponentRegistry deserialization slightly. +* Optimise Box2Rotated.TransformBox slightly. +* Added several test helpers to avoid boilerplate in integration tests around client connection / disconnection. ## 277.2.1 From 1b9de71b81bd361591c7e83ae7d58f444c58a834 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:46:57 +0200 Subject: [PATCH 071/178] Remove obsolete TryIndex overloads (#6478) * remove obsolete TryIndex overloads * a --------- Co-authored-by: metalgearsloth --- RELEASE-NOTES.md | 2 +- Robust.Shared/Prototypes/IPrototypeManager.cs | 59 ------------------- Robust.Shared/Prototypes/PrototypeManager.cs | 34 ----------- 3 files changed, 1 insertion(+), 94 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6887d3164b5..e4b51157b0d 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* The obsoleted TryIndex methods on PrototypeManager have now been removed. ### New features diff --git a/Robust.Shared/Prototypes/IPrototypeManager.cs b/Robust.Shared/Prototypes/IPrototypeManager.cs index fcb5cedb18e..e800d27f585 100644 --- a/Robust.Shared/Prototypes/IPrototypeManager.cs +++ b/Robust.Shared/Prototypes/IPrototypeManager.cs @@ -175,19 +175,6 @@ bool TryGetInstances([NotNullWhen(true)] out FrozenDictionary? ins /// bool Resolve([ForbidLiteral] EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype); - /// - /// Retrieve an by ID, optionally logging an error if it does not exist. - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if it does not. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex( - [ForbidLiteral] EntProtoId id, - [NotNullWhen(true)] out EntityPrototype? prototype, - bool logError = true); - /// /// Resolve an by ID. /// @@ -236,17 +223,6 @@ bool TryIndex( /// bool Resolve([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; - /// - /// Retrieve a prototype by ID, optionally logging an error if it does not exist. - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if it does not. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype; - /// /// Resolve a prototype by ID. /// @@ -299,25 +275,6 @@ bool TryIndex([ForbidLiteral] ProtoId id, [NotNullWhen(true)] out T? proto /// bool Resolve([ForbidLiteral] EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype); - /// - /// Retrieve an by ID, gracefully handling null, - /// and optionally logging an error if it does not exist. - /// - /// - /// - /// No error is logged if is null. - /// - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if was null, or it does not exist. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex( - [ForbidLiteral] EntProtoId? id, - [NotNullWhen(true)] out EntityPrototype? prototype, - bool logError = true); - /// /// Resolve an by ID, gracefully handling null. /// @@ -369,22 +326,6 @@ bool TryIndex( /// bool Resolve([ForbidLiteral] ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype; - /// - /// Retrieve a prototype by ID, gracefully handling null, - /// and optionally logging an error if it does not exist. - /// - /// - /// - /// No error is logged if is null. - /// - /// - /// The prototype ID to look up. - /// The prototype that was resolved, null if it does not exist. - /// If true (default), log an error if the prototype does not exist. - /// True if the prototype exists, false if was null, or it does not exist. - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - bool TryIndex([ForbidLiteral] ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) where T : class, IPrototype; - /// /// Resolve a prototype by ID, gracefully handling null. /// diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs index 527ae7ea2cf..99cdf7a0e29 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.cs @@ -801,14 +801,6 @@ public bool Resolve(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prot return false; } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex([ForbidLiteral] EntProtoId id, [NotNullWhen(true)] out EntityPrototype? prototype) { return TryIndex(id.Id, out prototype); @@ -823,15 +815,6 @@ public bool Resolve(ProtoId id, [NotNullWhen(true)] out T? prototype) wher return false; } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(ProtoId id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype { @@ -849,14 +832,6 @@ public bool Resolve(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? pro return Resolve(id.Value, out prototype); } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype, bool logError = true) - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(EntProtoId? id, [NotNullWhen(true)] out EntityPrototype? prototype) { if (id == null) @@ -879,15 +854,6 @@ public bool Resolve(ProtoId? id, [NotNullWhen(true)] out T? prototype) whe return Resolve(id.Value, out prototype); } - [Obsolete("Use Resolve() if you want to get a prototype without throwing but while still logging an error.")] - public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype, bool logError = true) - where T : class, IPrototype - { - if (logError) - return Resolve(id, out prototype); - return TryIndex(id, out prototype); - } - public bool TryIndex(ProtoId? id, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype { From f4e404a61cc390adcaf3e2c0952a3612dcae7c6c Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:47:37 -0700 Subject: [PATCH 072/178] (Try to) Fix a null reference exception in ComponentTreeSystem.UpdateTreePositions (#6604) --- Robust.Shared/ComponentTrees/ComponentTreeSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs index ded59959fe0..eaae8fab41e 100644 --- a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs +++ b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs @@ -242,9 +242,9 @@ public void UpdateTreePositions() { (pos, rot) = XformSystem.GetRelativePositionRotation( entry.Transform, - newTree!.Value); + newTree.Value); - newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot)); + newTreeComp?.Tree.Update(entry, ExtractAabb(entry, pos, rot)); continue; } @@ -258,7 +258,7 @@ public void UpdateTreePositions() (pos, rot) = XformSystem.GetRelativePositionRotation( entry.Transform, - newTree!.Value); + newTree.Value); newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot)); } From 8862e8d6326788a089177e2e1e0cce0287d0b30f Mon Sep 17 00:00:00 2001 From: Patrik Caes-Sayrs Date: Fri, 26 Jun 2026 05:21:19 -0600 Subject: [PATCH 073/178] Debug Console Autocompletes for Contains (#6418) * Debug Console Autocompletes for Contains Entering commands to console now filters for commands containing, not just starting with, what you typed. * RN --------- Co-authored-by: metalgearsloth --- RELEASE-NOTES.md | 2 +- .../CustomControls/DebugConsole.xaml.Completions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e4b51157b0d..23c0d28e4aa 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Completion filter now works by Contains instead of StartsWith ### Bugfixes diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs index 45f58301e77..a6fcbe1626f 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs @@ -262,7 +262,7 @@ private void UpdateCompletionsPopup() private CompletionOption[] FilterCompletions(IEnumerable completions, string curTyping) { return completions - .Where(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase)) + .Where(c => c.Value.Contains(curTyping, StringComparison.CurrentCultureIgnoreCase)) .ToArray(); } From 03ef5032859cb22529531da15ee3926c848409ee Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:45:32 +1000 Subject: [PATCH 074/178] Add batch drawing methods (#6655) * Add batched drawing API Internally Clyde batches drawing, however when drawing hundreds / thousands of the same quad the batch checks add up. This just omits those checks and directly draws instead. Same with skipping color modulation. Long-term could probably figure out a way to SIMD the transforms better. * RN --- RELEASE-NOTES.md | 1 + .../Graphics/Clyde/Clyde.RenderHandle.cs | 50 +++++ .../Graphics/Clyde/Clyde.Rendering.cs | 183 +++++++++++++++++- .../Graphics/Drawing/DrawingHandleWorld.cs | 34 ++++ 4 files changed, 264 insertions(+), 4 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 23c0d28e4aa..e6680a0ba22 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,7 @@ END TEMPLATE--> ### New features +* Add batched Box2 / Box2Rotated drawing methods to Clyde WorldHandle. * Completion filter now works by Contains instead of StartsWith ### Bugfixes diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs index 57b4c038eac..82efebf4bac 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs @@ -19,6 +19,8 @@ internal sealed class RenderHandle : IRenderHandle { private readonly Clyde _clyde; private readonly IEntityManager _entities; + private readonly ClydeTexture _whiteClydeTexture; + private readonly Box2 _whiteUv; public DrawingHandleScreen DrawingHandleScreen { get; } public DrawingHandleWorld DrawingHandleWorld { get; } @@ -29,6 +31,8 @@ public RenderHandle(Clyde clyde, IEntityManager entities) _entities = entities; var white = _clyde.GetStockTexture(ClydeStockTexture.White); + _whiteClydeTexture = ExtractTexture(white, null, out var whiteBounds); + _whiteUv = WorldTextureBoundsToUV(_whiteClydeTexture, whiteBounds); DrawingHandleScreen = new DrawingHandleScreenImpl(white, this); DrawingHandleWorld = new DrawingHandleWorldImpl(white, this); } @@ -93,6 +97,30 @@ public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl _clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr); } + public void DrawTextureWorldBatch(Texture texture, ReadOnlySpan rects, Color modulate) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatch(clydeTexture.TextureId, rects, modulate, in sr); + } + + public void DrawTextureWorldBatchUnmodulated(Texture texture, ReadOnlySpan rects) + { + var clydeTexture = ExtractTexture(texture, null, out var csr); + var sr = WorldTextureBoundsToUV(clydeTexture, csr); + _clyde.DrawTextureBatchUnmodulated(clydeTexture.TextureId, rects, in sr); + } + + public void DrawRectWorldBatch(ReadOnlySpan rects, Color modulate) + { + _clyde.DrawRectBatch(_whiteClydeTexture.TextureId, rects, modulate, in _whiteUv); + } + + public void DrawRectWorldBatchUnmodulated(ReadOnlySpan rects) + { + _clyde.DrawRectBatchUnmodulated(_whiteClydeTexture.TextureId, rects, in _whiteUv); + } + internal static Box2 WorldTextureBoundsToUV(ClydeTexture texture, UIBox2 csr) { var (w, h) = texture.Size; @@ -488,6 +516,16 @@ public override void DrawRect(Box2 rect, Color color, bool filled = true) } } + public override void DrawRects(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatch(rects, Modulate); + } + + public override void DrawRectsUnmodulated(ReadOnlySpan rects) + { + _renderHandle.DrawRectWorldBatchUnmodulated(rects); + } + public override void DrawRect(in Box2Rotated rect, Color color, bool filled = true) { if (filled) @@ -539,6 +577,18 @@ public override void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, quad.TopLeft, quad.TopRight, color, in subRegion); } + /// + public override void DrawTextureRects(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatch(texture, rects, Modulate); + } + + /// + public override void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects) + { + _renderHandle.DrawTextureWorldBatchUnmodulated(texture, rects); + } + public override void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, Texture texture, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs index f7d419161d9..7d6f67220c2 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Rendering.cs @@ -620,16 +620,191 @@ private void DrawTexture(ClydeHandle texture, Vector2 bl, Vector2 br, Vector2 tl // TODO: split batch if necessary. var vIdx = BatchVertexIndex; - BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, new Vector2(0, 0), modulate); - BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, new Vector2(1, 0), modulate); - BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, new Vector2(1, 1), modulate); - BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, new Vector2(0, 1), modulate); + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, modulate); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, modulate); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, modulate); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, modulate); BatchVertexIndex += 4; QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); _debugStats.LastClydeDrawCalls += 1; } + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawTextureBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawTextureBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawTextureBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + // Avoids calling EnsureBatchSpaceAvailable / EnsureBatchState for each individual quad. + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Modulate ?? Color.White; + + if (applyModulation) + color *= modulate; + + // Can probably SIMD this more somehow but future concern. + var quad = rect.Quad; + var transform = quad.Transform * _currentMatrixModel; + + var bl = Vector2.Transform(quad.Box.BottomLeft, transform); + var br = Vector2.Transform(quad.Box.BottomRight, transform); + var tr = Vector2.Transform(quad.Box.TopRight, transform); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, modulate, in texCoords, true); + } + + private void DrawRectBatchUnmodulated( + ClydeHandle texture, + ReadOnlySpan rects, + in Box2 texCoords) + { + DrawRectBatch(texture, rects, Color.White, in texCoords, false); + } + + private void DrawRectBatch( + ClydeHandle texture, + ReadOnlySpan rects, + Color modulate, + in Box2 texCoords, + bool applyModulation) + { + if (rects.Length == 0) + return; + + var primitiveType = GetQuadBatchPrimitiveType(); + var indexCount = GetQuadBatchIndexCount(); + var rectIndex = 0; + + while (rectIndex < rects.Length) + { + var availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + EnsureBatchState(texture, true, primitiveType, _queuedShader); + + availableQuads = GetAvailableBatchQuads(indexCount); + + if (availableQuads <= 0) + { + FlushBatchQueue(); + continue; + } + + var count = Math.Min(availableQuads, rects.Length - rectIndex); + + for (var i = 0; i < count; i++) + { + ref readonly var rect = ref rects[rectIndex + i]; + var color = rect.Color; + + if (applyModulation) + color *= modulate; + + var box = rect.Rect; + + var bl = Vector2.Transform(box.BottomLeft, _currentMatrixModel); + var br = Vector2.Transform(box.BottomRight, _currentMatrixModel); + var tr = Vector2.Transform(box.TopRight, _currentMatrixModel); + var tl = tr + bl - br; + + var vIdx = BatchVertexIndex; + BatchVertexData[vIdx + 0] = new Vertex2D(bl, texCoords.BottomLeft, Vector2.Zero, color); + BatchVertexData[vIdx + 1] = new Vertex2D(br, texCoords.BottomRight, Vector2.UnitX, color); + BatchVertexData[vIdx + 2] = new Vertex2D(tr, texCoords.TopRight, Vector2.One, color); + BatchVertexData[vIdx + 3] = new Vertex2D(tl, texCoords.TopLeft, Vector2.UnitY, color); + BatchVertexIndex += 4; + QuadBatchIndexWrite(BatchIndexData, ref BatchIndexIndex, (ushort) vIdx); + } + + rectIndex += count; + _debugStats.LastClydeDrawCalls += count; + } + } + + private int GetAvailableBatchQuads(int indexCount) + { + // The vertex size is so comically high you're probably never hitting it. + var availableVertices = Math.Max(0, BatchVertexData.Length - BatchVertexIndex - 1); + var availableIndices = Math.Max(0, BatchIndexData.Length - BatchIndexIndex); + return Math.Min(availableVertices / 4, availableIndices / indexCount); + } + private void DrawPrimitives(DrawPrimitiveTopology primitiveTopology, ClydeHandle textureId, ReadOnlySpan indices, ReadOnlySpan vertices) { diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs index 9fba50a0506..af7d88d03da 100644 --- a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs +++ b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Robust.Shared.Graphics; using Robust.Shared.Maths; @@ -22,6 +23,19 @@ protected DrawingHandleWorld(Texture white) : base(white) /// Is it filled with color, or just the border lines? public abstract void DrawRect(Box2 rect, Color color, bool filled = true); + /// + /// Draws multiple filled, untextured colored rectangles to the world. All rectangles use the current transform. + /// + /// + /// This is the batched equivalent of repeated filled calls. + /// + public abstract void DrawRects(ReadOnlySpan rects); + + /// + /// Draws multiple filled, untextured colored rectangles without multiplying by the handle modulation color. + /// + public abstract void DrawRectsUnmodulated(ReadOnlySpan rects); + /// /// Draws an untextured colored rectangle to the world.The coordinate system is right handed. /// Make sure to set @@ -58,6 +72,16 @@ public abstract void DrawTextureRectRegion(Texture texture, Box2 quad, public abstract void DrawTextureRectRegion(Texture texture, in Box2Rotated quad, Color? modulate = null, UIBox2? subRegion = null); + /// + /// Draws multiple rotated rectangles for the same texture. + /// + public abstract void DrawTextureRects(Texture texture, ReadOnlySpan rects); + + /// + /// Draws multiple rotated rectangles for the same texture without multiplying by the handle modulation color. + /// + public abstract void DrawTextureRectsUnmodulated(Texture texture, ReadOnlySpan rects); + private Box2 GetQuad(Texture texture, Vector2 position) { return Box2.FromDimensions(position, texture.Size / (float)Ppm); @@ -133,4 +157,14 @@ public void DrawTextureRect(Texture texture, in Box2Rotated quad, Color? modulat DrawTextureRectRegion(texture, in quad, modulate); } } + + /// + /// A rotated rectangle for batched world texture drawing. + /// + public readonly record struct WorldTextureRect(Box2Rotated Quad, Color? Modulate = null); + + /// + /// An axis-aligned rectangle for batched world rectangle drawing. + /// + public readonly record struct WorldRect(Box2 Rect, Color Color); } From c51009d4f08d2a66d0e3753756b10dd5b5e372d9 Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:58:16 -0700 Subject: [PATCH 075/178] Allow foreach with EntityQueryEnumerator and AllEntityQueryEnumerator (#6660) --- .../GameObjects/EntityQueryTests.cs | 68 +++++++ .../GameObjects/EntityManager.Components.cs | 176 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs diff --git a/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs b/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs new file mode 100644 index 00000000000..73601506b2e --- /dev/null +++ b/Robust.Shared.IntegrationTests/GameObjects/EntityQueryTests.cs @@ -0,0 +1,68 @@ +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.UnitTesting; + +namespace Robust.Shared.IntegrationTests.GameObjects; + +public sealed partial class EntityQueryTests : RobustIntegrationTest +{ + [Test] + public async Task TestEntityQueryForEach() + { + var server = StartServer(); + + await server.WaitIdleAsync(); + await server.WaitPost(() => + { + var sEntManager = server.ResolveDependency(); + for (var i = 0; i < 5; i++) + { + var ent = sEntManager.Spawn(null, MapCoordinates.Nullspace); + sEntManager.EnsureComponent(ent); + } + + for (var i = 0; i < 10; i++) + { + var ent = sEntManager.Spawn(null, MapCoordinates.Nullspace); + sEntManager.EnsureComponent(ent); + } + }); + + await server.WaitAssertion(() => + { + var sEntManager = server.ResolveDependency(); + var a = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + Assert.That(a, Does.Not.Contain(ent)); + a.Add(ent); + } + + Assert.That(a, Has.Count.EqualTo(5)); + + var b = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + Assert.That(b, Does.Not.Contain(ent)); + b.Add(ent); + } + + Assert.That(b, Has.Count.EqualTo(10)); + + var c = new List>(); + foreach (var ent in sEntManager.EntityQueryEnumerator()) + { + c.Add(ent); + } + + Assert.That(c, Is.Empty); + }); + } + + [RegisterComponent] + public sealed partial class EntityQueryTestsAComponent : Component; + + [RegisterComponent] + public sealed partial class EntityQueryTestsBComponent : Component; +} diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index b36fa6d1b2a..d6396a0815a 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -2308,6 +2308,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp)) + return false; + + Current = (id, comp); + return true; + } + } } /// @@ -2377,6 +2399,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2)) + return false; + + Current = (id, comp1, comp2); + return true; + } + } } /// @@ -2461,6 +2505,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3)) + return false; + + Current = (id, comp1, comp2, comp3); + return true; + } + } } /// @@ -2558,6 +2624,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(EntityQueryEnumerator enumerator) + { + private EntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3, out var comp4)) + return false; + + Current = (id, comp1, comp2, comp3, comp4); + return true; + } + } } #endregion @@ -2640,6 +2728,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp)) + return false; + + Current = (id, comp); + return true; + } + } } /// @@ -2700,6 +2810,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2)) + return false; + + Current = (id, comp1, comp2); + return true; + } + } } /// @@ -2774,6 +2906,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3)) + return false; + + Current = (id, comp1, comp2, comp3); + return true; + } + } } /// @@ -2860,6 +3014,28 @@ public void Dispose() { _traitDict.Dispose(); } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + public struct Enumerator(AllEntityQueryEnumerator enumerator) + { + private AllEntityQueryEnumerator _enumerator = enumerator; + + public Entity Current { get; private set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (!_enumerator.MoveNext(out var id, out var comp1, out var comp2, out var comp3, out var comp4)) + return false; + + Current = (id, comp1, comp2, comp3, comp4); + return true; + } + } } #endregion From 7b25f1e2564a1dae8546207679f351696487fa6e Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:59:35 +1200 Subject: [PATCH 076/178] Fix DataRecord serialization (#6619) --- .../Serialization/DataRecordTest.cs | 280 +++++++++++++++--- .../Serialization/DataStructTest.cs | 37 +++ .../SerializationManager.Instantiation.cs | 78 ++++- 3 files changed, 352 insertions(+), 43 deletions(-) diff --git a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs index 7f160205d77..c6e24003bfd 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataRecordTest.cs @@ -15,16 +15,79 @@ public sealed partial class DataRecordTest : OurSerializationTest public partial record TwoIntRecord(int aTest, int AnotherTest); [DataRecord] - public partial record OneByteOneDefaultIntRecord(byte A, int B = 5); + public partial record struct TwoIntRecordStruct(int aTest, int AnotherTest); [DataRecord] - public partial record OneLongRecord(long A); + public partial record PrimitiveRecord( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneLongDefaultRecord(long A = 5); + public partial record struct PrimitiveRecordStruct( + bool Bool, + byte Byte, + sbyte Sbyte, + char Char, + //decimal Decimal, + double Double, + float Float, + int Int, + uint Uint, + nint Nint, + nuint Nuint, + long Long, + ulong Ulong, + short Short, + ushort UShort); [DataRecord] - public partial record OneULongRecord(ulong A); + public partial record PrimitiveDefaultsRecord( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); + + [DataRecord] + public partial record struct PrimitiveDefaultsRecordStruct( + bool Bool = true, + byte Byte = byte.MaxValue, + sbyte Sbyte = sbyte.MinValue, + char Char = 'A', + //decimal Decimal = -1, + double Double = -1d, + float Float = -1f, + int Int = int.MinValue, + uint Uint = uint.MaxValue, + nint Nint = int.MinValue, + nuint Nuint = uint.MaxValue, + long Long = long.MinValue, + ulong Ulong = ulong.MaxValue, + short Short = short.MinValue, + ushort UShort = ushort.MaxValue); [PrototypeRecord("emptyTestPrototypeRecord")] public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototype; @@ -32,6 +95,9 @@ public partial record PrototypeRecord([field: IdDataField] string ID) : IPrototy [DataRecord] public partial record IntStructHolder(IntStruct Struct); + [DataRecord] + public partial record struct IntStructHolderStruct(IntStruct Struct); + [DataDefinition] public partial struct IntStruct { @@ -46,6 +112,9 @@ public IntStruct(int value) [DataRecord] public partial record TwoIntStructHolder(IntStruct Struct1, IntStruct Struct2); + [DataRecord] + public partial record struct TwoIntStructHolderStruct(IntStruct Struct1, IntStruct Struct2); + [DataRecord] public partial record struct DataRecordStruct(IntStruct Struct, string String, int Integer); @@ -58,6 +127,18 @@ public partial record struct DataRecordWithProperties public float X => Position.X; } + [DataRecord] + public partial record struct DataRecordWithDefaultFields() + { + public int A = 1; + } + + [DataRecord] + public partial record struct DataRecordWithDefaultFields2(int A = 1) + { + public int B = 2; + } + [DataRecord] public readonly partial record struct ReadonlyDataRecord { @@ -98,61 +179,178 @@ public void TwoIntRecordTest() } [Test] - public void OneByteOneDefaultIntRecordTest() + public void TwoIntRecordStructTest() { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"aTest", "1"}, + {"anotherTest", "2"} + }; + + var val = Serialization.Read(mapping); Assert.Multiple(() => { - Assert.That(val.A, Is.EqualTo(1)); - Assert.That(val.B, Is.EqualTo(5)); + Assert.That(val.aTest, Is.EqualTo(1)); + Assert.That(val.AnotherTest, Is.EqualTo(2)); }); - } - [Test] - public void OneLongRecordTest() - { - var mapping = new MappingDataNode {{"a", "1"}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var newMapping = Serialization.WriteValueAs(val); - Assert.That(val.A, Is.EqualTo(1)); - } + Assert.Multiple(() => + { + Assert.That(newMapping, Has.Count.EqualTo(2)); - [Test] - public void OneLongMinValueRecordTest() - { - var mapping = new MappingDataNode {{"a", long.MinValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + Assert.That(newMapping.TryGet("aTest", out var aTestNode)); + Assert.That(aTestNode!.Value, Is.EqualTo("1")); - Assert.That(val.A, Is.EqualTo(long.MinValue)); + Assert.That(newMapping.TryGet("anotherTest", out var anotherTestNode)); + Assert.That(anotherTestNode!.Value, Is.EqualTo("2")); + }); } [Test] - public void OneLongMaxValueRecordTest() + public void PrimitiveRecordTest() { - var mapping = new MappingDataNode {{"a", long.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode(); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(long.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(false)); + Assert.That(val2.Bool, Is.EqualTo(false)); + Assert.That(val1.Byte, Is.EqualTo(0)); + Assert.That(val2.Byte, Is.EqualTo(0)); + Assert.That(val1.Sbyte, Is.EqualTo(0)); + Assert.That(val2.Sbyte, Is.EqualTo(0)); + Assert.That(val1.Char, Is.EqualTo(default(char))); + Assert.That(val2.Char, Is.EqualTo(default(char))); + //Assert.That(val1.Decimal, Is.EqualTo(0)); + //Assert.That(val2.Decimal, Is.EqualTo(0)); + Assert.That(val1.Double, Is.EqualTo(0)); + Assert.That(val2.Double, Is.EqualTo(0)); + Assert.That(val1.Float, Is.EqualTo(0)); + Assert.That(val2.Float, Is.EqualTo(0)); + Assert.That(val1.Int, Is.EqualTo(0)); + Assert.That(val2.Int, Is.EqualTo(0)); + Assert.That(val1.Uint, Is.EqualTo(0)); + Assert.That(val2.Uint, Is.EqualTo(0)); + Assert.That(val1.Nint, Is.EqualTo((nint) 0)); + Assert.That(val2.Nint, Is.EqualTo((nint) 0)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) 0)); + Assert.That(val1.Long, Is.EqualTo(0)); + Assert.That(val2.Long, Is.EqualTo(0)); + Assert.That(val1.Ulong, Is.EqualTo(0)); + Assert.That(val2.Ulong, Is.EqualTo(0)); + Assert.That(val1.Short, Is.EqualTo(0)); + Assert.That(val2.Short, Is.EqualTo(0)); + Assert.That(val1.UShort, Is.EqualTo(0)); + Assert.That(val2.UShort, Is.EqualTo(0)); + }); } [Test] - public void OneLongDefaultRecordTest() + public void PrimitiveDefaultsRecordTest() { var mapping = new MappingDataNode(); - var val = Serialization.Read(mapping, notNullableOverride: true); + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(5)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val1.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val2.Nint, Is.EqualTo((nint) int.MinValue)); + Assert.That(val1.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val2.Nuint, Is.EqualTo((nuint) uint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] - public void OneULongRecordMaxValueTest() + public void PrimitiveRecordMinMaxValueTest() { - var mapping = new MappingDataNode {{"a", ulong.MaxValue.ToString()}}; - var val = Serialization.Read(mapping, notNullableOverride: true); + var mapping = new MappingDataNode + { + {"bool", "true"}, + {"byte", byte.MaxValue.ToString()}, + {"sbyte", sbyte.MinValue.ToString()}, + {"char", "A"}, + //{"decimal", "-1"}, + {"double", "-1"}, + {"float", "-1"}, + {"int", int.MinValue.ToString()}, + {"uint", uint.MaxValue.ToString()}, + // TODO SERIALIZATION add nint yaml serializer? + //{"nint", nint.MinValue.ToString()}, + //{"nuint", nuint.MinValue.ToString()}, + {"long", long.MinValue.ToString()}, + {"ulong", ulong.MaxValue.ToString()}, + {"short", short.MinValue.ToString()}, + {"ushort", ushort.MaxValue.ToString()}, + }; + var val1 = Serialization.Read(mapping, notNullableOverride: true); + var val2 = Serialization.Read(mapping); - Assert.That(val.A, Is.EqualTo(ulong.MaxValue)); + Assert.Multiple(() => + { + Assert.That(val1.Bool, Is.EqualTo(true)); + Assert.That(val2.Bool, Is.EqualTo(true)); + Assert.That(val1.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val2.Byte, Is.EqualTo(byte.MaxValue)); + Assert.That(val1.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val2.Sbyte, Is.EqualTo(sbyte.MinValue)); + Assert.That(val1.Char, Is.EqualTo('A')); + Assert.That(val2.Char, Is.EqualTo('A')); + //Assert.That(val1.Decimal, Is.EqualTo(-1)); + //Assert.That(val2.Decimal, Is.EqualTo(-1)); + Assert.That(val1.Double, Is.EqualTo(-1)); + Assert.That(val2.Double, Is.EqualTo(-1)); + Assert.That(val1.Float, Is.EqualTo(-1)); + Assert.That(val2.Float, Is.EqualTo(-1)); + Assert.That(val1.Int, Is.EqualTo(int.MinValue)); + Assert.That(val2.Int, Is.EqualTo(int.MinValue)); + Assert.That(val1.Uint, Is.EqualTo(uint.MaxValue)); + Assert.That(val2.Uint, Is.EqualTo(uint.MaxValue)); + //Assert.That(val1.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val2.Nint, Is.EqualTo(nint.MinValue)); + //Assert.That(val1.Nuint, Is.EqualTo(nuint.MaxValue)); + //Assert.That(val2.Nuint, Is.EqualTo(nuint.MaxValue)); + Assert.That(val1.Long, Is.EqualTo(long.MinValue)); + Assert.That(val2.Long, Is.EqualTo(long.MinValue)); + Assert.That(val1.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val2.Ulong, Is.EqualTo(ulong.MaxValue)); + Assert.That(val1.Short, Is.EqualTo(short.MinValue)); + Assert.That(val2.Short, Is.EqualTo(short.MinValue)); + Assert.That(val1.UShort, Is.EqualTo(ushort.MaxValue)); + Assert.That(val2.UShort, Is.EqualTo(ushort.MaxValue)); + }); } [Test] @@ -164,6 +362,18 @@ public void PrototypeTest() Assert.That(val.ID, Is.EqualTo("ABC")); } + [Test] + public void DataRecordWithDefaultFieldsTest() + { + var mapping = new MappingDataNode (); + var val = Serialization.Read(mapping); + Assert.That(val.A, Is.EqualTo(1)); + + var val2 = Serialization.Read(mapping); + Assert.That(val2.A, Is.EqualTo(1)); + Assert.That(val2.B, Is.EqualTo(2)); + } + [Test] public void RegisterPrototypeTest() { @@ -186,8 +396,10 @@ public void IntStructHolderTest() } }; var val = Serialization.Read(mapping, notNullableOverride: true); + var structVal = Serialization.Read(mapping); Assert.That(val.Struct.Value, Is.EqualTo(42)); + Assert.That(structVal.Struct.Value, Is.EqualTo(42)); } [Test] diff --git a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs index d9f62d1cc46..5c5771f902e 100644 --- a/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/DataStructTest.cs @@ -9,19 +9,56 @@ internal sealed partial class DataStructTest : OurSerializationTest [DataDefinition] public partial struct DefaultIntDataStruct { + [DataField] public int A = 5; + [DataField] + public int B; + public DefaultIntDataStruct() { + B = 1; } } + [DataDefinition] + public partial struct DefaultIntDataStructNoConstructor + { + [DataField] + public int A = 5; + + [DataField] + public int B; + } + [Test] public void DefaultIntDataStructTest() { var mapping = new MappingDataNode(); var val = Serialization.Read(mapping); + var val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"a", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); + + Assert.That(val.A, Is.EqualTo(10)); + Assert.That(val.B, Is.EqualTo(1)); + Assert.That(val2.A, Is.EqualTo(10)); + Assert.That(val2.B, Is.EqualTo(0)); + + mapping = new MappingDataNode {{"b", "10"}}; + val = Serialization.Read(mapping); + val2 = Serialization.Read(mapping); Assert.That(val.A, Is.EqualTo(5)); + Assert.That(val.B, Is.EqualTo(10)); + Assert.That(val2.A, Is.EqualTo(5)); + Assert.That(val2.B, Is.EqualTo(10)); } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs index 4202e9494cb..17e701d3017 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs @@ -47,11 +47,21 @@ private static void CreateClassInstantiator(ILGenerator generator, Type type) generator.Emit(OpCodes.Ret); } + /// + /// This generates IL code that will try to invoke a record's constructor by passing default values to any arguments. + /// private static void CreateRecordInstantiator(ILGenerator generator, Type type) { var constructors = type.GetConstructors(); if (constructors.Length == 0) - throw new ArgumentException($"Could not find a constructor for record class {type}"); + { + if (!type.IsValueType) + throw new ArgumentException($"Could not find a constructor for record class {type}"); + + // Handle constructorless data record struct by treating it like a normal struct + CreateValueTypeInstantiator(generator, type); + return; + } var constructor = constructors[0]; foreach (var parameter in constructor.GetParameters()) @@ -60,11 +70,61 @@ private static void CreateRecordInstantiator(ILGenerator generator, Type type) if (parameterType.IsPrimitive) { - var defaultValue = Convert.ToInt64(parameter.HasDefaultValue ? parameter.DefaultValue! : 0); - generator.Emit(OpCodes.Ldc_I4, defaultValue); - if (parameterType == typeof(long) || parameterType == typeof(ulong)) - generator.Emit(OpCodes.Conv_I8); + if (parameterType == typeof(decimal)) + { + // I CBF figuring out how to support them, so fuck it. + throw new NotSupportedException($"Record class {type} contains decimals. DataRecords don't currently support decimals."); + // If anyone wants to try, a value of 0 looks like this in IL: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::Zero + // While a default value of -1 uses another static field: + // > ldsfld valuetype [System.Runtime]System.Decimal [System.Runtime]System.Decimal::MinusOne + } + + if (parameterType == typeof(float)) + { + var floatDefault = parameter.HasDefaultValue ? (float) parameter.DefaultValue! : 0f; + generator.Emit(OpCodes.Ldc_R4, floatDefault); + } + else if (parameterType == typeof(double)) + { + var doubleDefault = parameter.HasDefaultValue ? (double) parameter.DefaultValue! : 0d; + generator.Emit(OpCodes.Ldc_R8, doubleDefault); + } + else if (parameterType == typeof(nint) || parameterType == typeof(nuint)) + { + int nintDefault = parameter.HasDefaultValue ? (int)Convert.ToInt64(parameter.DefaultValue) : 0; + generator.Emit(OpCodes.Ldc_I4, nintDefault); + + if (parameterType == typeof(nuint) && nintDefault < 0) // I'm only like 50% sure this is correct, but it makes the tests pass, so.... + generator.Emit(OpCodes.Conv_U); + else + generator.Emit(OpCodes.Conv_I); + } + else if (parameterType == typeof(long) || parameterType == typeof(ulong)) + { + var longDefault = 0L; + if (parameter.HasDefaultValue) + { + longDefault = parameterType == typeof(ulong) + ? (long) (ulong) parameter.DefaultValue! + : Convert.ToInt64(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I8, longDefault); + } + else + { + var intDefault = 0; + if (parameter.HasDefaultValue) + { + intDefault = parameterType == typeof(uint) + ? (int) (uint) parameter.DefaultValue! + : Convert.ToInt32(parameter.DefaultValue); + } + + generator.Emit(OpCodes.Ldc_I4, intDefault); + } } else if (parameterType.IsValueType) { @@ -104,13 +164,13 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< var generator = method.GetILGenerator(); - if (type.IsValueType) + if (isRecord) { - CreateValueTypeInstantiator(generator, type); + CreateRecordInstantiator(generator, type); } - else if (isRecord) + else if (type.IsValueType) { - CreateRecordInstantiator(generator, type); + CreateValueTypeInstantiator(generator, type); } else { From c0e29a3ad4c7bc80071971960e0ccc82063493cc Mon Sep 17 00:00:00 2001 From: tiders-shore <219183335+tiders-shore@users.noreply.github.com> Date: Sat, 27 Jun 2026 16:00:52 +0800 Subject: [PATCH 077/178] Add PhysicsBodyStatusChangedEvent (#6614) * Initial commit * Moved PhysicsBodyStatusChangedEvent after status update --- .../Events/PhysicsBodyStatusChangedEvent.cs | 13 +++++++++++++ .../Systems/SharedPhysicsSystem.Components.cs | 15 ++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs diff --git a/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs b/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs new file mode 100644 index 00000000000..5f1d22bf72b --- /dev/null +++ b/Robust.Shared/Physics/Events/PhysicsBodyStatusChangedEvent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Physics.Components; + +namespace Robust.Shared.Physics.Events; + +// This is called PhysicsBody and not just Body to differentiate from a potential literal living body +/// +/// Raised on an entity when it's 's is being changed. +/// Raised after is set to the new status. +/// +/// of the entity which this event was directed at. +[ByRefEvent] +public readonly record struct PhysicsBodyStatusChangedEvent(PhysicsComponent PhysicsComponent, BodyStatus OldStatus, BodyStatus NewStatus); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 88670635e7e..1c13c149d20 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -153,7 +153,7 @@ private void OnPhysicsHandleState(EntityUid uid, PhysicsComponent component, ref SetSleepingAllowed(uid, component, newState.SleepingAllowed, dirty: false); SetFixedRotation(uid, newState.FixedRotation, body: component, dirty: false); SetCanCollide(uid, newState.CanCollide, body: component, dirty: false); - component.BodyStatus = newState.Status; + SetBodyStatus(uid, component, newState.Status, dirty: false); SetLinearVelocity(uid, newState.LinearVelocity, dirty: false, body: component, manager: manager); SetAngularVelocity(uid, newState.AngularVelocity, dirty: false, body: component, manager: manager); @@ -224,7 +224,7 @@ public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, FixturesComponent return; } - SetLinearVelocity(uid,body.LinearVelocity + impulse * body._invMass, body: body); + SetLinearVelocity(uid, body.LinearVelocity + impulse * body._invMass, body: body); } public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, Vector2 point, FixturesComponent? manager = null, PhysicsComponent? body = null) @@ -342,7 +342,7 @@ public void ResetMassData(EntityUid uid, FixturesComponent? manager = null, Phys var oldCenter = body._localCenter; body._localCenter = localCenter; - if (((int) body.BodyType & (int) (BodyType.Kinematic | BodyType.Static)) == 0) + if (((int)body.BodyType & (int)(BodyType.Kinematic | BodyType.Static)) == 0) { // Update center of mass velocity. var comVelocityDiff = Vector2Helpers.Cross(body.AngularVelocity, localCenter - oldCenter); @@ -549,7 +549,12 @@ public void SetBodyStatus(EntityUid uid, PhysicsComponent body, BodyStatus statu if (body.BodyStatus == status) return; + var oldStatus = body.BodyStatus; body.BodyStatus = status; + + var ev = new PhysicsBodyStatusChangedEvent(body, oldStatus, status); + RaiseLocalEvent(uid, ref ev); + if (dirty) DirtyField(uid, body, nameof(PhysicsComponent.BodyStatus)); } @@ -770,7 +775,7 @@ public Box2 GetWorldAABB(EntityUid uid, FixturesComponent? manager = null, Physi var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); - var transform = new Transform(worldPos, (float) worldRot.Theta); + var transform = new Transform(worldPos, (float)worldRot.Theta); var bounds = new Box2(transform.Position, transform.Position); @@ -797,7 +802,7 @@ public Box2 GetHardAABB(EntityUid uid, FixturesComponent? manager = null, Physic var (worldPos, worldRot) = _transform.GetWorldPositionRotation(xform); - var transform = new Transform(worldPos, (float) worldRot.Theta); + var transform = new Transform(worldPos, (float)worldRot.Theta); var bounds = new Box2(transform.Position, transform.Position); From 9c45f8c86f255b1dccce4fcbbd591f592502f22f Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sat, 27 Jun 2026 04:14:21 -0400 Subject: [PATCH 078/178] Simplify generated code check in `AccessAnalyzer` (#6581) Replace generated code check with GeneratedCodeAnalysisFlags --- Robust.Analyzers/AccessAnalyzer.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Robust.Analyzers/AccessAnalyzer.cs b/Robust.Analyzers/AccessAnalyzer.cs index 3068d806dd5..7a37281617e 100644 --- a/Robust.Analyzers/AccessAnalyzer.cs +++ b/Robust.Analyzers/AccessAnalyzer.cs @@ -1,7 +1,5 @@ -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -14,7 +12,6 @@ namespace Robust.Analyzers public class AccessAnalyzer : DiagnosticAnalyzer { private const string AccessAttributeType = "Robust.Shared.Analyzers.AccessAttribute"; - private const string RobustAutoGeneratedAttributeType = "Robust.Shared.Analyzers.RobustAutoGeneratedAttribute"; private const string PureAttributeType = "System.Diagnostics.Contracts.PureAttribute"; [SuppressMessage("ReSharper", "RS2008")] @@ -32,7 +29,7 @@ public class AccessAnalyzer : DiagnosticAnalyzer public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterOperationAction(CheckFriendship, OperationKind.FieldReference, @@ -76,20 +73,11 @@ private void CheckFriendship(OperationAnalysisContext context) // Get the attributes var friendAttribute = context.Compilation.GetTypeByMetadataName(AccessAttributeType); - var autoGenAttribute = context.Compilation.GetTypeByMetadataName(RobustAutoGeneratedAttributeType); // Get the type that is containing this expression, or, the type where this is happening. if (context.ContainingSymbol?.ContainingType is not {} accessingType) return; - // Should we ignore the access attempt due to the accessing type being auto-generated? - if (accessingType.GetAttributes().FirstOrDefault(a => - a.AttributeClass != null && - a.AttributeClass.Equals(autoGenAttribute, SymbolEqualityComparer.Default)) is { } attr) - { - return; - } - // Determine which type of access is happening here... Read, write or execute? var accessAttempt = DetermineAccess(context, targetAccess, operation); From 5dbf86fb93d53393819fe80a2b2e2548bb959a16 Mon Sep 17 00:00:00 2001 From: TemporalOroboros Date: Sat, 27 Jun 2026 01:33:48 -0700 Subject: [PATCH 079/178] IMapManager to hospice care (#6579) * Move MapManager queries to SharedMapSystem Moves all variants of FindGridsIntersecting/TryFindGridAt to SharedMapSystem Hollows out the MapManager methods and converts them into relays to SharedMapSystem * Move CreateGrid to SharedMapSystem Moves the functionality for CreateGrid and its variants to SharedMapSystem Hollows out the MapManager methods and converts them into relays for the SharedMapSystem methods Obsoletes them too Also moves over the GetAllMapGrids and GetAllGrids methods * Move RaiseOnTileChanged to SharedMapSystem Also moves the SuppressOnTileChanged flag member to SharedMapSystem Hollows out and obsoletes the MapManager versions * Move default value constants to SharedMapSystem * Converts map pausing events into LocalizedEntityCommands * Move MapManager related delegates/structs to SharedMapSystem Moves the GridCreationOptions struct to the same namespace as SharedMapSystem and converts it into a record struct Move the GridCallback delegates to the same namespace as SharedMapSystem * Move CullDeletionHistory to SharedMapSystem Well, that was less painful than I thought it would be * Actually obsolete the NetworkedMapManager method * Rename file * Doc comments for new SharedMapSystem methods * Doc comment * Doc comments * Fix access --- Resources/Locale/en-US/commands.ftl | 9 + Robust.Server/GameStates/PvsSystem.Chunks.cs | 2 +- Robust.Server/GameStates/PvsSystem.cs | 4 +- .../Systems/SharedMapSystem.Grid.Queries.cs | 613 ++++++++++++++++++ .../Systems/SharedMapSystem.Grid.cs | 91 ++- .../GameObjects/Systems/SharedMapSystem.cs | 11 + .../Map/Commands/MapPausingCommands.cs | 90 +++ Robust.Shared/Map/IMapManager.cs | 49 +- Robust.Shared/Map/IMapManagerInternal.cs | 3 + .../Map/MapManager.GridCollection.cs | 78 +-- Robust.Shared/Map/MapManager.MapCollection.cs | 16 +- Robust.Shared/Map/MapManager.Pause.cs | 78 +-- Robust.Shared/Map/MapManager.Queries.cs | 262 ++------ Robust.Shared/Map/MapManager.cs | 21 +- Robust.Shared/Map/NetworkedMapManager.cs | 14 +- 15 files changed, 937 insertions(+), 404 deletions(-) create mode 100644 Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs create mode 100644 Robust.Shared/Map/Commands/MapPausingCommands.cs diff --git a/Resources/Locale/en-US/commands.ftl b/Resources/Locale/en-US/commands.ftl index 4d4e3264618..fcafc42eedf 100644 --- a/Resources/Locale/en-US/commands.ftl +++ b/Resources/Locale/en-US/commands.ftl @@ -306,6 +306,15 @@ cmd-addmap-help = Usage: {$command} [pre-init] cmd-rmmap-desc = Removes a map from the world. You cannot remove nullspace. cmd-rmmap-help = Usage: {$command} +cmd-pausemap-desc = Pauses a map, pausing all simulation processing on it. +cmd-pausemap-help = Usage: pausemap + +cmd-unpausemap-desc = Unpauses a map, resuming all simulation processing on it. +cmd-unpausemap-help = Usage: unpausemap + +cmd-querymappaused-desc = Check whether a map is paused or not. +cmd-querymappaused-help = Usage: querymappaused + cmd-savegrid-desc = Serializes a grid to disk. cmd-savegrid-help = Usage: {$command} diff --git a/Robust.Server/GameStates/PvsSystem.Chunks.cs b/Robust.Server/GameStates/PvsSystem.Chunks.cs index 99466b1f6b7..a270c7410b7 100644 --- a/Robust.Server/GameStates/PvsSystem.Chunks.cs +++ b/Robust.Server/GameStates/PvsSystem.Chunks.cs @@ -133,7 +133,7 @@ private void GetVisibleChunks(Entity eye, _grids.Clear(); var rangeVec = new Vector2(range, range); var box = new Box2(viewPos - rangeVec, viewPos + rangeVec); - _mapManager.FindGridsIntersecting(map, box, ref _grids, approx: true, includeMap: false); + _maps.FindGridsIntersecting(map, box, ref _grids, approx: true, includeMap: false); foreach (var (grid, _) in _grids) { diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs index 2095023f220..02615e9ea73 100644 --- a/Robust.Server/GameStates/PvsSystem.cs +++ b/Robust.Server/GameStates/PvsSystem.cs @@ -28,7 +28,6 @@ namespace Robust.Server.GameStates; internal sealed partial class PvsSystem : EntitySystem { [Dependency] private IConfigurationManager _configManager = default!; - [Dependency] private INetworkedMapManager _mapManager = default!; [Dependency] private IServerEntityNetworkManager _netEntMan = default!; [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IParallelManager _parallelManager = default!; @@ -40,6 +39,7 @@ internal sealed partial class PvsSystem : EntitySystem [Dependency] private IParallelManagerInternal _parallelMgr = default!; [Dependency] private PvsOverrideSystem _pvsOverride = default!; [Dependency] private IServerReplayRecordingManager _replay = default!; + [Dependency] private SharedMapSystem _maps = default!; // TODO make this a cvar. Make it in terms of seconds and tie it to tick rate? // Main issue is that I CBF figuring out the logic for handling it changing mid-game. @@ -288,7 +288,7 @@ private void CullDeletionHistory(GameTick oldestAck) { using var _ = Histogram.WithLabels("Cull History").NewTimer(); CullDeletionHistoryUntil(oldestAck); - _mapManager.CullDeletionHistory(oldestAck); + _maps.CullDeletionHistory(oldestAck); } private void GetEntityStates(PvsSession session) diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs new file mode 100644 index 00000000000..ae3165b0543 --- /dev/null +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs @@ -0,0 +1,613 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Map.Enumerators; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Shapes; +using Transform = Robust.Shared.Physics.Transform; + +namespace Robust.Shared.GameObjects; + +public abstract partial class SharedMapSystem +{ + + /// + /// Whether and its extended family should only approximately check for intersection by default. + /// + public const bool Approximate = false; + + /// + /// Whether and its extended family should also check the map itself by default. + /// + public const bool IncludeMap = true; + + #region TryFindGridAt + + /// + /// Attempts to find a grid which overlaps with a given position on a given map. + /// If the map is itself a grid and there is no other grid overlapping with the given position this will return the map itself as such a grid. + /// + /// The uid of the map to search for a valid grid. + /// The exact position within and relative to the map to search for a valid grid. + /// Returns the uid of the grid found, if any. + /// Returns the component of the grid found, if any. + /// True if a grid overlapping with the given position within the given map was found, or false otherwise. + public bool TryFindGridAt(EntityUid mapEnt, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + var rangeVec = new Vector2(0.2f, 0.2f); + + // Need to enlarge the AABB by at least the grid shrinkage size. + var aabb = new Box2(worldPos - rangeVec, worldPos + rangeVec); + + uid = EntityUid.Invalid; + grid = null; + var state = (uid, grid, worldPos, this, _transform); + + FindGridsIntersecting(mapEnt, aabb, ref state, static (EntityUid iUid, MapGridComponent iGrid, ref ( + EntityUid uid, + MapGridComponent? grid, + Vector2 worldPos, + SharedMapSystem mapSystem, + SharedTransformSystem xformSystem) tuple) => + { + // Turn the worldPos into a localPos and work out the relevant chunk we need to check + // This is much faster than iterating over every chunk individually. + // (though now we need some extra calcs up front). + + // Doesn't use WorldBounds because it's just an AABB. + var matrix = tuple.xformSystem.GetInvWorldMatrix(iUid); + var localPos = Vector2.Transform(tuple.worldPos, matrix); + + // NOTE: + // If you change this to use fixtures instead (i.e. if you want half-tiles) then you need to make sure + // you account for the fact that fixtures are shrunk slightly! + var chunkIndices = GetChunkIndices(localPos, iGrid.ChunkSize); + + if (!iGrid.Chunks.TryGetValue(chunkIndices, out var chunk)) + return true; + + var chunkRelative = GetChunkRelative(localPos, iGrid.ChunkSize); + var chunkTile = chunk.GetTile(chunkRelative); + + if (chunkTile.IsEmpty) + return true; + + tuple.uid = iUid; + tuple.grid = iGrid; + return false; + }, approx: true, includeMap: false); + + if (state.grid == null && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) + { + uid = mapEnt; + grid = mapGrid; + return true; + } + + uid = state.uid; + grid = state.grid; + return grid != null; + } + + /// + /// The id of the map to search for a valid grid. + public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + if (TryGetMap(mapId, out var map)) + return TryFindGridAt(map.Value, worldPos, out uid, out grid); + + uid = default; + grid = null; + return false; + } + + /// + /// The map position to search for a valid grid. + public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) + { + return TryFindGridAt(mapCoordinates.MapId, mapCoordinates.Position, out uid, out grid); + } + + #endregion + + #region MapId + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + MapId mapId, + TShape shape, + Transform transform, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, shape, transform, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var map)) + FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2 worldAABB, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var map)) + FindGridsIntersecting(map.Value, worldAABB, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided collection. + /// + public void FindGridsIntersecting( + MapId mapId, + Box2Rotated worldBounds, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + if (TryGetMap(mapId, out var mapEnt)) + FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx: approx, includeMap: includeMap); + } + + #endregion + + #region EntityUid + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given region to the provided list. + /// + /// The shape of the region to check. + /// The transform, relative to the map, of the region to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Transform transform, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) where TShape : IPhysShape + { + FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided collection. + /// + /// A set of regions to check. + /// The transform, relative to the map, of the regions to check. + public void FindGridsIntersecting( + EntityUid mapEnt, + List shapes, + Transform transform, + ref List> entities, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + foreach (var shape in shapes) + { + FindGridsIntersecting(mapEnt, shape, transform, ref entities, approx: approx, includeMap: includeMap); + } + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2 worldAABB, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldAABB); + FindGridsIntersecting(mapEnt, shape, worldAABB, Robust.Shared.Physics.Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + ref TState state, + GridCallback callback, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + public void FindGridsIntersecting( + EntityUid mapEnt, + Box2Rotated worldBounds, + ref List> grids, + bool approx = Approximate, + bool includeMap = IncludeMap) + { + var shape = new SlimPolygon(worldBounds); + FindGridsIntersecting(mapEnt, shape, Robust.Shared.Physics.Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + } + + #endregion + + /// + /// Enumerates all of the grids located on a given map. + /// + public IEnumerable> GetAllGrids(MapId mapId) + { + var query = AllEntityQuery(); + while (query.MoveNext(out var uid, out var grid, out var xform)) + { + if (xform.MapID != mapId) + continue; + + yield return (uid, grid); + } + } + + /// + /// This version only provides the component without the uid and should not be used. + /// + /// + [Obsolete("use GetAllGrids instead")] + public IEnumerable GetAllMapGrids(MapId mapId) + { + var query = AllEntityQuery(); + while (query.MoveNext(out var grid, out var xform)) + { + if (xform.MapID == mapId) + yield return grid; + } + } + + /// + /// Adds every grid on the specified map which intersects the given regions to the provided list. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + [Access(typeof(MapManager), Other = AccessPermissions.None)] + public void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + ref List> grids, + bool approx, + bool includeMap) where TShape : IPhysShape + { + var state = grids; + FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, + static (EntityUid uid, MapGridComponent grid, ref List> state) => + { + state.Add((uid, grid)); + return true; + }, + approx, includeMap + ); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + private void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + GridCallback callback, + bool approx, + bool includeMap) where TShape : IPhysShape + { + var state = callback; + FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, + static (EntityUid uid, MapGridComponent grid, ref GridCallback state) => state.Invoke(uid, grid), + approx, includeMap + ); + } + + /// + /// Invokes the provided callback on every grid on the specified map which intersect the given region. + /// Allows providing some additional to pass to the callback when it is invoked. + /// + /// The shape of the region to check. + /// The world-local axis aligned bounding box of the region to check. + /// The transform, relative to the map, of the region to check. + private void FindGridsIntersecting( + EntityUid mapEnt, + TShape shape, + Box2 worldAABB, + Transform transform, + ref TState state, + GridCallback callback, + bool approx, + bool includeMap) where TShape : IPhysShape + { + if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree)) + return; + + if (includeMap && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) + { + callback(mapEnt, mapGrid, ref state); + } + + var gridState = new GridQueryState( + callback, + state, + worldAABB, + shape, + transform, + gridTree.Tree, + this, + _transform, + approx); + + gridTree.Tree.Query(ref gridState, static (ref GridQueryState state, DynamicTree.Proxy proxy) => + { + // Even for approximate we'll check if any chunks roughly overlap. + var data = state.Tree.GetUserData(proxy); + var gridInvMatrix = state.TransformSystem.GetInvWorldMatrix(data.Uid); + var localAABB = gridInvMatrix.TransformBox(state.WorldAABB); + + var overlappingChunks = state.MapSystem.GetLocalMapChunks(data.Uid, data.Grid, localAABB); + + if (state.Approximate) + { + if (!overlappingChunks.MoveNext(out _)) + return true; + } + else if (!state.MapSystem.IsIntersecting(overlappingChunks, state.Shape, state.Transform, (data.Uid, data.Fixtures))) + { + return true; + } + + var callbackState = state.State; + var result = state.Callback(data.Uid, data.Grid, ref callbackState); + state.State = callbackState; + + return result; + }, worldAABB); + + // By-ref things + state = gridState.State; + } + + /// + /// Tests whether any of a collection of grid chunks intersect with a given region. + /// + private bool IsIntersecting( + ChunkEnumerator enumerator, + TShape shape, + Transform shapeTransform, + Entity grid) where TShape : IPhysShape + { + var gridTransform = _physics.GetPhysicsTransform(grid); + + while (enumerator.MoveNext(out var chunk)) + { + foreach (var id in chunk.Fixtures) + { + var fixture = grid.Comp.Fixtures[id]; + + for (var j = 0; j < fixture.Shape.ChildCount; j++) + { + if (_manifolds.TestOverlap(shape, 0, fixture.Shape, j, shapeTransform, gridTransform)) + { + return true; + } + } + } + } + + return false; + } + + private record struct GridQueryState( + GridCallback Callback, + TState State, + Box2 WorldAABB, + TShape Shape, + Transform Transform, + B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree, + SharedMapSystem MapSystem, + SharedTransformSystem TransformSystem, + bool Approximate + ) where TShape : IPhysShape; +} + +public delegate bool GridCallback(EntityUid gridUid, MapGridComponent gridComp); +public delegate bool GridCallback(EntityUid gridUid, MapGridComponent gridComp, ref TState state); diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs index 5e4709ffc36..2188eb2f514 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.cs @@ -22,6 +22,52 @@ namespace Robust.Shared.GameObjects; public abstract partial class SharedMapSystem { + #region CreateGrid + + /// + /// Creates a new grid entity on a given map. + /// + public Entity CreateGridEntity(MapId mapId, GridCreateOptions? options = null) + { + return CreateGridEntity(GetMap(mapId), options); + } + + /// + /// Creates a new grid entity on a given map. + /// + public Entity CreateGridEntity(EntityUid mapEnt, GridCreateOptions? options = null) + { + options ??= GridCreateOptions.Default; + return CreateGridInternal(mapEnt, options.Value); + } + + protected Entity CreateGridInternal(EntityUid mapEnt, GridCreateOptions options) + { + var gridEnt = EntityManager.CreateEntityUninitialized(null); + + var grid = EnsureComp(gridEnt); + grid.ChunkSize = options.ChunkSize; + + Log.Debug("Binding new grid {gridEnt}"); + + //TODO: This is a hack to get TransformComponent.MapId working before entity states + //are applied. After they are applied the parent may be different, but the MapId will + //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to + //use in transform states anytime before the state parent is properly set. + _transform.SetParent(gridEnt, mapEnt); + + var meta = _metaQuery.GetComponent(gridEnt); + EntityManager.System().SetEntityName(gridEnt, $"grid", meta); + EntityManager.InitializeComponents(gridEnt, meta); + EntityManager.StartComponents(gridEnt); + // Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized. + // I don't know whether that is intentional or not. + + return (gridEnt, grid); + } + + #endregion + #region Chunk helpers [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -323,7 +369,7 @@ private void ApplyChunkData( var gridIndices = deletedChunk.ChunkTileToGridTile((x, y)); var newTileRef = new TileRef(uid, gridIndices, Tile.Empty); - _mapInternal.RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index); + RaiseOnTileChanged(gridEnt, newTileRef, oldTile, index); } } @@ -485,6 +531,20 @@ private void GetFullState(EntityUid uid, MapGridComponent component, ref Compone #endif } + /// + /// Prunes tracked grid chunk deletions older than some given game tick. + /// + public void CullDeletionHistory(GameTick upToTick) + { + var query = AllEntityQuery(); + + while (query.MoveNext(out var grid)) + { + var chunks = grid.ChunkDeletionHistory; + chunks.RemoveAll(t => t.tick < upToTick); + } + } + private void OnGridAdd(EntityUid uid, MapGridComponent component, ComponentAdd args) { var msg = new GridAddEvent(uid); @@ -859,7 +919,7 @@ public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIn // Suppress sending out events for each tile changed // We're going to send them all out together at the end - MapManager.SuppressOnTileChanged = true; + SuppressOnTileChanged = true; foreach (var (gridIndices, tile) in tiles) { @@ -896,7 +956,7 @@ public void SetTiles(EntityUid uid, MapGridComponent grid, List<(Vector2i GridIn RegenerateCollision(uid, grid, modified); // Back to normal - MapManager.SuppressOnTileChanged = false; + SuppressOnTileChanged = false; } public TilesEnumerator GetLocalTilesEnumerator(EntityUid uid, MapGridComponent grid, Box2 aabb, @@ -1644,10 +1704,10 @@ private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapCh // The map serializer currently sets tiles of unbound grids as part of the deserialization process // It properly sets SuppressOnTileChanged so that the event isn't spammed for every tile on the grid. // ParentMapId is not able to be accessed on unbound grids, so we can't even call this function for unbound grids. - if (!MapManager.SuppressOnTileChanged) + if (!SuppressOnTileChanged) { var newTileRef = new TileRef(uid, gridTile, newTile); - _mapInternal.RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices); + RaiseOnTileChanged((uid, grid), newTileRef, oldTile, mapChunk.Indices); } if (shapeChanged && !mapChunk.SuppressCollisionRegeneration) @@ -1656,6 +1716,18 @@ private void OnTileModified(EntityUid uid, MapGridComponent grid, MapChunk mapCh } } + /// + /// Raises on the provided grid unless is set. + /// + internal void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) + { + if (SuppressOnTileChanged) + return; + + var ev = new TileChangedEvent(entity, tileRef, oldTile, chunk); + EntityManager.EventBus.RaiseLocalEvent(entity.Owner, ref ev, true); + } + /// /// Iterates the local tiles of the specified data. /// @@ -1751,3 +1823,12 @@ public bool MoveNext(out TileRef tile) } } } + +/// +/// Additional parameters used when creating a new grid entity. +/// +/// The number of tiles long/wide the grids chunks should be. +public record struct GridCreateOptions(ushort ChunkSize) +{ + public readonly static GridCreateOptions Default = new(ChunkSize: 16); +} diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 2f99eb9c7ae..94486e8077e 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -8,6 +8,7 @@ using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Physics; +using Robust.Shared.Physics.Collision; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -22,6 +23,7 @@ public abstract partial class SharedMapSystem : EntitySystem [Dependency] private ITileDefinitionManager _tileMan = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] protected IMapManager MapManager = default!; + [Dependency] private IManifoldManager _manifolds = default!; [Dependency] private IMapManagerInternal _mapInternal = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private FixtureSystem _fixtures = default!; @@ -34,9 +36,18 @@ public abstract partial class SharedMapSystem : EntitySystem private EntityQuery _gridQuery; private EntityQuery _metaQuery; private EntityQuery _xformQuery; + [Dependency] EntityQuery _gridTreeQuery; internal Dictionary Maps { get; } = new(); + /// + /// If set, this prevents the from being raised when modifying grids. + /// + /// + /// Useful if you want to create a new grid, delete an existing grid, or bulk-modify tiles and don't want to spam ten billion individual tile-changed events. + /// + internal bool SuppressOnTileChanged { get; set; } + /// /// This hashset is used to try prevent MapId re-use. This is mainly for auto-assigned map ids. /// Loading a map with a specific id (e.g., the various mapping commands) may still result in an id being diff --git a/Robust.Shared/Map/Commands/MapPausingCommands.cs b/Robust.Shared/Map/Commands/MapPausingCommands.cs new file mode 100644 index 00000000000..2b74ac3c15a --- /dev/null +++ b/Robust.Shared/Map/Commands/MapPausingCommands.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Robust.Shared.Map.Commands; + +/// +/// Pauses a given map, halting all entity processing on it. +/// +public sealed partial class PauseMapCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "pausemap"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + _mapSystem.SetPaused(mapId, true); + } +} + +/// +/// Unpauses a given map, resuming all entity processing on it. +/// +public sealed partial class UnpauseMapCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "unpausemap"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + _mapSystem.SetPaused(mapId, false); + } +} + +/// +/// Checks whether a given map is currently paused. +/// +public sealed partial class QueryMapPausedCommand : LocalizedEntityCommands +{ + [Dependency] SharedMapSystem _mapSystem = default!; + public override string Command => "querymappaused"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError("Need to supply a valid MapId"); + return; + } + + var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); + + if (!_mapSystem.MapExists(mapId)) + { + shell.WriteError("That map does not exist."); + return; + } + + shell.WriteLine(_mapSystem.IsPaused(mapId).ToString()); + } +} diff --git a/Robust.Shared/Map/IMapManager.cs b/Robust.Shared/Map/IMapManager.cs index d0aaea4a5a4..fad26a165af 100644 --- a/Robust.Shared/Map/IMapManager.cs +++ b/Robust.Shared/Map/IMapManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; -using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Maths; @@ -11,23 +10,20 @@ namespace Robust.Shared.Map { - public delegate bool GridCallback(EntityUid uid, MapGridComponent grid); - - public delegate bool GridCallback(EntityUid uid, MapGridComponent grid, ref TState state); - /// /// This manages all the grids and maps in the world. Largely superseded by . /// [NotContentImplementable] public interface IMapManager { - public const bool Approximate = false; - public const bool IncludeMap = true; + public const bool Approximate = SharedMapSystem.Approximate; + public const bool IncludeMap = SharedMapSystem.IncludeMap; /// /// Should the OnTileChanged event be suppressed? This is useful for initially loading the map /// so that you don't spam an event for each of the million station tiles. /// + [Obsolete("use SharedMapSystem")] bool SuppressOnTileChanged { get; set; } /// @@ -70,41 +66,56 @@ public interface IMapManager void DeleteMap(MapId mapId); // ReSharper disable once MethodOverloadWithOptionalParameter + [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16); + [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options); + [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] MapGridComponent CreateGrid(MapId currentMapId); + [Obsolete("Use MapSystem")] Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null); + [Obsolete("Use MapSystem")] Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null); + [Obsolete("Use MapSystem")] IEnumerable GetAllMapGrids(MapId mapId); + [Obsolete("Use MapSystem")] IEnumerable> GetAllGrids(MapId mapId); #region MapId + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap); @@ -112,38 +123,48 @@ public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List #region MapEnt + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; /// /// Returns true if any grids overlap the specified shapes. /// + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, ref List> entities, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); + [Obsolete("Use MapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap); @@ -152,6 +173,7 @@ public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, #region TryFindGridAt + [Obsolete("Use MapSystem")] public bool TryFindGridAt( EntityUid mapEnt, Vector2 worldPos, @@ -161,12 +183,14 @@ public bool TryFindGridAt( /// /// Attempts to find the map grid under the map location. /// + [Obsolete("Use MapSystem")] public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid); /// /// Attempts to find the map grid under the map location. /// + [Obsolete("Use MapSystem")] public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid); @@ -231,16 +255,5 @@ public IEnumerable FindGridsIntersecting(MapId mapId, Box2Rota [Obsolete("Use MapSystem")] bool IsMapInitialized(MapId mapId); - - } - - public struct GridCreateOptions - { - public static readonly GridCreateOptions Default = new() - { - ChunkSize = 16 - }; - - public ushort ChunkSize; } } diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs index e6a5b5b3637..c2b19a20f21 100644 --- a/Robust.Shared/Map/IMapManagerInternal.cs +++ b/Robust.Shared/Map/IMapManagerInternal.cs @@ -1,3 +1,4 @@ +using System; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Maths; @@ -5,6 +6,7 @@ namespace Robust.Shared.Map { /// + [Obsolete] internal interface IMapManagerInternal : IMapManager { /// @@ -12,6 +14,7 @@ internal interface IMapManagerInternal : IMapManager /// /// A reference to the new tile. /// The old tile that got replaced. + [Obsolete("use SharedMapSystem")] void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk); } } diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs index cf23be33c1c..75502418251 100644 --- a/Robust.Shared/Map/MapManager.GridCollection.cs +++ b/Robust.Shared/Map/MapManager.GridCollection.cs @@ -1,42 +1,42 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Utility; -// All the obsolete warnings about GridId are probably useless here. -#pragma warning disable CS0618 - namespace Robust.Shared.Map; internal partial class MapManager { // ReSharper disable once MethodOverloadWithOptionalParameter + [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] public MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16) { - return CreateGrid(GetMapEntityIdOrThrow(currentMapId), chunkSize, default); + return CreateGridEntity(currentMapId, options: GridCreateOptions.Default with { ChunkSize = chunkSize }).Comp; } + [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] public MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options) { - return CreateGrid(GetMapEntityIdOrThrow(currentMapId), options.ChunkSize, default); + return CreateGridEntity(currentMapId, options: options).Comp; } + [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] public MapGridComponent CreateGrid(MapId currentMapId) { - return CreateGrid(currentMapId, GridCreateOptions.Default); + return CreateGridEntity(currentMapId, options: GridCreateOptions.Default).Comp; } + [Obsolete("use SharedMapSystem.CreateGridEntity")] public Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null) { - return CreateGridEntity(GetMapEntityIdOrThrow(currentMapId), options); + return MapSystem.CreateGridEntity(currentMapId, options: options); } + [Obsolete("use SharedMapSystem.CreateGridEntity")] public Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null) { - options ??= GridCreateOptions.Default; - return CreateGrid(map, options.Value.ChunkSize, default); + return MapSystem.CreateGridEntity(map, options: options); } [Obsolete("Use HasComponent(uid)")] @@ -45,28 +45,19 @@ public bool IsGrid(EntityUid uid) return EntityManager.HasComponent(uid); } + [Obsolete("use SharedMapSystem.GetAllMapGrids")] public IEnumerable GetAllMapGrids(MapId mapId) { - var query = EntityManager.AllEntityQueryEnumerator(); - while (query.MoveNext(out var grid, out var xform)) - { - if (xform.MapID == mapId) - yield return grid; - } + return MapSystem.GetAllMapGrids(mapId); } + [Obsolete("use SharedMapSystem.GetAllGrids")] public IEnumerable> GetAllGrids(MapId mapId) { - var query = EntityManager.AllEntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var grid, out var xform)) - { - if (xform.MapID != mapId) - continue; - - yield return (uid, grid); - } + return MapSystem.GetAllGrids(mapId); } + [Obsolete("just delete the grid entity")] public virtual void DeleteGrid(EntityUid euid) { // Possible the grid was already deleted / is invalid @@ -89,44 +80,21 @@ public virtual void DeleteGrid(EntityUid euid) } /// - public bool SuppressOnTileChanged { get; set; } + [Obsolete("use SharedMapSystem.SuppressOnTileChanged")] + public bool SuppressOnTileChanged + { + get => MapSystem.SuppressOnTileChanged; + set { MapSystem.SuppressOnTileChanged = value; } + } /// /// Raises the OnTileChanged event. /// /// A reference to the new tile. /// The old tile that got replaced. + [Obsolete("use SharedMapSystem.RaiseOnTileChanged")] void IMapManagerInternal.RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) { - if (SuppressOnTileChanged) - return; - - var ev = new TileChangedEvent(entity, tileRef, oldTile, chunk); - EntityManager.EventBus.RaiseLocalEvent(entity.Owner, ref ev, true); - } - - protected Entity CreateGrid(EntityUid map, ushort chunkSize, EntityUid forcedGridEuid) - { - var gridEnt = EntityManager.CreateEntityUninitialized(null, forcedGridEuid); - - var grid = EntityManager.AddComponent(gridEnt); - grid.ChunkSize = chunkSize; - - _sawmill.Debug($"Binding new grid {gridEnt}"); - - //TODO: This is a hack to get TransformComponent.MapId working before entity states - //are applied. After they are applied the parent may be different, but the MapId will - //be the same. This causes TransformComponent.ParentUid of a grid to be unsafe to - //use in transform states anytime before the state parent is properly set. - EntityManager.GetComponent(gridEnt).AttachParent(map); - - var meta = EntityManager.GetComponent(gridEnt); - EntityManager.System().SetEntityName(gridEnt, $"grid", meta); - EntityManager.InitializeComponents(gridEnt, meta); - EntityManager.StartComponents(gridEnt); - // Note that this does not actually map-initialize the grid entity, even if the map its being spawn on has already been initialized. - // I don't know whether that is intentional or not. - - return (gridEnt, grid); + MapSystem.RaiseOnTileChanged(entity, tileRef, oldTile, chunk); } } diff --git a/Robust.Shared/Map/MapManager.MapCollection.cs b/Robust.Shared/Map/MapManager.MapCollection.cs index e8a5f44e181..3810652f9a4 100644 --- a/Robust.Shared/Map/MapManager.MapCollection.cs +++ b/Robust.Shared/Map/MapManager.MapCollection.cs @@ -30,7 +30,7 @@ internal partial class MapManager /// public virtual void DeleteMap(MapId mapId) { - _mapSystem.DeleteMap(mapId); + MapSystem.DeleteMap(mapId); } /// @@ -38,24 +38,24 @@ public MapId CreateMap(MapId? mapId = null) { if (mapId != null) { - _mapSystem.CreateMap(mapId.Value); + MapSystem.CreateMap(mapId.Value); return mapId.Value; } - _mapSystem.CreateMap(out var map); + MapSystem.CreateMap(out var map); return map; } /// public bool MapExists([NotNullWhen(true)] MapId? mapId) { - return _mapSystem.MapExists(mapId); + return MapSystem.MapExists(mapId); } /// public EntityUid GetMapEntityId(MapId mapId) { - return _mapSystem.GetMapOrInvalid(mapId); + return MapSystem.GetMapOrInvalid(mapId); } /// @@ -63,18 +63,18 @@ public EntityUid GetMapEntityId(MapId mapId) /// public EntityUid GetMapEntityIdOrThrow(MapId mapId) { - return _mapSystem.GetMap(mapId); + return MapSystem.GetMap(mapId); } public bool TryGetMap([NotNullWhen(true)] MapId? mapId, [NotNullWhen(true)] out EntityUid? uid) { - return _mapSystem.TryGetMap(mapId, out uid); + return MapSystem.TryGetMap(mapId, out uid); } /// public IEnumerable GetAllMapIds() { - return _mapSystem.GetAllMapIds(); + return MapSystem.GetAllMapIds(); } /// diff --git a/Robust.Shared/Map/MapManager.Pause.cs b/Robust.Shared/Map/MapManager.Pause.cs index d4fb8f58976..35415537c4a 100644 --- a/Robust.Shared/Map/MapManager.Pause.cs +++ b/Robust.Shared/Map/MapManager.Pause.cs @@ -7,100 +7,34 @@ internal partial class MapManager { public void SetMapPaused(MapId mapId, bool paused) { - _mapSystem.SetPaused(mapId, paused); + MapSystem.SetPaused(mapId, paused); } public void SetMapPaused(EntityUid uid, bool paused) { - _mapSystem.SetPaused(uid, paused); + MapSystem.SetPaused(uid, paused); } public void DoMapInitialize(MapId mapId) { - _mapSystem.InitializeMap(mapId); + MapSystem.InitializeMap(mapId); } public bool IsMapInitialized(MapId mapId) { - return _mapSystem.IsInitialized(mapId); + return MapSystem.IsInitialized(mapId); } /// public bool IsMapPaused(MapId mapId) { - return _mapSystem.IsPaused(mapId); + return MapSystem.IsPaused(mapId); } /// public bool IsMapPaused(EntityUid uid) { - return _mapSystem.IsPaused(uid); - } - - /// - /// Initializes the map pausing system. - /// - private void InitializeMapPausing() - { - _conhost.RegisterCommand("pausemap", - "Pauses a map, pausing all simulation processing on it.", - "pausemap ", - (shell, _, args) => - { - if (args.Length != 1) - { - shell.WriteError("Need to supply a valid MapId"); - return; - } - - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteError("That map does not exist."); - return; - } - - SetMapPaused(mapId, true); - }); - - _conhost.RegisterCommand("querymappaused", - "Check whether a map is paused or not.", - "querymappaused ", - (shell, _, args) => - { - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteError("That map does not exist."); - return; - } - - shell.WriteLine(_mapSystem.IsPaused(mapId).ToString()); - }); - - _conhost.RegisterCommand("unpausemap", - "unpauses a map, resuming all simulation processing on it.", - "Usage: unpausemap ", - (shell, _, args) => - { - if (args.Length != 1) - { - shell.WriteLine("Need to supply a valid MapId"); - return; - } - - var mapId = new MapId(int.Parse(args[0], CultureInfo.InvariantCulture)); - - if (!MapExists(mapId)) - { - shell.WriteLine("That map does not exist."); - return; - } - - SetMapPaused(mapId, false); - }); + return MapSystem.IsPaused(uid); } } } diff --git a/Robust.Shared/Map/MapManager.Queries.cs b/Robust.Shared/Map/MapManager.Queries.cs index f37a545525f..8916fa76d84 100644 --- a/Robust.Shared/Map/MapManager.Queries.cs +++ b/Robust.Shared/Map/MapManager.Queries.cs @@ -4,102 +4,74 @@ using System.Numerics; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; -using Robust.Shared.Map.Enumerators; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Physics.Collision.Shapes; -using Robust.Shared.Physics.Shapes; namespace Robust.Shared.Map; internal partial class MapManager { - private bool IsIntersecting( - ChunkEnumerator enumerator, - T shape, - Transform shapeTransform, - Entity grid) where T : IPhysShape - { - var gridTransform = _physics.GetPhysicsTransform(grid); - - while (enumerator.MoveNext(out var chunk)) - { - foreach (var id in chunk.Fixtures) - { - var fixture = grid.Comp.Fixtures[id]; - - for (var j = 0; j < fixture.Shape.ChildCount; j++) - { - if (_manifolds.TestOverlap(shape, 0, fixture.Shape, j, shapeTransform, gridTransform)) - { - return true; - } - } - } - } - - return false; - } - - #region MapId + #region MapId [Obsolete] + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, shape, transform, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, shape, transform, ref grids, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, shape, transform, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, shape, transform, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldAABB, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldAABB, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var map)) - FindGridsIntersecting(map.Value, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var map)) - FindGridsIntersecting(map.Value, worldAABB, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldAABB, ref grids, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldBounds, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - if (_mapSystem.TryGetMap(mapId, out var mapEnt)) - FindGridsIntersecting(mapEnt.Value, worldBounds, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapId, worldBounds, ref grids, approx: approx, includeMap: includeMap); } #endregion - #region MapEnt + #region MapEnt [Obsolete] + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting( EntityUid mapEnt, T shape, @@ -108,19 +80,10 @@ public void FindGridsIntersecting( bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, callback, approx: approx, includeMap: includeMap); - } - - private void FindGridsIntersecting(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - // This is here so we don't double up on code. - var state = callback; - - FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, - static (EntityUid uid, MapGridComponent grid, ref GridCallback state) => state.Invoke(uid, grid), - approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, shape, transform, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting( EntityUid mapEnt, T shape, @@ -130,236 +93,103 @@ public void FindGridsIntersecting( bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref state, callback, approx: approx, includeMap: includeMap); - } - - private void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Box2 worldAABB, - Transform transform, - ref TState state, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - if (!_gridTreeQuery.TryGetComponent(mapEnt, out var gridTree)) - return; - - if (includeMap && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) - { - callback(mapEnt, mapGrid, ref state); - } - - var gridState = new GridQueryState( - callback, - state, - worldAABB, - shape, - transform, - gridTree.Tree, - _mapSystem, - this, - _transformSystem, - approx); - - gridTree.Tree.Query(ref gridState, static (ref GridQueryState state, DynamicTree.Proxy proxy) => - { - // Even for approximate we'll check if any chunks roughly overlap. - var data = state.Tree.GetUserData(proxy); - var gridInvMatrix = state.TransformSystem.GetInvWorldMatrix(data.Uid); - var localAABB = gridInvMatrix.TransformBox(state.WorldAABB); - - var overlappingChunks = state.MapSystem.GetLocalMapChunks(data.Uid, data.Grid, localAABB); - - if (state.Approximate) - { - if (!overlappingChunks.MoveNext(out _)) - return true; - } - else if (!state.MapManager.IsIntersecting(overlappingChunks, state.Shape, state.Transform, (data.Uid, data.Fixtures))) - { - return true; - } - - var callbackState = state.State; - var result = state.Callback(data.Uid, data.Grid, ref callbackState); - state.State = callbackState; - - return result; - }, worldAABB); - - // By-ref things - state = gridState.State; + MapSystem.FindGridsIntersecting(mapEnt, shape, transform, ref state, callback, approx: approx, includeMap: includeMap); } /// /// Returns true if any grids overlap the specified shapes. /// + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, ref List> entities, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - foreach (var shape in shapes) - { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref entities, approx: approx, includeMap: includeMap); - } + MapSystem.FindGridsIntersecting(mapEnt, shapes, transform, ref entities, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - FindGridsIntersecting(mapEnt, shape, shape.ComputeAABB(transform, 0), transform, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape { - var state = grids; - - FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref state, - static (EntityUid uid, MapGridComponent grid, ref List> list) => - { - list.Add((uid, grid)); - return true; - }, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref grids, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldAABB, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldAABB); - FindGridsIntersecting(mapEnt, polygon, worldAABB, Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldAABB, ref grids, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldBounds, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref state, callback, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); } + [Obsolete("use SharedMapSystem")] public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) { - var polygon = new SlimPolygon(worldBounds); - FindGridsIntersecting(mapEnt, polygon, worldBounds.CalcBoundingBox(), Transform.Empty, ref grids, approx: approx, includeMap: includeMap); + MapSystem.FindGridsIntersecting(mapEnt, worldBounds, ref grids, approx: approx, includeMap: includeMap); } #endregion #region TryFindGridAt + [Obsolete("use SharedMapSystem")] public bool TryFindGridAt( EntityUid mapEnt, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) { - var rangeVec = new Vector2(0.2f, 0.2f); - - // Need to enlarge the AABB by at least the grid shrinkage size. - var aabb = new Box2(worldPos - rangeVec, worldPos + rangeVec); - - uid = EntityUid.Invalid; - grid = null; - var state = (uid, grid, worldPos, _mapSystem, _transformSystem); - - FindGridsIntersecting(mapEnt, aabb, ref state, static (EntityUid iUid, MapGridComponent iGrid, ref ( - EntityUid uid, - MapGridComponent? grid, - Vector2 worldPos, - SharedMapSystem mapSystem, - SharedTransformSystem xformSystem) tuple) => - { - // Turn the worldPos into a localPos and work out the relevant chunk we need to check - // This is much faster than iterating over every chunk individually. - // (though now we need some extra calcs up front). - - // Doesn't use WorldBounds because it's just an AABB. - var matrix = tuple.xformSystem.GetInvWorldMatrix(iUid); - var localPos = Vector2.Transform(tuple.worldPos, matrix); - - // NOTE: - // If you change this to use fixtures instead (i.e. if you want half-tiles) then you need to make sure - // you account for the fact that fixtures are shrunk slightly! - var chunkIndices = SharedMapSystem.GetChunkIndices(localPos, iGrid.ChunkSize); - - if (!iGrid.Chunks.TryGetValue(chunkIndices, out var chunk)) - return true; - - var chunkRelative = SharedMapSystem.GetChunkRelative(localPos, iGrid.ChunkSize); - var chunkTile = chunk.GetTile(chunkRelative); - - if (chunkTile.IsEmpty) - return true; - - tuple.uid = iUid; - tuple.grid = iGrid; - return false; - }, approx: true, includeMap: false); - - if (state.grid == null && _gridQuery.TryGetComponent(mapEnt, out var mapGrid)) - { - uid = mapEnt; - grid = mapGrid; - return true; - } - - uid = state.uid; - grid = state.grid; - return grid != null; + return MapSystem.TryFindGridAt(mapEnt, worldPos, out uid, out grid); } /// /// Attempts to find the map grid under the map location. /// + [Obsolete("use SharedMapSystem")] public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) { - if (_mapSystem.TryGetMap(mapId, out var map)) - return TryFindGridAt(map.Value, worldPos, out uid, out grid); - - uid = default; - grid = null; - return false; + return MapSystem.TryFindGridAt(mapId, worldPos, out uid, out grid); } /// /// Attempts to find the map grid under the map location. /// + [Obsolete("use SharedMapSystem")] public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) { - return TryFindGridAt(mapCoordinates.MapId, mapCoordinates.Position, out uid, out grid); + return MapSystem.TryFindGridAt(mapCoordinates, out uid, out grid); } #endregion - - private record struct GridQueryState( - GridCallback Callback, - TState State, - Box2 WorldAABB, - T Shape, - Transform Transform, - B2DynamicTree<(EntityUid Uid, FixturesComponent Fixtures, MapGridComponent Grid)> Tree, - SharedMapSystem MapSystem, - MapManager MapManager, - SharedTransformSystem TransformSystem, - bool Approximate); } diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs index c5e853c8b63..3bfd5f3e3ce 100644 --- a/Robust.Shared/Map/MapManager.cs +++ b/Robust.Shared/Map/MapManager.cs @@ -1,11 +1,7 @@ -using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map.Components; -using Robust.Shared.Physics.Collision; -using Robust.Shared.Physics.Systems; -using Robust.Shared.Timing; namespace Robust.Shared.Map; @@ -13,36 +9,23 @@ namespace Robust.Shared.Map; [Virtual] internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber { - [Dependency] public IGameTiming GameTiming = default!; [Dependency] public IEntityManager EntityManager = default!; - [Dependency] private IManifoldManager _manifolds = default!; [Dependency] private ILogManager _logManager = default!; - [Dependency] private IConsoleHost _conhost = default!; private ISawmill _sawmill = default!; - private SharedMapSystem _mapSystem = default!; - private SharedPhysicsSystem _physics = default!; - private SharedTransformSystem _transformSystem = default!; - - private EntityQuery _gridTreeQuery; - private EntityQuery _gridQuery; + protected SharedMapSystem MapSystem = default!; /// public void Initialize() { - _gridTreeQuery = EntityManager.GetEntityQuery(); - _gridQuery = EntityManager.GetEntityQuery(); - InitializeMapPausing(); _sawmill = _logManager.GetSawmill("system.map"); } /// public void Startup() { - _physics = EntityManager.System(); - _transformSystem = EntityManager.System(); - _mapSystem = EntityManager.System(); + MapSystem = EntityManager.System(); _sawmill.Debug("Starting..."); } diff --git a/Robust.Shared/Map/NetworkedMapManager.cs b/Robust.Shared/Map/NetworkedMapManager.cs index 683916bc46d..fdfbfadf781 100644 --- a/Robust.Shared/Map/NetworkedMapManager.cs +++ b/Robust.Shared/Map/NetworkedMapManager.cs @@ -1,23 +1,21 @@ -using Robust.Shared.Map.Components; +using System; using Robust.Shared.Timing; namespace Robust.Shared.Map; +[Obsolete] internal interface INetworkedMapManager : IMapManagerInternal { + [Obsolete] void CullDeletionHistory(GameTick upToTick); } +[Obsolete] internal sealed class NetworkedMapManager : MapManager, INetworkedMapManager { + [Obsolete] public void CullDeletionHistory(GameTick upToTick) { - var query = EntityManager.AllEntityQueryEnumerator(); - - while (query.MoveNext(out var grid)) - { - var chunks = grid.ChunkDeletionHistory; - chunks.RemoveAll(t => t.tick < upToTick); - } + MapSystem.CullDeletionHistory(upToTick); } } From 7fcf49ea61b1f34e8217c3edcd517a6c93839415 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:09:59 +1000 Subject: [PATCH 080/178] Add audio device switching API (#6661) --- RELEASE-NOTES.md | 1 + .../Audio/AudioManagerTest.cs | 48 +++++++ Robust.Client/Audio/AudioManager.cs | 120 ++++++++++++++++++ Robust.Client/Audio/HeadlessAudioManager.cs | 13 ++ Robust.Client/Audio/IAudioManager.cs | 5 + Robust.Shared/CVars.cs | 2 +- 6 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 7dabadb3dc5..0965ac9a5b5 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -51,6 +51,7 @@ END TEMPLATE--> * The obsoleted TryIndex methods on PrototypeManager have now been removed. * Add batched Box2 / Box2Rotated drawing methods to Clyde WorldHandle. * Completion filter now works by Contains instead of StartsWith +* Add SwitchAudioDevice API to AudioManager. ### Bugfixes diff --git a/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs b/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs new file mode 100644 index 00000000000..a6df0dfa80f --- /dev/null +++ b/Robust.Client.IntegrationTests/Audio/AudioManagerTest.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using Robust.Client.Audio; +using Robust.Shared; +using Robust.Shared.IoC; +using Robust.UnitTesting; + +namespace Robust.Client.IntegrationTests.Audio; + +[TestFixture] +[TestOf(typeof(AudioManager))] +[Explicit("Server test-runners don't typically have the means of running OpenAL.")] +public sealed class AudioManagerTest : RobustIntegrationTest +{ + [Test] + public async Task SwitchesAudioDevice() + { + var client = StartClient(new ClientIntegrationOptions + { + Pool = false, + InitIoC = () => + { + IoCManager.Register(overwrite: true); + IoCManager.Register(overwrite: true); + }, + }); + + await client.WaitIdleAsync(); + + var audio = client.ResolveDependency(); + Assert.That(audio, Is.TypeOf()); + + var defaultDevice = audio.GetDefaultAudioDevice(); + var devices = audio.GetAudioDevices(); + var testDevice = devices.FirstOrDefault(device => device != defaultDevice) ?? defaultDevice; + + if (testDevice == null) + Assert.Ignore("OpenAL did not expose any audio output devices."); + + await client.WaitAssertion(() => + { + client.CfgMan.SetCVar(CVars.AudioDevice, testDevice); + Assert.That(client.CfgMan.GetCVar(CVars.AudioDevice), Is.EqualTo(testDevice)); + + client.CfgMan.SetCVar(CVars.AudioDevice, string.Empty); + Assert.That(client.CfgMan.GetCVar(CVars.AudioDevice), Is.Empty); + }); + } +} diff --git a/Robust.Client/Audio/AudioManager.cs b/Robust.Client/Audio/AudioManager.cs index 63fc5fdc0da..f70f3b8d365 100644 --- a/Robust.Client/Audio/AudioManager.cs +++ b/Robust.Client/Audio/AudioManager.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using OpenTK.Audio.OpenAL; @@ -36,6 +38,7 @@ internal sealed partial class AudioManager : IAudioInternal private readonly HashSet _alcDeviceExtensions = new(); private readonly HashSet _alContextExtensions = new(); private Attenuation _attenuation; + private bool _audioInitialized; public bool HasAlDeviceExtension(string extension) => _alcDeviceExtensions.Contains(extension); public bool HasAlContextExtension(string extension) => _alContextExtensions.Contains(extension); @@ -92,6 +95,28 @@ private void _audioCreateContext() OpenALSawmill.Debug("HRTF status: {0}", hrtfEnabled == 1 ? "Enabled" : "Disabled"); } + public IReadOnlyList GetAudioDevices() + { + if (ALC.EnumerateAll.IsExtensionPresent()) + return ALC.EnumerateAll.GetStringList(GetEnumerateAllContextStringList.AllDevicesSpecifier).ToList(); + + if (ALC.IsExtensionPresent(ALDevice.Null, "ALC_ENUMERATION_EXT")) + return ALC.GetStringList(GetEnumerationStringList.DeviceSpecifier).ToList(); + + return Array.Empty(); + } + + public string? GetDefaultAudioDevice() + { + if (ALC.EnumerateAll.IsExtensionPresent()) + return ALC.EnumerateAll.GetString(ALDevice.Null, GetEnumerateAllContextString.DefaultAllDevicesSpecifier); + + if (ALC.IsExtensionPresent(ALDevice.Null, "ALC_ENUMERATION_EXT")) + return ALC.GetString(ALDevice.Null, AlcGetString.DefaultDeviceSpecifier); + + return null; + } + private bool _audioOpenDevice() { var preferredDevice = _cfg.GetCVar(CVars.AudioDevice); @@ -143,11 +168,81 @@ private void InitializeAudio() IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); _cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true); + _cfg.OnValueChanged(CVars.AudioDevice, OnAudioDeviceChanged); _reload.Register("/Audio", "*.ogg"); _reload.Register("/Audio", "*.wav"); _reload.OnChanged += OnReload; + _audioInitialized = true; + } + + private void OnAudioDeviceChanged(string deviceSpecifier) + { + if (!_audioInitialized) + return; + + SwitchAudioDevice(deviceSpecifier); + } + + private void SwitchAudioDevice(string requestedDevice) + { + OpenALSawmill.Info("Switching OpenAL output device to {0}.", + string.IsNullOrEmpty(requestedDevice) ? "" : requestedDevice); + + if (TryReopenAudioDevice(requestedDevice)) + { + ReloadDeviceExtensions(); + IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); + SetMasterGain(_cfg.GetCVar(CVars.AudioMasterVolume)); + return; + } + + // The skrunkly path that's hopefully never needed. + OpenALSawmill.Warning( + "ALC_SOFT_reopen_device is unavailable or failed. Falling back to full audio device rebuild."); + + DisposeAllAudio(); + FlushALDisposeQueues(); + + if (!_audioOpenDevice()) + { + OpenALSawmill.Error("Failed to reopen OpenAL device after device switch."); + return; + } + + _audioCreateContext(); + IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX"); + SetMasterGain(_cfg.GetCVar(CVars.AudioMasterVolume)); + } + + private bool TryReopenAudioDevice(string requestedDevice) + { + if (_openALDevice == ALDevice.Null || + !ALC.IsExtensionPresent(_openALDevice, "ALC_SOFT_reopen_device")) + { + return false; + } + + var reopen = LoadAlcDelegate("alcReopenDeviceSOFT"); + if (reopen == null) + return false; + + var reopenTarget = string.IsNullOrEmpty(requestedDevice) ? null : requestedDevice; + var reopened = reopen(_openALDevice, reopenTarget, IntPtr.Zero); + _checkAlcError(_openALDevice); + return reopened; + } + + private void ReloadDeviceExtensions() + { + _alcDeviceExtensions.Clear(); + + var s = ALC.GetString(_openALDevice, AlcGetString.Extensions) ?? ""; + foreach (var extension in s.Split(' ', StringSplitOptions.RemoveEmptyEntries)) + { + _alcDeviceExtensions.Add(extension); + } } private void OnReload(ResPath args) @@ -188,6 +283,31 @@ internal void LogError(string message) OpenALSawmill.Error(message); } + /* + * Evil hack because OpenTK doesn't expose the device switch call. + */ + + private delegate bool AlcReopenDeviceSoftDelegate(ALDevice device, string? deviceName, IntPtr attribs); + + private static TDelegate? LoadAlcDelegate(string name) + where TDelegate : Delegate + { + var type = typeof(ALC); + while (type != null) + { + var method = type.GetMethod( + "LoadDelegate", + BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); + + if (method != null) + return (TDelegate?) method.MakeGenericMethod(typeof(TDelegate)).Invoke(null, [name]); + + type = type.BaseType; + } + + return null; + } + /// /// Like _checkAlError but allows custom data to be passed in as relevant. /// diff --git a/Robust.Client/Audio/HeadlessAudioManager.cs b/Robust.Client/Audio/HeadlessAudioManager.cs index 46c2cc8e442..b38449adc6e 100644 --- a/Robust.Client/Audio/HeadlessAudioManager.cs +++ b/Robust.Client/Audio/HeadlessAudioManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Numerics; using Robust.Shared.Audio; @@ -13,6 +14,8 @@ namespace Robust.Client.Audio; /// internal sealed class HeadlessAudioManager : IAudioInternal { + + private readonly IReadOnlyList _emptyDevices = Array.Empty(); private int _audioBuffer; /// @@ -36,6 +39,16 @@ public IAudioSource CreateAudioSource(AudioStream stream) return DummyAudioSource.Instance; } + public IReadOnlyList GetAudioDevices() + { + return _emptyDevices; + } + + public string? GetDefaultAudioDevice() + { + return null; + } + /// public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false) { diff --git a/Robust.Client/Audio/IAudioManager.cs b/Robust.Client/Audio/IAudioManager.cs index ce65d1d0cb1..9396346b056 100644 --- a/Robust.Client/Audio/IAudioManager.cs +++ b/Robust.Client/Audio/IAudioManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using Robust.Shared.Audio.Sources; @@ -10,6 +11,10 @@ namespace Robust.Client.Audio; [NotContentImplementable] public interface IAudioManager { + IReadOnlyList GetAudioDevices(); + + string? GetDefaultAudioDevice(); + IAudioSource? CreateAudioSource(AudioStream stream); AudioStream LoadAudioOggVorbis(Stream stream, string? name = null); diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index d4c67e3998d..2d85fd055c9 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -1304,7 +1304,7 @@ protected CVars() /// Audio device to try to output audio to by default. /// public static readonly CVarDef AudioDevice = - CVarDef.Create("audio.device", string.Empty, CVar.CLIENTONLY); + CVarDef.Create("audio.device", string.Empty, CVar.CLIENTONLY | CVar.ARCHIVE); /// /// Master volume for audio output. From 13730c9203fd667739791b2e2d425b58a2ad6842 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Sat, 27 Jun 2026 19:18:52 +1000 Subject: [PATCH 081/178] Version: 278.0.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 22df1e03bc5..ebe81eaeda3 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 277.2.1 + 278.0.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0965ac9a5b5..0d36599caca 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,8 +35,32 @@ END TEMPLATE--> ### Breaking changes +*None yet* + +### New features + +*None yet* + +### Bugfixes + +*None yet* + +### Other + +*None yet* + +### Internal + +*None yet* + + +## 278.0.0 + +### Breaking changes + * Remove the duplicate serialization copy of components kept on ComponentRegistryEntry; now it only stores the deserialized component. To get the raw MappingDataNode for EntityPrototypes use PrototypeManager. This is expected to significantly reduce memory usage. * Obsolete LocalRotation in favor of the system method. The angle is now also normalized to 2PI and no longer grows indefinitely. +* Obsoleted IMapManager methods in lieu of SharedMapSystem. ### New features @@ -52,6 +76,8 @@ END TEMPLATE--> * Add batched Box2 / Box2Rotated drawing methods to Clyde WorldHandle. * Completion filter now works by Contains instead of StartsWith * Add SwitchAudioDevice API to AudioManager. +* Added a PhysicsBodyStatusChangedEvent (self-descriptive). +* Allow enumeration on EntityQuery. ### Bugfixes @@ -62,6 +88,7 @@ END TEMPLATE--> * Fixed `BoxContainer`'s `SeparationOverride` not overriding the Style Properties. * Fixed `SeparationOverride` not invalidating measure. * Fixed swapped parameters in `MapManager`'s `FindGridsIntersecting` methods. +* Fix DataRecord serialization. ### Other @@ -81,6 +108,7 @@ END TEMPLATE--> * Optimise ComponentRegistry deserialization slightly. * Optimise Box2Rotated.TransformBox slightly. * Added several test helpers to avoid boilerplate in integration tests around client connection / disconnection. +* Simplifed AccessAnalyzer check to speed it up. ## 277.2.1 From c6cf80334cccec06e799e974b1b2f4b1b56c62ee Mon Sep 17 00:00:00 2001 From: TheShuEd <96445749+TheShuEd@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:22:37 +0300 Subject: [PATCH 082/178] Server & shared containerSystem clean up (#6564) * clean up * revert readonly * file scoped SharedContainerSystem --- Robust.Server/Containers/ContainerSystem.cs | 14 +- .../SharedContainerSystem.Validation.cs | 2 - .../Containers/SharedContainerSystem.cs | 1069 ++++++++--------- 3 files changed, 536 insertions(+), 549 deletions(-) diff --git a/Robust.Server/Containers/ContainerSystem.cs b/Robust.Server/Containers/ContainerSystem.cs index d298fc2b481..4122bd7369d 100644 --- a/Robust.Server/Containers/ContainerSystem.cs +++ b/Robust.Server/Containers/ContainerSystem.cs @@ -1,15 +1,13 @@ using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.Log; -namespace Robust.Server.Containers +namespace Robust.Server.Containers; + +public sealed class ContainerSystem : SharedContainerSystem { - public sealed class ContainerSystem : SharedContainerSystem + protected override void ValidateMissingEntity(EntityUid uid, BaseContainer cont, EntityUid missing) { - protected override void ValidateMissingEntity(EntityUid uid, BaseContainer cont, EntityUid missing) - { - Log.Error($"Missing entity for container {ToPrettyString(uid)}. Missing uid: {missing}"); - //cont.InternalRemove(ent); - } + Log.Error($"Missing entity for container {ToPrettyString(uid)}. Missing uid: {missing}"); + //cont.InternalRemove(ent); } } diff --git a/Robust.Shared/Containers/SharedContainerSystem.Validation.cs b/Robust.Shared/Containers/SharedContainerSystem.Validation.cs index 1993c8209a6..bd00f1f90b4 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.Validation.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.Validation.cs @@ -4,7 +4,6 @@ namespace Robust.Shared.Containers; - // This partial class just exists for debug asserts and bug fixing public abstract partial class SharedContainerSystem : EntitySystem { @@ -65,5 +64,4 @@ private void ValidateChildren(TransformComponent xform, EntityQuery _managerQuery; - private EntityQuery _gridQuery; - private EntityQuery _mapQuery; - protected EntityQuery MetaQuery; - protected EntityQuery PhysicsQuery; - protected EntityQuery JointQuery; - protected EntityQuery TransformQuery; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnParentChanged); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnStartupValidation); - SubscribeLocalEvent(OnContainerGetState); - SubscribeLocalEvent(OnContainerManagerRemove); - - _managerQuery = GetEntityQuery(); - _gridQuery = GetEntityQuery(); - _mapQuery = GetEntityQuery(); - MetaQuery = GetEntityQuery(); - PhysicsQuery = GetEntityQuery(); - JointQuery = GetEntityQuery(); - TransformQuery = GetEntityQuery(); - } + [Dependency] private IDynamicTypeFactoryInternal _dynFactory = default!; + [Dependency] private INetManager _net = default!; + [Dependency] private SharedPhysicsSystem _physics = default!; + [Dependency] private EntityLookupSystem _lookup = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private SharedJointSystem _joint = default!; + [Dependency] private IGameTiming _timing = default!; + + [Dependency] private EntityQuery _managerQuery = default!; + [Dependency] private EntityQuery _gridQuery = default!; + [Dependency] private EntityQuery _mapQuery = default!; + [Dependency] protected EntityQuery MetaQuery = default!; + [Dependency] protected EntityQuery PhysicsQuery = default!; + [Dependency] protected EntityQuery JointQuery = default!; + [Dependency] protected EntityQuery TransformQuery = default!; + + /// + public override void Initialize() + { + base.Initialize(); - private void OnInit(Entity ent, ref ComponentInit args) + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnStartupValidation); + SubscribeLocalEvent(OnContainerGetState); + SubscribeLocalEvent(OnContainerManagerRemove); + } + + private void OnInit(Entity ent, ref ComponentInit args) + { + foreach (var (id, container) in ent.Comp.Containers) { - foreach (var (id, container) in ent.Comp.Containers) - { - container.Init(this, id, ent); - } + container.Init(this, id, ent); } + } - private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) + private void OnContainerGetState(EntityUid uid, ContainerManagerComponent component, ref ComponentGetState args) + { + Dictionary containerSet = + new(component.Containers.Count); + + foreach (var container in component.Containers.Values) { - Dictionary containerSet = - new(component.Containers.Count); + var uidArr = new NetEntity[container.ContainedEntities.Count]; - foreach (var container in component.Containers.Values) + for (var index = 0; index < container.ContainedEntities.Count; index++) { - var uidArr = new NetEntity[container.ContainedEntities.Count]; - - for (var index = 0; index < container.ContainedEntities.Count; index++) - { - uidArr[index] = GetNetEntity(container.ContainedEntities[index]); - } - - var sContainer = - new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name, - container.ShowContents, - container.OccludesLight, - uidArr); - containerSet.Add(container.ID, sContainer); + uidArr[index] = GetNetEntity(container.ContainedEntities[index]); } - args.State = new ContainerManagerComponent.ContainerManagerComponentState(containerSet); + var sContainer = + new ContainerManagerComponent.ContainerManagerComponentState.ContainerData(container.GetType().Name, + container.ShowContents, + container.OccludesLight, + uidArr); + containerSet.Add(container.ID, sContainer); } - private void OnContainerManagerRemove(EntityUid uid, ContainerManagerComponent component, ComponentRemove args) - { - foreach (var container in component.Containers.Values) - { - ShutdownContainer(container); - } + args.State = new ContainerManagerComponent.ContainerManagerComponentState(containerSet); + } - component.Containers.Clear(); + private void OnContainerManagerRemove(EntityUid uid, ContainerManagerComponent component, ComponentRemove args) + { + foreach (var container in component.Containers.Values) + { + ShutdownContainer(container); } - // TODO: Make ContainerManagerComponent ECS and make these proxy methods the real deal. + component.Containers.Clear(); + } - #region Proxy Methods + // TODO: Make ContainerManagerComponent ECS and make these proxy methods the real deal. - public T MakeContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - if (!Resolve(uid, ref containerManager, false)) - containerManager = AddComp(uid); // Happy Vera. + #region Proxy Methods - if (HasContainer(uid, id, containerManager)) - throw new ArgumentException($"Container with specified ID already exists: '{id}'"); + public T MakeContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + if (!Resolve(uid, ref containerManager, false)) + containerManager = AddComp(uid); // Happy Vera. - var container = _dynFactory.CreateInstanceUnchecked(typeof(T), inject: false); - container.Init(this, id, (uid, containerManager)); - containerManager.Containers[id] = container; - Dirty(uid, containerManager); - return container; - } + if (HasContainer(uid, id, containerManager)) + throw new ArgumentException($"Container with specified ID already exists: '{id}'"); - public virtual void ShutdownContainer(BaseContainer container) - { - container.InternalShutdown(EntityManager, this, _net.IsClient); - container.Manager.Containers.Remove(container.ID); - container.ExpectedEntities.Clear(); - } + var container = _dynFactory.CreateInstanceUnchecked(typeof(T), inject: false); + container.Init(this, id, (uid, containerManager)); + containerManager.Containers[id] = container; + Dirty(uid, containerManager); + return container; + } - public T EnsureContainer( - EntityUid uid, - string id, - out bool alreadyExisted, - ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - if (!Resolve(uid, ref containerManager, false)) - containerManager = AddComp(uid); + public virtual void ShutdownContainer(BaseContainer container) + { + container.InternalShutdown(EntityManager, this, _net.IsClient); + container.Manager.Containers.Remove(container.ID); + container.ExpectedEntities.Clear(); + } - if (TryGetContainer(uid, id, out var container, containerManager)) - { - alreadyExisted = true; - if (container is T cast) - return cast; + public T EnsureContainer( + EntityUid uid, + string id, + out bool alreadyExisted, + ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + if (!Resolve(uid, ref containerManager, false)) + containerManager = AddComp(uid); - throw new InvalidOperationException( - $"The container exists but is of a different type: {container.GetType()}"); - } + if (TryGetContainer(uid, id, out var container, containerManager)) + { + alreadyExisted = true; + if (container is T cast) + return cast; - alreadyExisted = false; - return MakeContainer(uid, id, containerManager); + throw new InvalidOperationException( + $"The container exists but is of a different type: {container.GetType()}"); } - public T EnsureContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - where T : BaseContainer - { - return EnsureContainer(uid, id, out _, containerManager); - } + alreadyExisted = false; + return MakeContainer(uid, id, containerManager); + } - public BaseContainer GetContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) - { - if (!Resolve(uid, ref containerManager)) - throw new ArgumentException("Entity does not have a ContainerManagerComponent!", nameof(uid)); + public T EnsureContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + where T : BaseContainer + { + return EnsureContainer(uid, id, out _, containerManager); + } - return containerManager.Containers[id]; - } + public BaseContainer GetContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager)) + throw new ArgumentException("Entity does not have a ContainerManagerComponent!", nameof(uid)); - public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager) - { - if (!Resolve(uid, ref containerManager, false)) - return false; + return containerManager.Containers[id]; + } - return containerManager.Containers.ContainsKey(id); - } + public bool HasContainer(EntityUid uid, string id, ContainerManagerComponent? containerManager) + { + if (!Resolve(uid, ref containerManager, false)) + return false; - public bool TryGetContainer( - EntityUid uid, - string id, - [NotNullWhen(true)] out BaseContainer? container, - ContainerManagerComponent? containerManager = null) + return containerManager.Containers.ContainsKey(id); + } + + public bool TryGetContainer( + EntityUid uid, + string id, + [NotNullWhen(true)] out BaseContainer? container, + ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager, false)) { - if (!Resolve(uid, ref containerManager, false)) - { - container = null; - return false; - } + container = null; + return false; + } - if (!containerManager.Containers.TryGetValue(id, out container)) - return false; + if (!containerManager.Containers.TryGetValue(id, out container)) + return false; - DebugTools.AssertEqual(container.ID, id); - DebugTools.AssertNotNull(container.Manager); - DebugTools.AssertNotEqual(container.Owner, EntityUid.Invalid); - return true; + DebugTools.AssertEqual(container.ID, id); + DebugTools.AssertNotNull(container.Manager); + DebugTools.AssertNotEqual(container.Owner, EntityUid.Invalid); + return true; + } + + public bool TryGetContainingContainer( + EntityUid uid, + EntityUid containedUid, + [NotNullWhen(true)] out BaseContainer? container, + ContainerManagerComponent? containerManager = null) + { + DebugTools.Assert(Exists(containedUid)); + if (!Resolve(uid, ref containerManager, false)) + { + container = null; + return false; } - public bool TryGetContainingContainer( - EntityUid uid, - EntityUid containedUid, - [NotNullWhen(true)] out BaseContainer? container, - ContainerManagerComponent? containerManager = null) + foreach (var contain in containerManager.Containers.Values) { - DebugTools.Assert(Exists(containedUid)); - if (!Resolve(uid, ref containerManager, false)) + if (contain.Contains(containedUid)) { - container = null; - return false; + container = contain; + return true; } + } - foreach (var contain in containerManager.Containers.Values) - { - if (contain.Contains(containedUid)) - { - container = contain; - return true; - } - } + container = default; + return false; + } - container = default; + public bool ContainsEntity( + EntityUid uid, + EntityUid containedUid, + ContainerManagerComponent? containerManager = null) + { + DebugTools.Assert(Exists(containedUid)); + if (!Resolve(uid, ref containerManager, false)) return false; - } - public bool ContainsEntity( - EntityUid uid, - EntityUid containedUid, - ContainerManagerComponent? containerManager = null) + foreach (var container in containerManager.Containers.Values) { - DebugTools.Assert(Exists(containedUid)); - if (!Resolve(uid, ref containerManager, false)) - return false; + if (container.Contains(containedUid)) + return true; + } - foreach (var container in containerManager.Containers.Values) - { - if (container.Contains(containedUid)) - return true; - } + return false; + } + public bool RemoveEntity( + EntityUid uid, + EntityUid toremove, + ContainerManagerComponent? containerManager = null, + TransformComponent? containedXform = null, + MetaDataComponent? containedMeta = null, + bool reparent = true, + bool force = false, + EntityCoordinates? destination = null, + Angle? localRotation = null) + { + if (!Resolve(uid, ref containerManager) || !Resolve(toremove, ref containedMeta, ref containedXform)) return false; - } - public bool RemoveEntity( - EntityUid uid, - EntityUid toremove, - ContainerManagerComponent? containerManager = null, - TransformComponent? containedXform = null, - MetaDataComponent? containedMeta = null, - bool reparent = true, - bool force = false, - EntityCoordinates? destination = null, - Angle? localRotation = null) + foreach (var containers in containerManager.Containers.Values) { - if (!Resolve(uid, ref containerManager) || !Resolve(toremove, ref containedMeta, ref containedXform)) - return false; - - foreach (var containers in containerManager.Containers.Values) - { - if (containers.Contains(toremove)) - return Remove((toremove, containedXform, containedMeta), - containers, - reparent, - force, - destination, - localRotation); - } - - return true; // If we don't contain the entity, it will always be removed + if (containers.Contains(toremove)) + return Remove((toremove, containedXform, containedMeta), + containers, + reparent, + force, + destination, + localRotation); } - public ContainerManagerComponent.AllContainersEnumerable GetAllContainers( - EntityUid uid, - ContainerManagerComponent? containerManager = null) - { - if (!Resolve(uid, ref containerManager)) - return new ContainerManagerComponent.AllContainersEnumerable(); + return true; // If we don't contain the entity, it will always be removed + } - return new(containerManager); - } + public ContainerManagerComponent.AllContainersEnumerable GetAllContainers( + EntityUid uid, + ContainerManagerComponent? containerManager = null) + { + if (!Resolve(uid, ref containerManager)) + return new ContainerManagerComponent.AllContainersEnumerable(); - #endregion + return new(containerManager); + } - #region Container Helpers + #endregion - public bool TryGetContainingContainer( - Entity ent, - [NotNullWhen(true)] out BaseContainer? container) - { - container = null; + #region Container Helpers - if (!Resolve(ent, ref ent.Comp2, false)) - return false; + public bool TryGetContainingContainer( + Entity ent, + [NotNullWhen(true)] out BaseContainer? container) + { + container = null; - if ((ent.Comp2.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None) - return false; + if (!Resolve(ent, ref ent.Comp2, false)) + return false; - if (!Resolve(ent, ref ent.Comp1, false)) - return false; + if ((ent.Comp2.Flags & MetaDataFlags.InContainer) == MetaDataFlags.None) + return false; - return TryGetContainingContainer(ent.Comp1.ParentUid, ent, out container); - } + if (!Resolve(ent, ref ent.Comp1, false)) + return false; - /// - /// Checks whether the given entity is inside of a container. This will only check if this entity's direct - /// parent is containing it. To recursively if the entity, or any parent, is inside a container, use - /// - /// If the entity is inside of a container. - public bool IsEntityInContainer(EntityUid uid, MetaDataComponent? meta = null) - { - if (!Resolve(uid, ref meta, false)) - return false; + return TryGetContainingContainer(ent.Comp1.ParentUid, ent, out container); + } - return (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer; - } + /// + /// Checks whether the given entity is inside of a container. This will only check if this entity's direct + /// parent is containing it. To recursively if the entity, or any parent, is inside a container, use + /// + /// If the entity is inside of a container. + public bool IsEntityInContainer(EntityUid uid, MetaDataComponent? meta = null) + { + if (!Resolve(uid, ref meta, false)) + return false; - /// - /// Recursively check if the entity or any parent is inside of a container. - /// - /// If the entity is inside of a container. - public bool IsEntityOrParentInContainer( - EntityUid uid, - MetaDataComponent? meta = null, - TransformComponent? xform = null) - { - if (!MetaQuery.Resolve(uid, ref meta)) - return false; + return (meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer; + } - if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) - return true; + /// + /// Recursively check if the entity or any parent is inside of a container. + /// + /// If the entity is inside of a container. + public bool IsEntityOrParentInContainer( + EntityUid uid, + MetaDataComponent? meta = null, + TransformComponent? xform = null) + { + if (!MetaQuery.Resolve(uid, ref meta)) + return false; - if (!TransformQuery.Resolve(uid, ref xform)) - return false; + if ((meta.Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) + return true; - if (!xform.ParentUid.Valid) - return false; + if (!TransformQuery.Resolve(uid, ref xform)) + return false; - return IsEntityOrParentInContainer(xform.ParentUid); - } + if (!xform.ParentUid.Valid) + return false; - /// - /// Finds the first instance of a component on the recursive parented containers that hold an entity - /// - public bool TryFindComponentOnEntityContainerOrParent( - EntityUid uid, - EntityQuery entityQuery, - [NotNullWhen(true)] ref T? foundComponent, - MetaDataComponent? meta = null, - TransformComponent? xform = null) where T : IComponent - { - if (!MetaQuery.Resolve(uid, ref meta)) - return false; + return IsEntityOrParentInContainer(xform.ParentUid); + } - if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) - return false; + /// + /// Finds the first instance of a component on the recursive parented containers that hold an entity + /// + public bool TryFindComponentOnEntityContainerOrParent( + EntityUid uid, + EntityQuery entityQuery, + [NotNullWhen(true)] ref T? foundComponent, + MetaDataComponent? meta = null, + TransformComponent? xform = null) where T : IComponent + { + if (!MetaQuery.Resolve(uid, ref meta)) + return false; - if (!TransformQuery.Resolve(uid, ref xform)) - return false; + if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) + return false; - if (!xform.ParentUid.Valid) - return false; + if (!TransformQuery.Resolve(uid, ref xform)) + return false; - if (entityQuery.TryComp(xform.ParentUid, out foundComponent)) - return true; + if (!xform.ParentUid.Valid) + return false; - return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent); - } + if (entityQuery.TryComp(xform.ParentUid, out foundComponent)) + return true; - /// - /// Finds all instances of a component on the recursive parented containers that hold an entity - /// - public bool TryFindComponentsOnEntityContainerOrParent( - EntityUid uid, - EntityQuery entityQuery, - List foundComponents, - MetaDataComponent? meta = null, - TransformComponent? xform = null) where T : IComponent - { - if (!MetaQuery.Resolve(uid, ref meta)) - return foundComponents.Any(); + return TryFindComponentOnEntityContainerOrParent(xform.ParentUid, entityQuery, ref foundComponent); + } - if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) - return foundComponents.Any(); + /// + /// Finds all instances of a component on the recursive parented containers that hold an entity + /// + public bool TryFindComponentsOnEntityContainerOrParent( + EntityUid uid, + EntityQuery entityQuery, + List foundComponents, + MetaDataComponent? meta = null, + TransformComponent? xform = null) where T : IComponent + { + if (!MetaQuery.Resolve(uid, ref meta)) + return foundComponents.Any(); - if (!TransformQuery.Resolve(uid, ref xform)) - return foundComponents.Any(); + if ((meta.Flags & MetaDataFlags.InContainer) != MetaDataFlags.InContainer) + return foundComponents.Any(); - if (!xform.ParentUid.Valid) - return foundComponents.Any(); + if (!TransformQuery.Resolve(uid, ref xform)) + return foundComponents.Any(); - if (TryComp(xform.ParentUid, out T? foundComponent)) - foundComponents.Add(foundComponent); + if (!xform.ParentUid.Valid) + return foundComponents.Any(); - return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents); - } + if (TryComp(xform.ParentUid, out T? foundComponent)) + foundComponents.Add(foundComponent); - /// - /// Returns true if the two entities are not contained, or are contained in the same container. - /// - public bool IsInSameOrNoContainer( - Entity user, - Entity other) - { - var isUserContained = TryGetContainingContainer(user, out var userContainer); - var isOtherContained = TryGetContainingContainer(other, out var otherContainer); + return TryFindComponentsOnEntityContainerOrParent(xform.ParentUid, entityQuery, foundComponents); + } - // Both entities are not in a container - if (!isUserContained && !isOtherContained) return true; + /// + /// Returns true if the two entities are not contained, or are contained in the same container. + /// + public bool IsInSameOrNoContainer( + Entity user, + Entity other) + { + var isUserContained = TryGetContainingContainer(user, out var userContainer); + var isOtherContained = TryGetContainingContainer(other, out var otherContainer); - // Both entities are in different contained states - if (isUserContained != isOtherContained) return false; + // Both entities are not in a container + if (!isUserContained && !isOtherContained) return true; - // Both entities are in the same container - return userContainer == otherContainer; - } + // Both entities are in different contained states + if (isUserContained != isOtherContained) return false; - /// - /// Returns true if the two entities are not contained, or are contained in the same container, or if one - /// entity contains the other (i.e., is the parent). - /// - public bool IsInSameOrParentContainer( - Entity user, - Entity other) - { - return IsInSameOrParentContainer(user, other, out _, out _); - } + // Both entities are in the same container + return userContainer == otherContainer; + } - /// - public bool IsInSameOrParentContainer( - Entity user, - Entity other, - out BaseContainer? userContainer, - out BaseContainer? otherContainer) - { - var isUserContained = TryGetContainingContainer(user, out userContainer); - var isOtherContained = TryGetContainingContainer(other, out otherContainer); + /// + /// Returns true if the two entities are not contained, or are contained in the same container, or if one + /// entity contains the other (i.e., is the parent). + /// + public bool IsInSameOrParentContainer( + Entity user, + Entity other) + { + return IsInSameOrParentContainer(user, other, out _, out _); + } - // Both entities are not in a container - if (!isUserContained && !isOtherContained) return true; + /// + public bool IsInSameOrParentContainer( + Entity user, + Entity other, + out BaseContainer? userContainer, + out BaseContainer? otherContainer) + { + var isUserContained = TryGetContainingContainer(user, out userContainer); + var isOtherContained = TryGetContainingContainer(other, out otherContainer); - // One contains the other - if (userContainer?.Owner == other || otherContainer?.Owner == user) return true; + // Both entities are not in a container + if (!isUserContained && !isOtherContained) return true; - // Both entities are in different contained states - if (isUserContained != isOtherContained) return false; + // One contains the other + if (userContainer?.Owner == other || otherContainer?.Owner == user) return true; - // Both entities are in the same container - return userContainer == otherContainer; - } + // Both entities are in different contained states + if (isUserContained != isOtherContained) return false; - /// - /// Check whether a given entity can see another entity despite whatever containers they may be in. - /// - /// - /// This is effectively a variant of that also checks whether the - /// containers are transparent. Additionally, an entity can "see" the entity that contains it, but unless - /// otherwise specified the containing entity cannot see into itself. For example, a human in a locker can - /// see the locker and other items in that locker, but the human cannot see their own organs. Note that - /// this means that the two entity arguments are NOT interchangeable. - /// - public bool IsInSameOrTransparentContainer( - Entity user, - Entity other, - BaseContainer? userContainer = null, - BaseContainer? otherContainer = null, - bool userSeeInsideSelf = false) - { - if (userContainer == null) - TryGetContainingContainer(user, out userContainer); + // Both entities are in the same container + return userContainer == otherContainer; + } - if (otherContainer == null) - TryGetContainingContainer(other, out otherContainer); + /// + /// Check whether a given entity can see another entity despite whatever containers they may be in. + /// + /// + /// This is effectively a variant of that also checks whether the + /// containers are transparent. Additionally, an entity can "see" the entity that contains it, but unless + /// otherwise specified the containing entity cannot see into itself. For example, a human in a locker can + /// see the locker and other items in that locker, but the human cannot see their own organs. Note that + /// this means that the two entity arguments are NOT interchangeable. + /// + public bool IsInSameOrTransparentContainer( + Entity user, + Entity other, + BaseContainer? userContainer = null, + BaseContainer? otherContainer = null, + bool userSeeInsideSelf = false) + { + if (userContainer == null) + TryGetContainingContainer(user, out userContainer); - // Are both entities in the same container (or none)? - if (userContainer == otherContainer) return true; + if (otherContainer == null) + TryGetContainingContainer(other, out otherContainer); - // Is the user contained in the other entity? - if (userContainer?.Owner == other) return true; + // Are both entities in the same container (or none)? + if (userContainer == otherContainer) return true; - // Does the user contain the other and can they see through themselves? - if (userSeeInsideSelf && otherContainer?.Owner == user) return true; + // Is the user contained in the other entity? + if (userContainer?.Owner == other) return true; - // Next we check for see-through containers. This uses some recursion, but it should be fine unless people - // start spawning in glass matryoshka dolls. + // Does the user contain the other and can they see through themselves? + if (userSeeInsideSelf && otherContainer?.Owner == user) return true; - // Is the user in a see-through container? - if (userContainer?.ShowContents ?? false) - return IsInSameOrTransparentContainer((userContainer.Owner, null, null), other, otherContainer: otherContainer); + // Next we check for see-through containers. This uses some recursion, but it should be fine unless people + // start spawning in glass matryoshka dolls. - // Is the other entity in a see-through container? - if (otherContainer?.ShowContents ?? false) - return IsInSameOrTransparentContainer(user, (otherContainer.Owner, null, null), userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf); + // Is the user in a see-through container? + if (userContainer?.ShowContents ?? false) + return IsInSameOrTransparentContainer((userContainer.Owner, null, null), other, otherContainer: otherContainer); - return false; - } + // Is the other entity in a see-through container? + if (otherContainer?.ShowContents ?? false) + return IsInSameOrTransparentContainer(user, (otherContainer.Owner, null, null), userContainer: userContainer, userSeeInsideSelf: userSeeInsideSelf); - /// - /// Returns the full chain of containers containing the entity passed in, from innermost to outermost. - /// - /// - /// The resulting collection includes the container directly containing the entity (if any), - /// the container containing that container, and so on until reaching the outermost container. - /// - public IEnumerable GetContainingContainers(Entity ent) - { - if (!ent.Owner.IsValid()) - yield break; + return false; + } + + /// + /// Returns the full chain of containers containing the entity passed in, from innermost to outermost. + /// + /// + /// The resulting collection includes the container directly containing the entity (if any), + /// the container containing that container, and so on until reaching the outermost container. + /// + public IEnumerable GetContainingContainers(Entity ent) + { + if (!ent.Owner.IsValid()) + yield break; - if (!Resolve(ent, ref ent.Comp)) - yield break; + if (!Resolve(ent, ref ent.Comp)) + yield break; - var child = ent.Owner; - var parent = ent.Comp.ParentUid; + var child = ent.Owner; + var parent = ent.Comp.ParentUid; - while (parent.IsValid()) + while (parent.IsValid()) + { + if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && + _managerQuery.TryGetComponent(parent, out var conManager) && + TryGetContainingContainer(parent, child, out var parentContainer, conManager)) { - if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && - _managerQuery.TryGetComponent(parent, out var conManager) && - TryGetContainingContainer(parent, child, out var parentContainer, conManager)) - { - yield return parentContainer; - } - - var parentXform = TransformQuery.GetComponent(parent); - child = parent; - parent = parentXform.ParentUid; + yield return parentContainer; } - } - /// - /// Gets the top-most container in the hierarchy for this entity, if it exists. - /// - public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, [NotNullWhen(true)] out BaseContainer? container) - { - return TryGetOuterContainer(uid, xform, out container, TransformQuery); + var parentXform = TransformQuery.GetComponent(parent); + child = parent; + parent = parentXform.ParentUid; } + } - public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, - [NotNullWhen(true)] out BaseContainer? container, EntityQuery xformQuery) - { - container = null; + /// + /// Gets the top-most container in the hierarchy for this entity, if it exists. + /// + public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, [NotNullWhen(true)] out BaseContainer? container) + { + return TryGetOuterContainer(uid, xform, out container, TransformQuery); + } + + public bool TryGetOuterContainer(EntityUid uid, TransformComponent xform, + [NotNullWhen(true)] out BaseContainer? container, EntityQuery xformQuery) + { + container = null; - if (!uid.IsValid()) - return false; + if (!uid.IsValid()) + return false; - var child = uid; - var parent = xform.ParentUid; + var child = uid; + var parent = xform.ParentUid; - while (parent.IsValid()) + while (parent.IsValid()) + { + if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && + _managerQuery.TryGetComponent(parent, out var conManager) && + TryGetContainingContainer(parent, child, out var parentContainer, conManager)) { - if (((MetaQuery.GetComponent(child).Flags & MetaDataFlags.InContainer) == MetaDataFlags.InContainer) && - _managerQuery.TryGetComponent(parent, out var conManager) && - TryGetContainingContainer(parent, child, out var parentContainer, conManager)) - { - container = parentContainer; - } - - var parentXform = xformQuery.GetComponent(parent); - child = parent; - parent = parentXform.ParentUid; + container = parentContainer; } - return container != null; + var parentXform = xformQuery.GetComponent(parent); + child = parent; + parent = parentXform.ParentUid; } - /// - /// Attempts to remove an entity from its container, if any. - /// - /// Entity that might be inside a container. - /// Whether to forcibly remove the entity from the container. - /// Whether the entity was actually inside a container or not. - /// If the entity could be removed. Also returns false if it wasn't inside a container. - public bool TryRemoveFromContainer(Entity entity, bool force, out bool wasInContainer) - { - DebugTools.Assert(Exists(entity)); + return container != null; + } - if (TryGetContainingContainer(entity, out var container)) - { - wasInContainer = true; + /// + /// Attempts to remove an entity from its container, if any. + /// + /// Entity that might be inside a container. + /// Whether to forcibly remove the entity from the container. + /// Whether the entity was actually inside a container or not. + /// If the entity could be removed. Also returns false if it wasn't inside a container. + public bool TryRemoveFromContainer(Entity entity, bool force, out bool wasInContainer) + { + DebugTools.Assert(Exists(entity)); - if (!force) - return Remove(entity, container); + if (TryGetContainingContainer(entity, out var container)) + { + wasInContainer = true; - Remove(entity, container, force: true); - return true; - } + if (!force) + return Remove(entity, container); - wasInContainer = false; - return false; + Remove(entity, container, force: true); + return true; } - /// - /// Attempts to remove an entity from its container, if any. - /// - /// Entity that might be inside a container. - /// Whether to forcibly remove the entity from the container. - /// If the entity could be removed. Also returns false if it wasn't inside a container. - public bool TryRemoveFromContainer(Entity entity, bool force = false) - { - return TryRemoveFromContainer(entity, force, out _); - } + wasInContainer = false; + return false; + } - /// - /// Attempts to remove all entities in a container. Returns removed entities. - /// - public List EmptyContainer( - BaseContainer container, - bool force = false, - EntityCoordinates? destination = null, - bool reparent = true) - { - var removed = new List(container.ContainedEntities); - for (var i = removed.Count - 1; i >= 0; i--) - { - if (Remove(removed[i], container, reparent: reparent, force: force, destination: destination)) - continue; + /// + /// Attempts to remove an entity from its container, if any. + /// + /// Entity that might be inside a container. + /// Whether to forcibly remove the entity from the container. + /// If the entity could be removed. Also returns false if it wasn't inside a container. + public bool TryRemoveFromContainer(Entity entity, bool force = false) + { + return TryRemoveFromContainer(entity, force, out _); + } - // failed to remove entity. - DebugTools.Assert(container.Contains(removed[i])); - removed.RemoveSwap(i); - } + /// + /// Attempts to remove all entities in a container. Returns removed entities. + /// + public List EmptyContainer( + BaseContainer container, + bool force = false, + EntityCoordinates? destination = null, + bool reparent = true) + { + var removed = new List(container.ContainedEntities); + for (var i = removed.Count - 1; i >= 0; i--) + { + if (Remove(removed[i], container, reparent: reparent, force: force, destination: destination)) + continue; - return removed; + // failed to remove entity. + DebugTools.Assert(container.Contains(removed[i])); + removed.RemoveSwap(i); } - /// - /// Attempts to remove and delete all entities in a container. - /// - public void CleanContainer(BaseContainer container) + return removed; + } + + /// + /// Attempts to remove and delete all entities in a container. + /// + public void CleanContainer(BaseContainer container) + { + foreach (var ent in container.ContainedEntities.ToArray()) { - foreach (var ent in container.ContainedEntities.ToArray()) - { - if (Deleted(ent)) - continue; + if (Deleted(ent)) + continue; - Remove(ent, container, force: true); - PredictedDel(ent); - } + Remove(ent, container, force: true); + PredictedDel(ent); } + } - public void AttachParentToContainerOrGrid(Entity transform) + public void AttachParentToContainerOrGrid(Entity transform) + { + // TODO make this check upwards for any container, and parent to that. + // Currently this just checks the direct parent, so entities will still teleport through containers. + if (!transform.Comp.ParentUid.IsValid() + || !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container) + || !TryInsertIntoContainer(transform, container)) { - // TODO make this check upwards for any container, and parent to that. - // Currently this just checks the direct parent, so entities will still teleport through containers. - if (!transform.Comp.ParentUid.IsValid() - || !TryGetContainingContainer((transform.Comp.ParentUid, Transform(transform.Comp.ParentUid)), out var container) - || !TryInsertIntoContainer(transform, container)) - { - _transform.AttachToGridOrMap(transform, transform.Comp); - } + _transform.AttachToGridOrMap(transform, transform.Comp); } + } - private bool TryInsertIntoContainer(Entity transform, BaseContainer container) - { - if (Insert((transform.Owner, transform.Comp, null, null), container)) - return true; + private bool TryInsertIntoContainer(Entity transform, BaseContainer container) + { + if (Insert((transform.Owner, transform.Comp, null, null), container)) + return true; - var ownerXform = Transform(container.Owner); - if (ownerXform.ParentUid.IsValid() - && TryGetContainingContainer((container.Owner, ownerXform), out var newContainer)) - return TryInsertIntoContainer(transform, newContainer); + var ownerXform = Transform(container.Owner); + if (ownerXform.ParentUid.IsValid() + && TryGetContainingContainer((container.Owner, ownerXform), out var newContainer)) + return TryInsertIntoContainer(transform, newContainer); - return false; - } + return false; + } - internal bool TryGetManagerComp(EntityUid entity, [NotNullWhen(true)] out ContainerManagerComponent? manager) - { - DebugTools.Assert(Exists(entity)); + internal bool TryGetManagerComp(EntityUid entity, [NotNullWhen(true)] out ContainerManagerComponent? manager) + { + DebugTools.Assert(Exists(entity)); - if (TryComp(entity, out manager)) - return true; + if (TryComp(entity, out manager)) + return true; - // RECURSION ALERT - var transform = Transform(entity); - if (transform.ParentUid.IsValid()) - return TryGetManagerComp(transform.ParentUid, out manager); + // RECURSION ALERT + var transform = Transform(entity); + if (transform.ParentUid.IsValid()) + return TryGetManagerComp(transform.ParentUid, out manager); - return false; - } + return false; + } - #endregion + #endregion - protected virtual void OnParentChanged(ref EntParentChangedMessage message) - { - var meta = MetaData(message.Entity); - if ((meta.Flags & MetaDataFlags.InContainer) == 0) - return; + protected virtual void OnParentChanged(ref EntParentChangedMessage message) + { + var meta = MetaData(message.Entity); + if ((meta.Flags & MetaDataFlags.InContainer) == 0) + return; - // Eject entities from their parent container if the parent change is done via setting the transform. - if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager)) - RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true); - } + // Eject entities from their parent container if the parent change is done via setting the transform. + if (TryComp(message.OldParent, out ContainerManagerComponent? containerManager)) + RemoveEntity(message.OldParent.Value, message.Entity, containerManager, message.Transform, meta, reparent: false, force: true); + } - [Conditional("DEBUG"), Access(typeof(BaseContainer))] - public void AssertInContainer(EntityUid uid, BaseContainer container) - { - if (_timing.ApplyingState) - return; // Entity might not yet have had its state updated. + [Conditional("DEBUG"), Access(typeof(BaseContainer))] + public void AssertInContainer(EntityUid uid, BaseContainer container) + { + if (_timing.ApplyingState) + return; // Entity might not yet have had its state updated. - var flags = MetaData(uid).Flags; - DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, - $"Entity has bad container flags. Ent: {ToPrettyString(uid)}. Container: {container.ID}, Owner: {ToPrettyString(container.Owner)}"); - } + var flags = MetaData(uid).Flags; + DebugTools.Assert((flags & MetaDataFlags.InContainer) != 0, + $"Entity has bad container flags. Ent: {ToPrettyString(uid)}. Container: {container.ID}, Owner: {ToPrettyString(container.Owner)}"); } } From 7e74ae7084cb49754cd884d8aa9fb57b7de7edef Mon Sep 17 00:00:00 2001 From: Connor Huffine Date: Sat, 27 Jun 2026 23:08:00 -0400 Subject: [PATCH 083/178] Reduce `ReallyBeIdle` default ticks (#6675) Reduce tick counts --- Robust.UnitTesting/Pool/TestPair.Helpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.UnitTesting/Pool/TestPair.Helpers.cs b/Robust.UnitTesting/Pool/TestPair.Helpers.cs index 624b7241e35..1ada68dcb5a 100644 --- a/Robust.UnitTesting/Pool/TestPair.Helpers.cs +++ b/Robust.UnitTesting/Pool/TestPair.Helpers.cs @@ -231,7 +231,7 @@ public async Task RunUntilSynced() /// Runs the server-client pair in sync, but also ensures they are both idle each tick. /// /// How many ticks to run - public async Task ReallyBeIdle(int runTicks = 25) + public async Task ReallyBeIdle(int runTicks = 5) { for (var i = 0; i < runTicks; i++) { From 08dd77afc9656f72a81fa944b1da5184ffa4c284 Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:16:35 -0700 Subject: [PATCH 084/178] Fix OnClientRequestFull throwing an error when trying to log data about a deleted entity (#6420) * Fix OnClientRequestFull throwing an error when trying to log data about a deleted entity * review --------- Co-authored-by: metalgearsloth --- Robust.Server/GameStates/PvsSystem.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Robust.Server/GameStates/PvsSystem.cs b/Robust.Server/GameStates/PvsSystem.cs index 02615e9ea73..1f93d1e54be 100644 --- a/Robust.Server/GameStates/PvsSystem.cs +++ b/Robust.Server/GameStates/PvsSystem.cs @@ -243,8 +243,15 @@ private void OnClientRequestFull(ICommonSession session, GameTick tick, NetEntit if (missingEntity != null) { - var (entity, meta) = GetEntityData(missingEntity.Value); - sb.Append($" Apparently they received an entity without metadata: {ToPrettyString(entity)}."); + if (TryGetEntityData(missingEntity.Value, out var uid, out _)) + { + sb.Append($" Apparently they received an entity without metadata: {ToPrettyString(uid)}."); + } + else + { + sb.Append($" Apparently they received an entity without metadata (No entity found)."); + } + //sb.Append($" Entity last seen: {meta.PvsData[sessionData.Index].EntityLastAcked}"); } From 6c017eba7641e0f9a25fff23fdaa66e33c957095 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:28:23 +0200 Subject: [PATCH 085/178] Fix ApplyLinearImpulse treating the impulse as being both in world and local space (#6444) Fix impulse being set in world coordinates but angular velocity applied in local coordinates --- .../Physics/Systems/SharedPhysicsSystem.Components.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 1c13c149d20..4a093b451ca 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -235,7 +235,8 @@ public void ApplyLinearImpulse(EntityUid uid, Vector2 impulse, Vector2 point, Fi } SetLinearVelocity(uid, body.LinearVelocity + impulse * body._invMass, body: body); - SetAngularVelocity(uid, body.AngularVelocity + body.InvI * Vector2Helpers.Cross(point - body._localCenter, impulse), body: body); + var matrix = _transform.GetWorldMatrix(uid); + SetAngularVelocity(uid, body.AngularVelocity + body.InvI * Vector2Helpers.Cross(Vector2.Transform(point, matrix) - Vector2.Transform(body._localCenter, matrix), impulse), body: body); } #endregion From dd2d4b11826457cb30e225f2d8761b18917cb683 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Sun, 28 Jun 2026 03:48:54 -0700 Subject: [PATCH 086/178] Fix `Robust.Benchmarks` compile (#6679) fix Robust.Benchmarks compile --- Directory.Build.props | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 446a43ae83c..8d998165911 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -24,4 +24,10 @@ --> $(NoWarn);NU1510 + + + + false + From c3f80b99bd977a94357a6249877a3df97544a964 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sun, 28 Jun 2026 03:56:10 -0700 Subject: [PATCH 087/178] Change SpawnAtPoisition EntityCoordinates overload to use the rotation of the attached entity, and to allow a rotation override. (#6527) * minor API addition * better * I forgor --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../GameObjects/ClientEntityManager.Spawn.cs | 7 +++++++ Robust.Shared/GameObjects/EntityManager.Spawn.cs | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs b/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs index a83ef38a9a2..46a97463b6b 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.Spawn.cs @@ -40,6 +40,13 @@ public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoor return ent; } + public override EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + { + var ent = SpawnAtPosition(protoName, coordinates, rotation, overrides); + FlagPredicted(ent); + return ent; + } + public override bool PredictedTrySpawnNextTo( string? protoName, EntityUid target, diff --git a/Robust.Shared/GameObjects/EntityManager.Spawn.cs b/Robust.Shared/GameObjects/EntityManager.Spawn.cs index 94a86489b37..046cf3deab5 100644 --- a/Robust.Shared/GameObjects/EntityManager.Spawn.cs +++ b/Robust.Shared/GameObjects/EntityManager.Spawn.cs @@ -125,7 +125,11 @@ public virtual EntityUid Spawn(string? protoName, MapCoordinates coordinates, Co [MethodImpl(MethodImplOptions.AggressiveInlining)] public EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, ComponentRegistry? overrides = null) - => Spawn(protoName, _xforms.ToMapCoordinates(coordinates), overrides); + => SpawnAtPosition(protoName, coordinates, _xforms.GetWorldRotation(coordinates.EntityId), overrides); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EntityUid SpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + => Spawn(protoName, _xforms.ToMapCoordinates(coordinates), overrides, rotation: rotation); public bool TrySpawnNextTo( string? protoName, @@ -254,6 +258,11 @@ public virtual EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoord return SpawnAtPosition(protoName, coordinates, overrides); } + public virtual EntityUid PredictedSpawnAtPosition(string? protoName, EntityCoordinates coordinates, Angle rotation, ComponentRegistry? overrides = null) + { + return SpawnAtPosition(protoName, coordinates, rotation, overrides); + } + public virtual bool PredictedTrySpawnNextTo( string? protoName, EntityUid target, From d9cd400e006f0e0b1b2f7233c05a3f8aeb5ce795 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 28 Jun 2026 21:18:15 +1000 Subject: [PATCH 088/178] Optimise Box2i (#6662) --- RELEASE-NOTES.md | 2 +- Robust.Shared.Maths.Tests/Box2i_Test.cs | 129 ++++- Robust.Shared.Maths/Box2i.cs | 669 ++++++++++++++++-------- 3 files changed, 556 insertions(+), 244 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 0d36599caca..677400e8b93 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Validate Box2i inputs to ensure no negative-sized boxes. ### New features diff --git a/Robust.Shared.Maths.Tests/Box2i_Test.cs b/Robust.Shared.Maths.Tests/Box2i_Test.cs index c924fc50f6c..be1a5cdd5b2 100644 --- a/Robust.Shared.Maths.Tests/Box2i_Test.cs +++ b/Robust.Shared.Maths.Tests/Box2i_Test.cs @@ -1,38 +1,129 @@ using NUnit.Framework; -namespace Robust.Shared.Maths.Tests +namespace Robust.Shared.Maths.Tests; + +[TestFixture, Parallelizable, TestOf(typeof(Box2i))] +internal sealed class Box2i_Test { - [TestFixture, Parallelizable, TestOf(typeof(Box2i))] - internal sealed class Box2i_Test + [Test] + public void Box2iUnion() { - [Test] - public void Box2iUnion() - { - var boxOne = new Box2i(-1, -1, 1, 1); - var boxTwo = new Box2i(0, 0, 2, 2); + var boxOne = new Box2i(-1, -1, 1, 1); + var boxTwo = new Box2i(0, 0, 2, 2); - var result = boxOne.Union(boxTwo); + var result = boxOne.Union(boxTwo); + using (Assert.EnterMultipleScope()) + { Assert.That(result.Left, Is.EqualTo(-1)); Assert.That(result.Bottom, Is.EqualTo(-1)); Assert.That(result.Right, Is.EqualTo(2)); Assert.That(result.Top, Is.EqualTo(2)); } + } + + [Test] + public void Box2iVector2iUnion() + { + var box = new Box2i(); + Assert.That(box, Is.EqualTo(Box2i.Empty)); + + box = box.UnionTile(Vector2i.Zero); + Assert.That(box.Right, Is.EqualTo(1)); + + box = box.UnionTile(Vector2i.One); + Assert.That(box.Top, Is.EqualTo(2)); + + box = box.Union(new Vector2i(2, 0)); + Assert.That(box.Right, Is.EqualTo(2)); + } + + [Test] + public void Box2iUsesDirectDimensions() + { + var valid = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.That(valid.Width, Is.EqualTo(4)); + Assert.That(valid.Height, Is.EqualTo(6)); + Assert.That(valid.Size, Is.EqualTo(new Vector2i(4, 6))); + Assert.That(valid.IsValid(), Is.True); + } + } + + [Test] + public void Box2iValidatesConstruction() + { + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => new Box2i(3, 4, -1, -2)); + Assert.Throws(() => new Box2i(new Vector2i(3, 4), new Vector2i(-1, -2))); + } + } + + [Test] + public void Box2iValidatesProperties() + { + var box = new Box2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) + { + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Bottom = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Top = -3); + Assert.Throws(() => box.BottomLeft = new Vector2i(4, 0)); + Assert.Throws(() => box.TopRight = new Vector2i(0, -3)); + } + } - [Test] - public void Box2iVector2iUnion() + [Test] + public void Box2iFromTwoPointsNormalizes() + { + var box = Box2i.FromTwoPoints(new Vector2i(3, -2), new Vector2i(-1, 4)); + + Assert.That(box, Is.EqualTo(new Box2i(-1, -2, 3, 4))); + Assert.That(box.IsValid(), Is.True); + } + + [Test] + public void Box2iContainsUsesValidBounds() + { + var box = new Box2i(-1, -1, 1, 1); + + using (Assert.EnterMultipleScope()) { - var box = new Box2i(); - Assert.That(box, Is.EqualTo(Box2i.Empty)); + Assert.That(box.Contains(Vector2i.Zero), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1)), Is.True); + Assert.That(box.Contains(new Vector2i(1, 1), false), Is.False); + Assert.That(box.Contains(new Box2i(0, 0, 1, 1)), Is.True); + Assert.That(box.Encloses(new Box2i(0, 0, 1, 1)), Is.False); + } + } + + [Test] + public void Box2iIntersect() + { + var boxOne = new Box2i(-1, -1, 2, 2); + var boxTwo = new Box2i(0, 1, 3, 4); - box = box.UnionTile(Vector2i.Zero); - Assert.That(box.Right, Is.EqualTo(1)); + using (Assert.EnterMultipleScope()) + { + Assert.That(boxOne.Intersect(boxTwo), Is.EqualTo(new Box2i(0, 1, 2, 2))); + Assert.That(boxOne.Intersect(new Box2i(3, 3, 4, 4)), Is.EqualTo(Box2i.Empty)); + } + } - box = box.UnionTile(Vector2i.One); - Assert.That(box.Top, Is.EqualTo(2)); + [Test] + public void Box2iClosestPoint() + { + var box = new Box2i(-1, -2, 3, 4); - box = box.Union(new Vector2i(2, 0)); - Assert.That(box.Right, Is.EqualTo(2)); + using (Assert.EnterMultipleScope()) + { + Assert.That(box.ClosestPoint(new Vector2i(10, -10)), Is.EqualTo(new Vector2i(3, -2))); + Assert.That(box.ClosestPoint(Vector2i.Zero), Is.EqualTo(Vector2i.Zero)); } } } diff --git a/Robust.Shared.Maths/Box2i.cs b/Robust.Shared.Maths/Box2i.cs index f0596ed57ec..710af7a11f9 100644 --- a/Robust.Shared.Maths/Box2i.cs +++ b/Robust.Shared.Maths/Box2i.cs @@ -5,293 +5,514 @@ using System.Runtime.InteropServices; using Robust.Shared.Utility; -namespace Robust.Shared.Maths -{ - [Serializable] - [StructLayout(LayoutKind.Explicit)] - public struct Box2i : IEquatable, ISpanFormattable - { - public static Box2i Empty => new(); +namespace Robust.Shared.Maths; - [FieldOffset(sizeof(int) * 0)] public int Left; - [FieldOffset(sizeof(int) * 1)] public int Bottom; - [FieldOffset(sizeof(int) * 2)] public int Right; - [FieldOffset(sizeof(int) * 3)] public int Top; - - [FieldOffset(sizeof(int) * 0)] public Vector2i BottomLeft; - [FieldOffset(sizeof(int) * 2)] public Vector2i TopRight; +[Serializable] +[StructLayout(LayoutKind.Explicit)] +public struct Box2i : IEquatable, ISpanFormattable +{ + public static Box2i Empty => new(); - public readonly Vector2i BottomRight => new(Right, Bottom); - public readonly Vector2i TopLeft => new(Left, Top); - public readonly int Width => Math.Abs(Right - Left); - public readonly int Height => Math.Abs(Top - Bottom); - public readonly Vector2i Size => new(Width, Height); + [FieldOffset(sizeof(int) * 0)] internal int _left; + [FieldOffset(sizeof(int) * 1)] internal int _bottom; + [FieldOffset(sizeof(int) * 2)] internal int _right; + [FieldOffset(sizeof(int) * 3)] internal int _top; - public readonly int Area => Width * Height; - public readonly Vector2 Center => Size / 2f + BottomLeft; + [FieldOffset(sizeof(int) * 0)] internal Vector2i _bottomLeft; + [FieldOffset(sizeof(int) * 2)] internal Vector2i _topRight; - public Box2i(Vector2i bottomLeft, Vector2i topRight) + public int Left + { + readonly get => _left; + set { - Unsafe.SkipInit(out this); + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); - BottomLeft = bottomLeft; - TopRight = topRight; + _left = value; } + } - public Box2i(int left, int bottom, int right, int top) + public int Bottom + { + readonly get => _bottom; + set { - Unsafe.SkipInit(out this); + if (value > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be greater than Top."); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + _bottom = value; } + } - public static Box2i FromDimensions(int left, int bottom, int width, int height) + public int Right + { + readonly get => _right; + set { - return new(left, bottom, left + width, bottom + height); - } + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); - public static Box2i FromDimensions(Vector2i position, Vector2i size) - { - return FromDimensions(position.X, position.Y, size.X, size.Y); + _right = value; } + } - public readonly bool Contains(int x, int y) + public int Top + { + readonly get => _top; + set { - return Contains(new Vector2i(x, y)); - } + if (value < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be less than Bottom."); - public readonly bool Contains(Vector2i point, bool closedRegion = true) - { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Bottom ^ point.Y > Top - : point.Y > Bottom ^ point.Y >= Top; - return xOk && yOk; + _top = value; } + } - /// - /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. - /// - public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) + public Vector2i BottomLeft + { + readonly get => _bottomLeft; + set { - var xOk = closedRegion - ? tile.X >= Left ^ tile.X + 1 > Right - : tile.X > Left ^ tile.X + 1 >= Right; - var yOk = closedRegion - ? tile.Y >= Bottom ^ tile.Y + 1 > Top - : tile.Y > Bottom ^ tile.Y + 1 >= Top; - return xOk && yOk; - } + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.X cannot be greater than Right."); - public readonly bool IsEmpty() - { - return Bottom == Top || Left == Right; - } + if (value.Y > _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomLeft.Y cannot be greater than Top."); - /// Returns a UIBox2 translated by the given amount. - public readonly Box2i Translated(Vector2i point) - { - return new(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); + _bottomLeft = value; } + } - /// - /// Returns the smallest rectangle that contains both of the rectangles. - /// - [Pure] - public readonly Box2i Union(in Box2i other) + public Vector2i TopRight + { + readonly get => _topRight; + set { - var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); - var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.X cannot be less than Left."); - if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) - return new Box2i(botLeft, topRight); + if (value.Y < _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopRight.Y cannot be less than Bottom."); - return new Box2i(); + _topRight = value; } + } - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as a single point and not an entire tile. - /// - public readonly Box2i Union(in Vector2i other) - { - if (Contains(other)) - return this; + public readonly Vector2i BottomRight => new(Right, Bottom); - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other); + public readonly Vector2i TopLeft => new(Left, Top); - return new Box2i(botLeft, topRight); - } + public readonly int Width => _right - _left; - /// - /// Unions the box2i with the specified Vector2i. - /// - /// - /// Union treating other as an entire tile and not a single point. - /// - public readonly Box2i UnionTile(in Vector2i other) - { - if (ContainsTile(other)) - return this; + public readonly int Height => _top - _bottom; - var botLeft = Vector2i.ComponentMin(BottomLeft, other); - var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + public readonly Vector2i Size => new(Width, Height); - return new Box2i(botLeft, topRight); - } + public readonly int Area => Width * Height; + public readonly Vector2 Center => new Vector2(_left + _right, _bottom + _top) / 2f; - // override object.Equals - public readonly override bool Equals(object? obj) - { - if (obj is Box2i box) - { - return Equals(box); - } + private static void Validate(int left, int bottom, int right, int top) + { + if (left > right) + throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); - return false; - } + if (bottom > top) + throw new ArgumentException("Bottom cannot be greater than Top.", nameof(bottom)); + } - public readonly bool Equals(Box2i other) - { - return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; - } + public Box2i(Vector2i bottomLeft, Vector2i topRight) + { + Unsafe.SkipInit(out this); - // override object.GetHashCode - public readonly override int GetHashCode() - { - var code = Left.GetHashCode(); - code = (code * 929) ^ Right.GetHashCode(); - code = (code * 929) ^ Top.GetHashCode(); - code = (code * 929) ^ Bottom.GetHashCode(); - return code; - } + Validate(bottomLeft.X, bottomLeft.Y, topRight.X, topRight.Y); - public static explicit operator Box2i(Box2 box) - { - return new((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); - } + _bottomLeft = bottomLeft; + _topRight = topRight; + } - public static implicit operator Box2(Box2i box) - { - return new(box.Left, box.Bottom, box.Right, box.Top); - } + public Box2i(int left, int bottom, int right, int top) + { + Unsafe.SkipInit(out this); - public readonly override string ToString() - { - return $"({Left}, {Bottom}, {Right}, {Top})"; - } + Validate(left, bottom, right, top); - public readonly string ToString(string? format, IFormatProvider? formatProvider) - { - return ToString(); - } + _left = left; + _right = right; + _top = top; + _bottom = bottom; + } - public readonly bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider) - { - return FormatHelpers.TryFormatInto( - destination, - out charsWritten, - $"({Left}, {Bottom}, {Right}, {Top})"); - } + /// + /// Creates a Box2i with no bounds validation applied, use at your own risk. + /// + internal static Box2i DangerousCreate(int left, int bottom, int right, int top) + { + Unsafe.SkipInit(out Box2i box); + box._left = left; + box._right = right; + box._top = top; + box._bottom = bottom; + return box; + } - /// - /// Multiplies each side of the box by the scalar. - /// - [Pure] - public Box2i Scale(int scalar) - { - return new Box2i( - Left * scalar, - Bottom * scalar, - Right * scalar, - Top * scalar); - } + [Pure] + public static Box2i FromDimensions(int left, int bottom, int width, int height) + { + return new Box2i(left, bottom, left + width, bottom + height); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public bool Intersects(in Box2i other) - { - return other.Bottom <= this.Top && other.Top >= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; - } + [Pure] + public static Box2i FromDimensions(Vector2i position, Vector2i size) + { + return FromDimensions(position.X, position.Y, size.X, size.Y); + } + + [Pure] + public static Box2i FromTwoPoints(Vector2i a, Vector2i b) + { + return new Box2i(Vector2i.ComponentMin(a, b), Vector2i.ComponentMax(a, b)); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - [Pure] - public readonly Box2i Enlarged(int size) + [Pure] + public readonly bool Contains(int x, int y) + { + return Contains(new Vector2i(x, y)); + } + + [Pure] + public readonly bool Contains(in Box2i inner) + => Left <= inner.Left + && Bottom <= inner.Bottom + && Right >= inner.Right + && Top >= inner.Top; + + [Pure] + public readonly bool Contains(Vector2i point, bool closedRegion = true) + { + var xOk = closedRegion + ? point.X >= Left ^ point.X > Right + : point.X > Left ^ point.X >= Right; + var yOk = closedRegion + ? point.Y >= Bottom ^ point.Y > Top + : point.Y > Bottom ^ point.Y >= Top; + return xOk && yOk; + } + + /// + /// Unlike Contains this assumes the Vector2i occupies an entire tile so we need the point to the top-right of it for consideration. + /// + [Pure] + public readonly bool ContainsTile(Vector2i tile, bool closedRegion = true) + { + if (closedRegion) { - return new(Left - size, Bottom - size, Right + size, Top + size); + return tile.X >= Left + && tile.X + 1 <= Right + && tile.Y >= Bottom + && tile.Y + 1 <= Top; } + + return tile.X > Left + && tile.X + 1 < Right + && tile.Y > Bottom + && tile.Y + 1 < Top; + } + + [Pure] + public readonly bool IsEmpty() + { + return Bottom >= Top || Left >= Right; + } + + /// Returns a UIBox2 translated by the given amount. + [Pure] + public readonly Box2i Translated(Vector2i point) + { + return new Box2i(Left + point.X, Bottom + point.Y, Right + point.X, Top + point.Y); + } + + /// + /// Returns the smallest rectangle that contains both of the rectangles. + /// + [Pure] + public readonly Box2i Union(in Box2i other) + { + var botLeft = Vector2i.ComponentMin(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMax(TopRight, other.TopRight); + + if (botLeft.X <= topRight.X && botLeft.Y <= topRight.Y) + return new Box2i(botLeft, topRight); + + return new Box2i(); + } + + /// + /// Unions the box2i with the specified Vector2i. + /// + /// + /// Union treating other as a single point and not an entire tile. + /// + [Pure] + public readonly Box2i Union(in Vector2i other) + { + if (Contains(other)) + return this; + + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other); + + return new Box2i(botLeft, topRight); } /// - /// Iterates neighbouring tiles to a box2i. + /// Unions the box2i with the specified Vector2i. /// - public struct Box2iEdgeEnumerator + /// + /// Union treating other as an entire tile and not a single point. + /// + [Pure] + public readonly Box2i UnionTile(in Vector2i other) { - private readonly bool _corners; - private readonly Box2i _box; - private readonly int _offset; - private int _x; - private int _y; + if (ContainsTile(other)) + return this; - public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + var botLeft = Vector2i.ComponentMin(BottomLeft, other); + var topRight = Vector2i.ComponentMax(TopRight, other + Vector2i.One); + + return new Box2i(botLeft, topRight); + } + + // override object.Equals + public readonly override bool Equals(object? obj) + { + if (obj is Box2i box) { - _box = box; - _corners = corners; - _x = _box.Left - offset; - _y = _box.Bottom - offset; - _offset = offset; + return Equals(box); } - public bool MoveNext(out Vector2i index) + return false; + } + + public readonly bool Equals(Box2i other) + { + return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; + } + + // override object.GetHashCode + public readonly override int GetHashCode() + { + var code = Left.GetHashCode(); + code = (code * 929) ^ Right.GetHashCode(); + code = (code * 929) ^ Top.GetHashCode(); + code = (code * 929) ^ Bottom.GetHashCode(); + return code; + } + + public static explicit operator Box2i(Box2 box) + { + return new Box2i((int) box.Left, (int) box.Bottom, (int) box.Right, (int) box.Top); + } + + public static implicit operator Box2(Box2i box) + { + return new Box2(box.Left, box.Bottom, box.Right, box.Top); + } + + public readonly override string ToString() + { + return $"({Left}, {Bottom}, {Right}, {Top})"; + } + + /// + /// Compares two objects for equality by value. + /// + public static bool operator ==(Box2i a, Box2i b) + { + return a.Equals(b); + } + + public static bool operator !=(Box2i a, Box2i b) + { + return !a.Equals(b); + } + + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString(); + } + + public readonly bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider) + { + return FormatHelpers.TryFormatInto( + destination, + out charsWritten, + $"({Left}, {Bottom}, {Right}, {Top})"); + } + + /// + /// Multiplies each side of the box by the scalar. + /// + [Pure] + public readonly Box2i Scale(int scalar) + { + return new Box2i( + Left * scalar, + Bottom * scalar, + Right * scalar, + Top * scalar); + } + + [Pure] + public readonly bool Intersects(in Box2i other) + { + return other._bottom <= _top + && other._top >= _bottom + && other._right >= _left + && other._left <= _right; + } + + [Pure] + public readonly Box2i Enlarged(int size) + { + return new Box2i(Left - size, Bottom - size, Right + size, Top + size); + } + + /// + /// Returns the intersection box created when two boxes overlap. + /// + [Pure] + public readonly Box2i Intersect(in Box2i other) + { + var bottomLeft = Vector2i.ComponentMax(BottomLeft, other.BottomLeft); + var topRight = Vector2i.ComponentMin(TopRight, other.TopRight); + + if (bottomLeft.X <= topRight.X && bottomLeft.Y <= topRight.Y) + return new Box2i(bottomLeft, topRight); + + return new Box2i(); + } + + [Pure] + public readonly bool IsValid() + { + return Right >= Left && Top >= Bottom; + } + + [Pure] + public readonly bool Encloses(in Box2i inner) + { + return Left < inner.Left && Bottom < inner.Bottom && Right > inner.Right && Top > inner.Top; + } + + /// + /// Returns this box enlarged to also contain the specified position. + /// + [Pure] + public readonly Box2i ExtendToContain(Vector2i vec) + { + return new Box2i(Vector2i.ComponentMin(BottomLeft, vec), Vector2i.ComponentMax(TopRight, vec)); + } + + /// + /// Given a point, returns the closest point to it inside the box. + /// + [Pure] + public readonly Vector2i ClosestPoint(in Vector2i position) + { + return new Vector2i( + MathHelper.Clamp(position.X, Left, Right), + MathHelper.Clamp(position.Y, Bottom, Top)); + } + + public static int Perimeter(in Box2i box) + => (box.Width + box.Height) * 2; + + public static int UnionPerimeter(in Box2i a, in Box2i b) + { + var left = Math.Min(a._left, b._left); + var bottom = Math.Min(a._bottom, b._bottom); + var right = Math.Max(a._right, b._right); + var top = Math.Max(a._top, b._top); + + return 2 * ((right - left) + (top - bottom)); + } + + [Pure] + public static Box2i Union(Box2i a, Box2i b) + { + return new Box2i( + Vector2i.ComponentMin(a.BottomLeft, b.BottomLeft), + Vector2i.ComponentMax(a.TopRight, b.TopRight)); + } + + [Pure] + public static Box2i Union(in Vector2i a, in Vector2i b) + { + return FromTwoPoints(a, b); + } +} + +/// +/// Iterates neighbouring tiles to a box2i. +/// +public struct Box2iEdgeEnumerator +{ + private readonly bool _corners; + private readonly Box2i _box; + private readonly int _offset; + private int _x; + private int _y; + + public Box2iEdgeEnumerator(Box2i box, bool corners, int offset = 1) + { + _box = box; + _corners = corners; + _x = _box.Left - offset; + _y = _box.Bottom - offset; + _offset = offset; + } + + public bool MoveNext(out Vector2i index) + { + for (var x = _x; x < _box.Right + _offset; x++) { - for (var x = _x; x < _box.Right + _offset; x++) + for (var y = _y; y < _box.Top + _offset; y++) { - for (var y = _y; y < _box.Top + _offset; y++) + if (x != _box.Left - _offset && + x != _box.Right + (_offset - 1) && + y != _box.Bottom - _offset && + y != _box.Top + (_offset - 1)) { - if (x != _box.Left - _offset && - x != _box.Right + (_offset - 1) && - y != _box.Bottom - _offset && - y != _box.Top + (_offset - 1)) - { - continue; - } - - if (!_corners && - (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || - x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) - { - continue; - } - - _x = x; - _y = y + 1; - - if (_y == _box.Top + _offset) - { - _x++; - _y = _box.Bottom - _offset; - } - - index = new Vector2i(x, y); - return true; + continue; + } + + if (!_corners && + (x == _box.Left - _offset && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)) || + x == _box.Right && (y == _box.Bottom - _offset || y == _box.Top + (_offset - 1)))) + { + continue; } - } - index = default; - return false; + _x = x; + _y = y + 1; + + if (_y == _box.Top + _offset) + { + _x++; + _y = _box.Bottom - _offset; + } + + index = new Vector2i(x, y); + return true; + } } + + index = default; + return false; } } From 7dbb2a2614a3e1c08cd4b661b0d834601cfc1dbe Mon Sep 17 00:00:00 2001 From: TheShuEd <96445749+TheShuEd@users.noreply.github.com> Date: Sun, 28 Jun 2026 15:31:06 +0300 Subject: [PATCH 089/178] Clean up Player manager (#6566) * file-scoping Player manager * readonly playerManager * Update PlayerManager.cs * revert readonly --- Robust.Client/Player/PlayerManager.cs | 479 +++++++++++++------------- Robust.Server/Player/FilterSystem.cs | 20 +- Robust.Server/Player/PlayerManager.cs | 296 ++++++++-------- 3 files changed, 395 insertions(+), 400 deletions(-) diff --git a/Robust.Client/Player/PlayerManager.cs b/Robust.Client/Player/PlayerManager.cs index 0bc24f35cce..16d248ea5e6 100644 --- a/Robust.Client/Player/PlayerManager.cs +++ b/Robust.Client/Player/PlayerManager.cs @@ -10,312 +10,311 @@ using Robust.Shared.Player; using Robust.Shared.Utility; -namespace Robust.Client.Player +namespace Robust.Client.Player; + +/// +/// Here's the player controller. This will handle attaching GUIs and input to controllable things. +/// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. +/// This class also communicates with the server to let the server control what entity it is attached to. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { + [Dependency] private IClientNetManager _network = default!; + [Dependency] private IBaseClient _client = default!; + /// - /// Here's the player controller. This will handle attaching GUIs and input to controllable things. - /// Why not just attach the inputs directly? It's messy! This makes the whole thing nicely encapsulated. - /// This class also communicates with the server to let the server control what entity it is attached to. + /// Received player states that had an unknown . /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - [Dependency] private IClientNetManager _network = default!; - [Dependency] private IBaseClient _client = default!; + private Dictionary _pendingStates = new(); + private List _pending = new(); - /// - /// Received player states that had an unknown . - /// - private Dictionary _pendingStates = new(); - private List _pending = new(); - - /// - public override ICommonSession[] NetworkedSessions + /// + public override ICommonSession[] NetworkedSessions + { + get { - get - { - return LocalSession != null - ? new[] { LocalSession } - : Array.Empty(); - } + return LocalSession != null + ? new[] { LocalSession } + : Array.Empty(); } + } - /// - public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; + /// + public override int MaxPlayers => _client.GameInfo?.ServerMaxPlayers ?? -1; - public LocalPlayer? LocalPlayer { get; private set; } + public LocalPlayer? LocalPlayer { get; private set; } - public event Action? LocalStatusChanged; - public event Action? PlayerListUpdated; - public event Action? LocalPlayerDetached; - public event Action? LocalPlayerAttached; - public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; + public event Action? LocalStatusChanged; + public event Action? PlayerListUpdated; + public event Action? LocalPlayerDetached; + public event Action? LocalPlayerAttached; + public event Action<(ICommonSession? Old, ICommonSession? New)>? LocalSessionChanged; - /// - public override void Initialize(int maxPlayers) + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerList); + PlayerStatusChanged += StatusChanged; + } + + private void StatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.Session == LocalPlayer?.Session) + LocalStatusChanged?.Invoke(e); + } + + public void SetupSinglePlayer(string name) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + var session = CreateAndAddSession(default, name); + session.ClientSide = true; + SetLocalSession(session); + Startup(); + PlayerListUpdated?.Invoke(); + } + + public void SetupMultiplayer(INetChannel channel) + { + if (LocalSession != null) + throw new InvalidOperationException($"Player manager already running?"); + + SetLocalSession(CreateAndAddSession(channel)); + Startup(); + _network.ClientSendMessage(new MsgPlayerListReq()); + } + + public void SetLocalSession(ICommonSession? session) + { + if (session == LocalSession) + return; + + var old = LocalSession; + + if (old?.AttachedEntity is { } oldUid) { - base.Initialize(maxPlayers); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(HandlePlayerList); - PlayerStatusChanged += StatusChanged; + LocalSession = null; + LocalPlayer = null; + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); + EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); + LocalPlayerDetached?.Invoke(oldUid); } - private void StatusChanged(object? sender, SessionStatusEventArgs e) + LocalSession = session; + LocalPlayer = session == null ? null : new LocalPlayer(session); + Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); + LocalSessionChanged?.Invoke((old, LocalSession)); + + if (session?.AttachedEntity is { } newUid) { - if (e.Session == LocalPlayer?.Session) - LocalStatusChanged?.Invoke(e); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); + EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); + LocalPlayerAttached?.Invoke(newUid); } + } - public void SetupSinglePlayer(string name) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + /// + public override void Shutdown() + { + SetAttachedEntity(LocalSession, null, out _); + LocalPlayer = null; + LocalSession = null; + _pendingStates.Clear(); + base.Shutdown(); + PlayerListUpdated?.Invoke(); + } - var session = CreateAndAddSession(default, name); - session.ClientSide = true; - SetLocalSession(session); - Startup(); - PlayerListUpdated?.Invoke(); - } + public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) + { + kicked = null; + if (session == null) + return false; - public void SetupMultiplayer(INetChannel channel) - { - if (LocalSession != null) - throw new InvalidOperationException($"Player manager already running?"); + if (session.AttachedEntity == uid) + return true; + + var old = session.AttachedEntity; + if (!base.SetAttachedEntity(session, uid, out kicked, force)) + return false; + + if (session != LocalSession) + return true; - SetLocalSession(CreateAndAddSession(channel)); - Startup(); - _network.ClientSendMessage(new MsgPlayerListReq()); + if (old.HasValue) + { + Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); + EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); + LocalPlayerDetached?.Invoke(old.Value); } - public void SetLocalSession(ICommonSession? session) + if (uid == null) { - if (session == LocalSession) - return; + Sawmill.Info($"Local player is no longer attached to any entity."); + return true; + } - var old = LocalSession; + if (!EntManager.EntityExists(uid)) + { + Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); + return true; + } - if (old?.AttachedEntity is { } oldUid) - { - LocalSession = null; - LocalPlayer = null; - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(oldUid)}."); - EntManager.EventBus.RaiseLocalEvent(oldUid, new LocalPlayerDetachedEvent(oldUid), true); - LocalPlayerDetached?.Invoke(oldUid); - } + if (!EntManager.HasComponent(uid.Value)) + { + if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) + Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); + var eye = Factory.GetComponent(); + eye.NetSyncEnabled = false; + EntManager.AddComponent(uid.Value, eye); + } - LocalSession = session; - LocalPlayer = session == null ? null : new LocalPlayer(session); - Sawmill.Info($"Changing local session from {old?.ToString() ?? "null"} to {session?.ToString() ?? "null"}."); - LocalSessionChanged?.Invoke((old, LocalSession)); + Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); + EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); + LocalPlayerAttached?.Invoke(uid.Value); + return true; + } - if (session?.AttachedEntity is { } newUid) - { - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(newUid)}."); - EntManager.EventBus.RaiseLocalEvent(newUid, new LocalPlayerAttachedEvent(newUid), true); - LocalPlayerAttached?.Invoke(newUid); - } - } + public void ApplyPlayerStates(IReadOnlyCollection list) + { + var dirty = ApplyStates(list, true); - /// - public override void Shutdown() + if (_pendingStates.Count == 0) { - SetAttachedEntity(LocalSession, null, out _); - LocalPlayer = null; - LocalSession = null; + // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. + _pending.Clear(); + _pending.AddRange(_pendingStates.Values); _pendingStates.Clear(); - base.Shutdown(); - PlayerListUpdated?.Invoke(); + dirty |= ApplyStates(_pending, false); } - public override bool SetAttachedEntity(ICommonSession? session, EntityUid? uid, out ICommonSession? kicked, bool force = false) - { - kicked = null; - if (session == null) - return false; + if (dirty) + PlayerListUpdated?.Invoke(); + } - if (session.AttachedEntity == uid) - return true; + private bool ApplyStates(IReadOnlyCollection list, bool fullList) + { + if (list.Count == 0) + return false; - var old = session.AttachedEntity; - if (!base.SetAttachedEntity(session, uid, out kicked, force)) - return false; + DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. + , "Received player state without being connected?"); + DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - if (session != LocalSession) - return true; + var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - if (old.HasValue) + bool dirty = false; + if (state != null) + { + dirty = true; + if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) + && state.ControlledEntity is { Valid: true }) { - Sawmill.Info($"Detaching local player from {EntManager.ToPrettyString(old)}."); - EntManager.EventBus.RaiseLocalEvent(old.Value, new LocalPlayerDetachedEvent(old.Value), true); - LocalPlayerDetached?.Invoke(old.Value); + Sawmill.Error($"Received player state for local player with an unknown net entity!"); + _pendingStates[state.UserId] = state; } - - if (uid == null) + else { - Sawmill.Info($"Local player is no longer attached to any entity."); - return true; + _pendingStates.Remove(state.UserId); } - if (!EntManager.EntityExists(uid)) - { - Sawmill.Error($"Attempted to attach player to non-existent entity {uid}!"); - return true; - } + SetAttachedEntity(LocalSession, uid, out _, true); + SetStatus(LocalSession, state.Status); + } - if (!EntManager.HasComponent(uid.Value)) - { - if (_client.RunLevel != ClientRunLevel.SinglePlayerGame) - Sawmill.Warning($"Attaching local player to an entity {EntManager.ToPrettyString(uid)} without an eye. This eye will not be netsynced and may cause issues."); - var eye = Factory.GetComponent(); - eye.NetSyncEnabled = false; - EntManager.AddComponent(uid.Value, eye); - } + return UpdatePlayerList(list, fullList) || dirty; + } - Sawmill.Info($"Attaching local player to {EntManager.ToPrettyString(uid)}."); - EntManager.EventBus.RaiseLocalEvent(uid.Value, new LocalPlayerAttachedEvent(uid.Value), true); - LocalPlayerAttached?.Invoke(uid.Value); - return true; - } + /// + /// Handles the incoming PlayerList message from the server. + /// + private void HandlePlayerList(MsgPlayerList msg) + { + ApplyPlayerStates(msg.Plyrs); + } - public void ApplyPlayerStates(IReadOnlyCollection list) + /// + /// Compares the server player list to the client one, and updates if needed. + /// + private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + { + var dirty = false; + var users = new List(); + foreach (var state in remotePlayers) { - var dirty = ApplyStates(list, true); + users.Add(state.UserId); - if (_pendingStates.Count == 0) + if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) + && state.ControlledEntity is { Valid: true }) { - // This is somewhat inefficient as it might try to re-apply states that failed just a moment ago. - _pending.Clear(); - _pending.AddRange(_pendingStates.Values); - _pendingStates.Clear(); - dirty |= ApplyStates(_pending, false); + _pendingStates[state.UserId] = state; + } + else + { + _pendingStates.Remove(state.UserId); } - if (dirty) - PlayerListUpdated?.Invoke(); - } - - private bool ApplyStates(IReadOnlyCollection list, bool fullList) - { - if (list.Count == 0) - return false; - - DebugTools.Assert(_network.IsConnected || _client.RunLevel == ClientRunLevel.SinglePlayerGame // replays use state application. - , "Received player state without being connected?"); - DebugTools.Assert(LocalSession != null, "Received player state before Session finished setup."); - - var state = list.FirstOrDefault(s => s.UserId == LocalSession.UserId); - - bool dirty = false; - if (state != null) + if (!InternalSessions.TryGetValue(state.UserId, out var session)) { + // This is a new userid, so we create a new session. + DebugTools.Assert(state.UserId != LocalPlayer?.UserId); + var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); + SetStatus(newSession, state.Status); + SetAttachedEntity(newSession, controlled, out _, true); dirty = true; - if (!EntManager.TryGetEntity(state.ControlledEntity, out var uid) - && state.ControlledEntity is { Valid: true }) - { - Sawmill.Error($"Received player state for local player with an unknown net entity!"); - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - SetAttachedEntity(LocalSession, uid, out _, true); - SetStatus(LocalSession, state.Status); + continue; } - return UpdatePlayerList(list, fullList) || dirty; - } + // Check if the data is actually different + if (session.Name == state.Name + && session.Status == state.Status + && session.AttachedEntity == controlled) + { + continue; + } - /// - /// Handles the incoming PlayerList message from the server. - /// - private void HandlePlayerList(MsgPlayerList msg) - { - ApplyPlayerStates(msg.Plyrs); + dirty = true; + var local = (ICommonSessionInternal)session; + local.SetName(state.Name); + SetStatus(local, state.Status); + SetAttachedEntity(local, controlled, out _, true); } - /// - /// Compares the server player list to the client one, and updates if needed. - /// - private bool UpdatePlayerList(IEnumerable remotePlayers, bool fullList) + // Remove old users. This only works if the provided state is a list of all players + if (fullList) { - var dirty = false; - var users = new List(); - foreach (var state in remotePlayers) + foreach (var oldUser in InternalSessions.Keys.ToArray()) { - users.Add(state.UserId); - - if (!EntManager.TryGetEntity(state.ControlledEntity, out var controlled) - && state.ControlledEntity is { Valid: true }) - { - _pendingStates[state.UserId] = state; - } - else - { - _pendingStates.Remove(state.UserId); - } - - if (!InternalSessions.TryGetValue(state.UserId, out var session)) - { - // This is a new userid, so we create a new session. - DebugTools.Assert(state.UserId != LocalPlayer?.UserId); - var newSession = (ICommonSessionInternal)CreateAndAddSession(state.UserId, state.Name); - SetStatus(newSession, state.Status); - SetAttachedEntity(newSession, controlled, out _, true); - dirty = true; + if (users.Contains(oldUser)) continue; - } - // Check if the data is actually different - if (session.Name == state.Name - && session.Status == state.Status - && session.AttachedEntity == controlled) - { + if (InternalSessions[oldUser].ClientSide) continue; - } + DebugTools.Assert(oldUser != LocalUser + || LocalUser == null + || LocalUser == default(NetUserId), + "Client is still connected to the server but not in the list of players?"); + RemoveSession(oldUser); + _pendingStates.Remove(oldUser); dirty = true; - var local = (ICommonSessionInternal)session; - local.SetName(state.Name); - SetStatus(local, state.Status); - SetAttachedEntity(local, controlled, out _, true); } - - // Remove old users. This only works if the provided state is a list of all players - if (fullList) - { - foreach (var oldUser in InternalSessions.Keys.ToArray()) - { - if (users.Contains(oldUser)) - continue; - - if (InternalSessions[oldUser].ClientSide) - continue; - - DebugTools.Assert(oldUser != LocalUser - || LocalUser == null - || LocalUser == default(NetUserId), - "Client is still connected to the server but not in the list of players?"); - RemoveSession(oldUser); - _pendingStates.Remove(oldUser); - dirty = true; - } - } - - return dirty; } - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) - { - if (LocalEntity == uid) - { - session = LocalSession!; - return true; - } + return dirty; + } - session = null; - return false; + public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (LocalEntity == uid) + { + session = LocalSession!; + return true; } + + session = null; + return false; } } diff --git a/Robust.Server/Player/FilterSystem.cs b/Robust.Server/Player/FilterSystem.cs index f89ce972912..207e9f6abbe 100644 --- a/Robust.Server/Player/FilterSystem.cs +++ b/Robust.Server/Player/FilterSystem.cs @@ -1,20 +1,18 @@ -using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Player; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +internal sealed class FilterSystem : SharedFilterSystem { - internal sealed class FilterSystem : SharedFilterSystem + public override Filter FromEntities(Filter filter, params EntityUid[] entities) { - public override Filter FromEntities(Filter filter, params EntityUid[] entities) + foreach (var uid in entities) { - foreach (var uid in entities) - { - if (TryComp(uid, out ActorComponent? actor)) - filter.AddPlayer(actor.PlayerSession); - } - - return filter; + if (TryComp(uid, out ActorComponent? actor)) + filter.AddPlayer(actor.PlayerSession); } + + return filter; } } diff --git a/Robust.Server/Player/PlayerManager.cs b/Robust.Server/Player/PlayerManager.cs index b46f18a4bc7..07323cfc950 100644 --- a/Robust.Server/Player/PlayerManager.cs +++ b/Robust.Server/Player/PlayerManager.cs @@ -16,194 +16,192 @@ using Robust.Shared.Player; using Robust.Shared.Reflection; using Robust.Shared.Timing; -using Robust.Shared.Utility; -namespace Robust.Server.Player +namespace Robust.Server.Player; + +/// +/// This class will manage connected player sessions. +/// +internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager { - /// - /// This class will manage connected player sessions. - /// - internal sealed partial class PlayerManager : SharedPlayerManager, IPlayerManager - { - private static readonly Gauge PlayerCountMetric = Metrics - .CreateGauge("robust_player_count", "Number of players on the server."); + private static readonly Gauge PlayerCountMetric = Metrics + .CreateGauge("robust_player_count", "Number of players on the server."); - [Dependency] private IBaseServer _baseServer = default!; - [Dependency] private IGameTiming _timing = default!; - [Dependency] private IServerNetManager _network = default!; - [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IEntityManager _entityManager = default!; - [Dependency] private IServerNetConfigurationManager _cfg = default!; + [Dependency] private IBaseServer _baseServer = default!; + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IServerNetManager _network = default!; + [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IServerNetConfigurationManager _cfg = default!; - public BoundKeyMap KeyMap { get; private set; } = default!; + public BoundKeyMap KeyMap { get; private set; } = default!; - /// - public override void Initialize(int maxPlayers) - { - base.Initialize(maxPlayers); - KeyMap = new BoundKeyMap(_reflectionManager); - KeyMap.PopulateKeyFunctionsMap(); + /// + public override void Initialize(int maxPlayers) + { + base.Initialize(maxPlayers); + KeyMap = new BoundKeyMap(_reflectionManager); + KeyMap.PopulateKeyFunctionsMap(); - _network.RegisterNetMessage(HandlePlayerListReq); - _network.RegisterNetMessage(); - _network.RegisterNetMessage(); + _network.RegisterNetMessage(HandlePlayerListReq); + _network.RegisterNetMessage(); + _network.RegisterNetMessage(); - _network.Connecting += OnConnecting; - _network.Connected += NewSession; - _network.Disconnect += EndSession; - } + _network.Connecting += OnConnecting; + _network.Connected += NewSession; + _network.Disconnect += EndSession; + } - public override void Shutdown() - { - base.Shutdown(); - KeyMap = default!; + public override void Shutdown() + { + base.Shutdown(); + KeyMap = default!; - _network.Connecting -= OnConnecting; - _network.Connected -= NewSession; - _network.Disconnect -= EndSession; - } + _network.Connecting -= OnConnecting; + _network.Connected -= NewSession; + _network.Disconnect -= EndSession; + } - private Task OnConnecting(NetConnectingArgs args) + private Task OnConnecting(NetConnectingArgs args) + { + if (PlayerCount >= _baseServer.MaxPlayers) { - if (PlayerCount >= _baseServer.MaxPlayers) - { - args.Deny("The server is full."); - } - - return Task.CompletedTask; + args.Deny("The server is full."); } - /// - /// Creates a new session for a client. - /// - /// - /// - private void NewSession(object? sender, NetChannelArgs args) - { - CreateAndAddSession(args.Channel); - PlayerCountMetric.Set(PlayerCount); - // Synchronize base time. - var msgTimeBase = new MsgSyncTimeBase(); - (msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase; - _network.ServerSendMessage(msgTimeBase, args.Channel); - - _cfg.SyncConnectingClient(args.Channel); - } + return Task.CompletedTask; + } - private void EndSession(object? sender, NetChannelArgs args) - { - EndSession(args.Channel.UserId); - } + /// + /// Creates a new session for a client. + /// + /// + /// + private void NewSession(object? sender, NetChannelArgs args) + { + CreateAndAddSession(args.Channel); + PlayerCountMetric.Set(PlayerCount); + // Synchronize base time. + var msgTimeBase = new MsgSyncTimeBase(); + (msgTimeBase.Time, msgTimeBase.Tick) = _timing.TimeBase; + _network.ServerSendMessage(msgTimeBase, args.Channel); + + _cfg.SyncConnectingClient(args.Channel); + } - /// - /// Ends a clients session, and disconnects them. - /// - internal void EndSession(NetUserId user) - { - if (!TryGetSessionById(user, out var session)) - return; + private void EndSession(object? sender, NetChannelArgs args) + { + EndSession(args.Channel.UserId); + } - RemoveSession(session.UserId); - SetStatus(session, SessionStatus.Disconnected); - SetAttachedEntity(session, null, out _, true); + /// + /// Ends a clients session, and disconnects them. + /// + internal void EndSession(NetUserId user) + { + if (!TryGetSessionById(user, out var session)) + return; - var viewSys = EntManager.System(); - foreach (var eye in session.ViewSubscriptions.ToArray()) - { - viewSys.RemoveViewSubscriber(eye, session); - } + RemoveSession(session.UserId); + SetStatus(session, SessionStatus.Disconnected); + SetAttachedEntity(session, null, out _, true); - PlayerCountMetric.Set(PlayerCount); - Dirty(); + var viewSys = EntManager.System(); + foreach (var eye in session.ViewSubscriptions.ToArray()) + { + viewSys.RemoveViewSubscriber(eye, session); } - private void HandlePlayerListReq(MsgPlayerListReq message) - { - var channel = message.MsgChannel; - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialPlayerListReqDone = true; + PlayerCountMetric.Set(PlayerCount); + Dirty(); + } - if (!session.InitialResourcesDone) - return; + private void HandlePlayerListReq(MsgPlayerListReq message) + { + var channel = message.MsgChannel; + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialPlayerListReqDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialResourcesDone) + return; - public void MarkPlayerResourcesSent(INetChannel channel) - { - var session = (CommonSession)GetSessionByChannel(channel); - session.InitialResourcesDone = true; + SendPlayerList(channel, session); + } - if (!session.InitialPlayerListReqDone) - return; + public void MarkPlayerResourcesSent(INetChannel channel) + { + var session = (CommonSession)GetSessionByChannel(channel); + session.InitialResourcesDone = true; - SendPlayerList(channel, session); - } + if (!session.InitialPlayerListReqDone) + return; - private void SendPlayerList(INetChannel channel, CommonSession session) - { - var players = Sessions; - var netMsg = new MsgPlayerList(); + SendPlayerList(channel, session); + } - // client session is complete, set their status accordingly. - // This is done before the packet is built, so that the client - // can see themselves Connected. - session.ConnectedTime = DateTime.UtcNow; - SetStatus(session, SessionStatus.Connected); + private void SendPlayerList(INetChannel channel, CommonSession session) + { + var players = Sessions; + var netMsg = new MsgPlayerList(); - var list = new List(); - foreach (var client in players) - { - var info = new SessionState - { - UserId = client.UserId, - Name = client.Name, - Status = client.Status - }; - list.Add(info); - } - netMsg.Plyrs = list; - - channel.SendMessage(netMsg); - } + // client session is complete, set their status accordingly. + // This is done before the packet is built, so that the client + // can see themselves Connected. + session.ConnectedTime = DateTime.UtcNow; + SetStatus(session, SessionStatus.Connected); - public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + var list = new List(); + foreach (var client in players) { - if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) + var info = new SessionState { - session = null; - return false; - } - - session = actor.PlayerSession; - return true; + UserId = client.UserId, + Name = client.Name, + Status = client.Status + }; + list.Add(info); } + netMsg.Plyrs = list; - internal ICommonSession AddDummySession(NetUserId user, string name) + channel.SendMessage(netMsg); + } + + public override bool TryGetSessionByEntity(EntityUid uid, [NotNullWhen(true)] out ICommonSession? session) + { + if (!_entityManager.TryGetComponent(uid, out ActorComponent? actor)) { + session = null; + return false; + } + + session = actor.PlayerSession; + return true; + } + + internal ICommonSession AddDummySession(NetUserId user, string name) + { #if FULL_RELEASE // Lets not make it completely trivial to fake player counts. throw new NotSupportedException(); #endif - Lock.EnterWriteLock(); - DummySession session; - try - { - UserIdMap[name] = user; - if (!PlayerData.TryGetValue(user, out var data)) - PlayerData[user] = data = new(user, name); - - session = new DummySession(user, name, data); - InternalSessions.Add(user, session); - } - finally - { - Lock.ExitWriteLock(); - } - - UpdateState(session); + Lock.EnterWriteLock(); + DummySession session; + try + { + UserIdMap[name] = user; + if (!PlayerData.TryGetValue(user, out var data)) + PlayerData[user] = data = new(user, name); - return session; + session = new DummySession(user, name, data); + InternalSessions.Add(user, session); } + finally + { + Lock.ExitWriteLock(); + } + + UpdateState(session); + + return session; } } From 2cbcb92cb813a23756f6fc2eedd9000d7a66f530 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 28 Jun 2026 23:39:55 +1000 Subject: [PATCH 090/178] Add DictionaryEquals helper (#6682) * Add DictionaryEquals helper * test because why not --- RELEASE-NOTES.md | 2 +- .../Utility/CollectionExtensions_Test.cs | 42 +++++++++++++++++++ Robust.Shared/Utility/CollectionExtensions.cs | 33 +++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 677400e8b93..f919c42f1b9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Added a DictionaryEquals extension method to check equality between two dictionaries. ### Bugfixes diff --git a/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs index eeabd0c76f1..a135d941448 100644 --- a/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs +++ b/Robust.Shared.Tests/Utility/CollectionExtensions_Test.cs @@ -30,5 +30,47 @@ public void TestFirstOrNull() Assert.That(new[] {1}.FirstOrNull(p => p == 2), Is.Null); Assert.That(new[] {1, 2, 3}.FirstOrNull(p => p == 2), Is.EqualTo(2)); } + + [Test] + public void DictionaryEqualsTest() + { + var dict = new Dictionary + { + ["one"] = 1, + ["two"] = 2, + }; + + var same = new Dictionary + { + ["two"] = 2, + ["one"] = 1, + }; + + var differentCount = new Dictionary + { + ["one"] = 1, + }; + + var differentKey = new Dictionary + { + ["one"] = 1, + ["three"] = 2, + }; + + var differentValue = new Dictionary + { + ["one"] = 1, + ["two"] = 3, + }; + + Assert.Multiple(() => + { + Assert.That(dict.DictionaryEquals(dict), Is.True); + Assert.That(dict.DictionaryEquals(same), Is.True); + Assert.That(dict.DictionaryEquals(differentCount), Is.False); + Assert.That(dict.DictionaryEquals(differentKey), Is.False); + Assert.That(dict.DictionaryEquals(differentValue), Is.False); + }); + } } } diff --git a/Robust.Shared/Utility/CollectionExtensions.cs b/Robust.Shared/Utility/CollectionExtensions.cs index 12bbd677294..b53112ea183 100644 --- a/Robust.Shared/Utility/CollectionExtensions.cs +++ b/Robust.Shared/Utility/CollectionExtensions.cs @@ -56,6 +56,39 @@ public static Dictionary ShallowClone(this Dictionar return dict; } + /// + /// Compares the entries inside of 2 dictionaries to check equality. + /// + /// + /// The base Equals implementation checks references hence this. + /// + /// + /// + /// + /// + /// + public static bool DictionaryEquals( + this IReadOnlyDictionary self, + IReadOnlyDictionary other) + where TKey : notnull + { + if (self.Count != other.Count) + return false; + + // Checking itself. + if (self.Equals(other)) + return true; + + var valueComparer = EqualityComparer.Default; + foreach (var (key, value) in self) + { + if (!other.TryGetValue(key, out var otherValue) || !valueComparer.Equals(value, otherValue)) + return false; + } + + return true; + } + public static bool TryGetValue(this IList list, int index, out T value) { if (index >= 0 && list.Count > index) From 8da4630d7ce06737d3c5e266986aa8ceedb88964 Mon Sep 17 00:00:00 2001 From: JohnKieser Date: Mon, 29 Jun 2026 04:43:58 -0400 Subject: [PATCH 091/178] SharedPhysicsSystem.Fixture typo fix one line (#6687) * Typo fix * typo fix --- Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs index d7243920e35..b3267cbb24c 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Fixtures.cs @@ -168,7 +168,7 @@ public bool IsHardCollidable(Entity bodyA public bool IsHardCollidable(FixturesComponent fixturesA, FixturesComponent fixturesB) { var (aLayer, aMask) = GetHardCollision(fixturesA); - var (bLayer, bMask) = GetHardCollision(fixturesA); + var (bLayer, bMask) = GetHardCollision(fixturesB); return ((aLayer & bMask) | (bLayer & aMask)) != 0; } From e128a7f6950113df3c5c4140fd256004aa9687f3 Mon Sep 17 00:00:00 2001 From: Jessica M Date: Mon, 29 Jun 2026 08:49:43 +0000 Subject: [PATCH 092/178] Partially Revert "More Cursor Usage in Controls" (#6654) revert button and scrollbar changes --- Robust.Client/UserInterface/Controls/BaseButton.cs | 2 -- Robust.Client/UserInterface/Controls/ScrollBar.cs | 9 --------- 2 files changed, 11 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/BaseButton.cs b/Robust.Client/UserInterface/Controls/BaseButton.cs index e8a6856166f..d590b9619c7 100644 --- a/Robust.Client/UserInterface/Controls/BaseButton.cs +++ b/Robust.Client/UserInterface/Controls/BaseButton.cs @@ -90,7 +90,6 @@ public bool Disabled if (old != value) { - DefaultCursorShape = Disabled ? CursorShape.NotAllowed : CursorShape.Pointer; DrawModeChanged(); } } @@ -235,7 +234,6 @@ public bool MuteSounds protected BaseButton() { MouseFilter = MouseFilterMode.Stop; - DefaultCursorShape = Disabled ? CursorShape.NotAllowed : CursorShape.Pointer; } protected virtual void DrawModeChanged() diff --git a/Robust.Client/UserInterface/Controls/ScrollBar.cs b/Robust.Client/UserInterface/Controls/ScrollBar.cs index 0aa25ed45a6..7d5d1b2efd0 100644 --- a/Robust.Client/UserInterface/Controls/ScrollBar.cs +++ b/Robust.Client/UserInterface/Controls/ScrollBar.cs @@ -46,7 +46,6 @@ protected ScrollBar(OrientationMode orientation) ReservesSpace = true; _orientation = orientation; - DefaultCursorShape = CursorShape.Pointer; } public bool IsAtEnd @@ -132,19 +131,11 @@ protected internal override void KeyBindUp(GUIBoundKeyEventArgs args) protected internal override void MouseMove(GUIMouseMoveEventArgs args) { - DefaultCursorShape = CursorShape.Arrow; - - if (_isHovered || _grabData != null) - { - DefaultCursorShape = CursorShape.Pointer; - } - if (_grabData == null) { var box = _getGrabberBox(); _isHovered = box.Contains(args.RelativePixelPosition); _updatePseudoClass(); - return; } From 39306dfd294b54a2d95eb0815c770472158a62ed Mon Sep 17 00:00:00 2001 From: Perry Fraser Date: Mon, 29 Jun 2026 05:29:42 -0400 Subject: [PATCH 093/178] feat: raise event on audio despawns (#6056) Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- RELEASE-NOTES.md | 1 + Robust.Shared/Audio/Components/AudioComponent.cs | 6 ++++++ Robust.Shared/Audio/Systems/SharedAudioSystem.cs | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f919c42f1b9..3c21a3b95ab 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,6 +39,7 @@ END TEMPLATE--> ### New features +* Adds `AttachedAudioDespawnedEvent`, which is raised against the parent of a despawning `AudioComponent`. * Added a DictionaryEquals extension method to check equality between two dictionaries. ### Bugfixes diff --git a/Robust.Shared/Audio/Components/AudioComponent.cs b/Robust.Shared/Audio/Components/AudioComponent.cs index 2f8275694cb..d8652146634 100644 --- a/Robust.Shared/Audio/Components/AudioComponent.cs +++ b/Robust.Shared/Audio/Components/AudioComponent.cs @@ -262,6 +262,12 @@ public void Dispose() } } +/// +/// An event raised against an AudioComponent's parent when it is despawned. +/// +[ByRefEvent] +public readonly record struct AttachedAudioDespawnedEvent; + [Serializable, NetSerializable] public enum AudioState : byte { diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 4a22cebb0ed..651a88e45c4 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -58,6 +58,7 @@ public override void Initialize() Subs.CVar(CfgManager, CVars.AudioZOffset, SetZOffset); SubscribeLocalEvent(OnAudioGetStateAttempt); SubscribeLocalEvent(OnAudioUnpaused); + SubscribeLocalEvent(OnTimedDespawn); } /// @@ -264,6 +265,16 @@ private void OnAudioGetStateAttempt(EntityUid uid, AudioComponent component, ref } } + private void OnTimedDespawn(Entity ent, ref TimedDespawnEvent args) + { + var parent = Transform(ent).ParentUid; + if (!Exists(parent)) + return; + + var ev = new AttachedAudioDespawnedEvent(); + RaiseLocalEvent(parent, ref ev); + } + /// /// Considers Z-offset for audio and gets the adjusted distance. /// From d9330ecda37ada2b64946325fbebc1e3743b22e3 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:26:54 +1000 Subject: [PATCH 094/178] Add test timeouts (#6695) --- .github/workflows/build-test.yml | 1 + .github/workflows/test-content.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 87cc7573d36..7248d80cfd9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -13,6 +13,7 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - uses: actions/checkout@v4.2.2 diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index f8014cc945a..7a43626d327 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -9,6 +9,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Check out content From dfe1d4ef8a518db75766c2eb197332e9971072bc Mon Sep 17 00:00:00 2001 From: Whatstone <166147148+whatston3@users.noreply.github.com> Date: Tue, 30 Jun 2026 03:12:55 -0400 Subject: [PATCH 095/178] Cleanup: ProtoMan Part 3 - remove redundant EntitySystem IPrototypeManager, IComponentFactory instances (#6693) --- .../EntitySystems/AnimationPlayerSystem.cs | 7 +------ .../EntitySystems/SpriteSystem.Helpers.cs | 6 +++--- .../GameObjects/EntitySystems/SpriteSystem.cs | 7 +------ .../EntitySystems/UserInterfaceSystem.cs | 4 ++-- Robust.Client/Placement/PlacementManager.cs | 3 ++- .../GameObjects/Systems/MetaDataSystem.cs | 5 +++-- .../GameObjects/Systems/PrototypeReloadSystem.cs | 14 ++++++-------- .../Systems/SharedUserInterfaceSystem.cs | 2 -- 8 files changed, 18 insertions(+), 30 deletions(-) diff --git a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs index 5bb934f2a88..14d15a4abe5 100644 --- a/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/AnimationPlayerSystem.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Robust.Client.Animations; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Utility; namespace Robust.Client.GameObjects @@ -14,10 +13,6 @@ public sealed partial class AnimationPlayerSystem : EntitySystem private EntityQuery _playerQuery; private EntityQuery _metaQuery; -#if DEBUG - [Dependency] private IComponentFactory _compFact = default!; -#endif - public override void Initialize() { base.Initialize(); @@ -129,7 +124,7 @@ public void Play(Entity ent, Animation animation, stri if (IsClientSide(ent) || !animatedComp.NetSyncEnabled) continue; - var reg = _compFact.GetRegistration(animatedComp); + var reg = Factory.GetRegistration(animatedComp); // In principle there is nothing wrong with this, as long as the property of the component being // animated is not part of the networked state and setting it does not dirty the component. Hence only a diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs index dec67c6cec2..c544b9b3dc8 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Helpers.cs @@ -56,7 +56,7 @@ public Texture GetIcon(IconComponent icon) /// public IRsiStateLike GetPrototypeIcon(string prototype) { - if (!_proto.TryIndex(prototype, out var entityPrototype)) + if (!ProtoMan.TryIndex(prototype, out var entityPrototype)) { // The specified prototype doesn't exist, return the fallback "error" sprite. _sawmill.Error("Failed to load PrototypeIcon {0}", prototype); @@ -82,7 +82,7 @@ public IRsiStateLike GetPrototypeIcon(EntityPrototype prototype) private IRsiStateLike GetPrototypeIconInternal(EntityPrototype prototype) { // IconComponent takes precedence. If it has a valid icon, return that. Otherwise, continue as normal. - if (prototype.TryGetComponent(out IconComponent? icon, _factory)) + if (prototype.TryComp(out IconComponent? icon, Factory)) return GetIcon(icon); // If the prototype doesn't have a SpriteComponent, then there's nothing we can do but return the fallback. @@ -108,7 +108,7 @@ public IEnumerable GetPrototypeTextures(EntityProto var results = new List(); noRot = false; - if (proto.TryGetComponent(out IconComponent? icon, _factory)) + if (proto.TryComp(out IconComponent? icon, Factory)) { results.Add(GetIcon(icon)); return results; diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index 0780a670f15..e481a64f398 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; using JetBrains.Annotations; using Robust.Client.ComponentTrees; using Robust.Client.Graphics; @@ -13,7 +12,6 @@ using Robust.Shared.Graphics.RSI; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Timing; @@ -31,10 +29,7 @@ public sealed partial class SpriteSystem : EntitySystem [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] private IPrototypeManager _proto = default!; [Dependency] private IResourceCache _resourceCache = default!; - [Dependency] private ILogManager _logManager = default!; - [Dependency] private IComponentFactory _factory = default!; // Note that any new system dependencies have to be added to RobustUnitTest.BaseSetup() [Dependency] private SharedTransformSystem _xforms = default!; @@ -64,7 +59,7 @@ public override void Initialize() SubscribeLocalEvent(OnInit); Subs.CVar(_cfg, CVars.RenderSpriteDirectionBias, OnBiasChanged, true); - _sawmill = _logManager.GetSawmill("sprite"); + _sawmill = LogManager.GetSawmill("sprite"); _query = GetEntityQuery(); } diff --git a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs index 289164db9ad..924a4085f7b 100644 --- a/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/UserInterfaceSystem.cs @@ -16,13 +16,13 @@ public sealed class UserInterfaceSystem : SharedUserInterfaceSystem public override void Initialize() { base.Initialize(); - ProtoManager.PrototypesReloaded += OnProtoReload; + ProtoMan.PrototypesReloaded += OnProtoReload; } public override void Shutdown() { base.Shutdown(); - ProtoManager.PrototypesReloaded -= OnProtoReload; + ProtoMan.PrototypesReloaded -= OnProtoReload; } /// diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index eda141df12e..f2828a08ee7 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -40,6 +40,7 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I [Dependency] private IEntitySystemManager _entitySystemManager = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IComponentFactory _factory = default!; [Dependency] private IBaseClient _baseClient = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] internal IClyde Clyde = default!; @@ -787,7 +788,7 @@ public void PreparePlacementTexList(List? texs, boo sc.Comp.NoRotation = noRot; - if (prototype != null && prototype.TryGetComponent("Sprite", out var spriteComp)) + if (prototype != null && prototype.TryComp(out var spriteComp, _factory)) { Sprite.SetScale(sc.AsNullable(), spriteComp.Scale); } diff --git a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs index cd02f4c1a94..dff87b4930e 100644 --- a/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs +++ b/Robust.Shared/GameObjects/Systems/MetaDataSystem.cs @@ -10,7 +10,6 @@ namespace Robust.Shared.GameObjects; public abstract partial class MetaDataSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; - [Dependency] private IPrototypeManager _proto = default!; private EntityPausedEvent _pausedEvent; @@ -18,6 +17,8 @@ public abstract partial class MetaDataSystem : EntitySystem public override void Initialize() { + base.Initialize(); + _metaQuery = GetEntityQuery(); SubscribeLocalEvent(OnMetaDataHandle); SubscribeLocalEvent(OnMetaDataGetState); @@ -37,7 +38,7 @@ private void OnMetaDataHandle(EntityUid uid, MetaDataComponent component, ref Co component._entityDescription = state.Description; if(state.PrototypeId != null && state.PrototypeId != component._entityPrototype?.ID) - component._entityPrototype = _proto.Index(state.PrototypeId); + component._entityPrototype = ProtoMan.Index(state.PrototypeId); component.PauseTime = state.PauseTime; } diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index e514d97e8e9..4463d86db51 100644 --- a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs +++ b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Robust.Shared.IoC; using Robust.Shared.Prototypes; namespace Robust.Shared.GameObjects; @@ -11,11 +10,10 @@ namespace Robust.Shared.GameObjects; /// internal sealed partial class PrototypeReloadSystem : EntitySystem { - [Dependency] private IPrototypeManager _prototypes = default!; - [Dependency] private IComponentFactory _componentFactory = default!; - public override void Initialize() { + base.Initialize(); + SubscribeLocalEvent(OnPrototypesReloaded); } @@ -31,7 +29,7 @@ private void OnPrototypesReloaded(PrototypesReloadedEventArgs eventArgs) if (id == null || !set.Modified.ContainsKey(id)) continue; - var proto = _prototypes.Index(id); + var proto = ProtoMan.Index(id); UpdateEntity(uid, metadata, proto); } } @@ -42,12 +40,12 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr var oldPrototypeComponents = oldPrototype?.Components.Keys .Where(n => n != "Transform" && n != "MetaData") - .Select(name => (name, _componentFactory.GetRegistration(name).Type)) + .Select(name => (name, Factory.GetRegistration(name).Type)) .ToList() ?? new List<(string name, Type Type)>(); var newPrototypeComponents = newPrototype.Components.Keys .Where(n => n != "Transform" && n != "MetaData") - .Select(name => (name, _componentFactory.GetRegistration(name).Type)) + .Select(name => (name, Factory.GetRegistration(name).Type)) .ToList(); var ignoredComponents = new List(); @@ -71,7 +69,7 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr .Except(oldPrototypeComponents)) { var data = newPrototype.Components[name]; - var component = _componentFactory.GetComponent(name); + var component = Factory.GetComponent(name); if (!HasComp(entity, component.GetType())) AddComp(entity, component); diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 5b07899cced..1d43809f1d3 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -10,7 +10,6 @@ using Robust.Shared.IoC; using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Threading; using Robust.Shared.Timing; @@ -24,7 +23,6 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private IParallelManager _parallel = default!; - [Dependency] protected IPrototypeManager ProtoManager = default!; [Dependency] private IReflectionManager _reflection = default!; [Dependency] protected ISharedPlayerManager Player = default!; [Dependency] private SharedTransformSystem _transforms = default!; From 58fd434a51db9df7f9e3af46a0a027f67c841ca6 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:01:41 +1000 Subject: [PATCH 096/178] Remove QuadTree (#6664) --- RELEASE-NOTES.md | 1 + Robust.Shared/Utility/QuadTree.cs | 529 ------------------------------ 2 files changed, 1 insertion(+), 529 deletions(-) delete mode 100644 Robust.Shared/Utility/QuadTree.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3c21a3b95ab..ad428787253 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -36,6 +36,7 @@ END TEMPLATE--> ### Breaking changes * Validate Box2i inputs to ensure no negative-sized boxes. +* Removed QuadTree due to lack of maintenance, test coverage, and usage. ### New features diff --git a/Robust.Shared/Utility/QuadTree.cs b/Robust.Shared/Utility/QuadTree.cs deleted file mode 100644 index 1c7737d2213..00000000000 --- a/Robust.Shared/Utility/QuadTree.cs +++ /dev/null @@ -1,529 +0,0 @@ -// From http://csharpquadtree.codeplex.com/ - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Numerics; -using Robust.Shared.Maths; - -#nullable disable - -namespace Robust.Shared.Utility -{ - public sealed class QuadTree where T : class, IQuadObject - { - private readonly bool sort; - private readonly Vector2 minLeafSizeF; - private readonly int maxObjectsPerLeaf; - private QuadNode root = null; - private Dictionary objectToNodeLookup = new(); - private Dictionary objectSortOrder = new(); - public QuadNode Root { get { return root; } } - private object syncLock = new(); - private int objectSortId = 0; - - public QuadTree(Vector2 minLeafSizeF, int maxObjectsPerLeaf) - { - this.minLeafSizeF = minLeafSizeF; - this.maxObjectsPerLeaf = maxObjectsPerLeaf; - } - - public int GetSortOrder(T quadObject) - { - lock (objectSortOrder) - { - if (!objectSortOrder.ContainsKey(quadObject)) - return -1; - else - { - return objectSortOrder[quadObject]; - } - } - } - - /// - /// - /// - /// The smallest SizeF a leaf will split into - /// Maximum number of objects per leaf before it forces a split into sub quadrants - /// Whether or not queries will return objects in the order in which they were added - public QuadTree(Vector2 minLeafSizeF, int maxObjectsPerLeaf, bool sort) - : this(minLeafSizeF, maxObjectsPerLeaf) - { - this.sort = sort; - } - - public void Insert(T quadObject) - { - lock (syncLock) - { - if (sort && !objectSortOrder.ContainsKey(quadObject)) - { - objectSortOrder.Add(quadObject, objectSortId++); - } - - var bounds = quadObject.Bounds; - if (root == null) - { - var rootSizeF = new Vector2((float)Math.Ceiling(bounds.Width / minLeafSizeF.X), - (float)Math.Ceiling(bounds.Height / minLeafSizeF.Y)); - double multiplier = Math.Max(rootSizeF.X, rootSizeF.Y); - rootSizeF = new Vector2((float)(minLeafSizeF.X * multiplier), (float)(minLeafSizeF.Y * multiplier)); - var center = new Vector2i((int)(bounds.Left + bounds.Width / 2), (int)(bounds.Top + bounds.Height / 2)); - var rootOrigin = new Vector2i((int)(center.X - rootSizeF.X / 2), (int)(center.Y - rootSizeF.Y / 2)); - root = new QuadNode(Box2.FromDimensions(rootOrigin, rootSizeF)); - } - - while (!root.Bounds.Encloses(bounds)) - { - ExpandRoot(bounds); - } - - InsertNodeObject(root, quadObject); - } - } - - public List Query(Box2 bounds) - { - lock (syncLock) - { - List results = new List(); - if (root != null) - Query(bounds, root, results); - if (sort) - results.Sort((a, b) => { return objectSortOrder[a].CompareTo(objectSortOrder[b]); }); - return results; - } - } - - private void Query(Box2 bounds, QuadNode node, List results) - { - lock (syncLock) - { - if (node == null) return; - - if (bounds.Intersects(node.Bounds)) - { - foreach (T quadObject in node.Objects) - { - if (bounds.Intersects(quadObject.Bounds)) - results.Add(quadObject); - } - - foreach (QuadNode childNode in node.Nodes) - { - Query(bounds, childNode, results); - } - } - } - } - - private void ExpandRoot(Box2 newChildBounds) - { - lock (syncLock) - { - bool isNorth = root.Bounds.Top < newChildBounds.Top; - bool isWest = root.Bounds.Left < newChildBounds.Left; - - DiRectangleFion rootDiRectangleFion; - if (isNorth) - { - rootDiRectangleFion = isWest ? DiRectangleFion.NW : DiRectangleFion.NE; - } - else - { - rootDiRectangleFion = isWest ? DiRectangleFion.SW : DiRectangleFion.SE; - } - - double newX = (rootDiRectangleFion == DiRectangleFion.NW || rootDiRectangleFion == DiRectangleFion.SW) - ? root.Bounds.Left - : root.Bounds.Left - root.Bounds.Width; - double newY = (rootDiRectangleFion == DiRectangleFion.NW || rootDiRectangleFion == DiRectangleFion.NE) - ? root.Bounds.Top - : root.Bounds.Top - root.Bounds.Height; - var newRootBounds = Box2.FromDimensions((float)newX, (float)newY, root.Bounds.Width * 2f, root.Bounds.Height * 2f); - QuadNode newRoot = new QuadNode(newRootBounds); - SetupChildNodes(newRoot); - newRoot[rootDiRectangleFion] = root; - root = newRoot; - } - } - - private void InsertNodeObject(QuadNode node, T quadObject) - { - lock (syncLock) - { - if (!node.Bounds.Encloses(quadObject.Bounds)) - throw new Exception("This should not happen, child does not fit within node bounds"); - - if (!node.HasChildNodes() && node.Objects.Count + 1 > maxObjectsPerLeaf) - { - SetupChildNodes(node); - - List childObjects = new List(node.Objects); - List childrenToRelocate = new List(); - - foreach (T childObject in childObjects) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode == null) - continue; - - if (childNode.Bounds.Encloses(childObject.Bounds)) - { - childrenToRelocate.Add(childObject); - } - } - } - - foreach (T childObject in childrenToRelocate) - { - RemoveQuadObjectFromNode(childObject); - InsertNodeObject(node, childObject); - } - } - - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - if (childNode.Bounds.Encloses(quadObject.Bounds)) - { - InsertNodeObject(childNode, quadObject); - return; - } - } - } - - AddQuadObjectToNode(node, quadObject); - } - } - - private void ClearQuadObjectsFromNode(QuadNode node) - { - lock (syncLock) - { - List quadObjects = new List(node.Objects); - foreach (T quadObject in quadObjects) - { - RemoveQuadObjectFromNode(quadObject); - } - } - } - - private void RemoveQuadObjectFromNode(T quadObject) - { - lock (syncLock) - { - QuadNode node = objectToNodeLookup[quadObject]; - node.quadObjects.Remove(quadObject); - objectToNodeLookup.Remove(quadObject); - } - } - - private void AddQuadObjectToNode(QuadNode node, T quadObject) - { - lock (syncLock) - { - node.quadObjects.Add(quadObject); - objectToNodeLookup.Add(quadObject, node); - } - } - - private void SetupChildNodes(QuadNode node) - { - lock (syncLock) - { - if (minLeafSizeF.X <= node.Bounds.Width / 2 && minLeafSizeF.Y <= node.Bounds.Height / 2) - { - node[DiRectangleFion.NW] = new QuadNode(node.Bounds.Left, node.Bounds.Top, node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.NE] = new QuadNode(node.Bounds.Left + node.Bounds.Width / 2, node.Bounds.Top, - node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.SW] = new QuadNode(node.Bounds.Left, node.Bounds.Top + node.Bounds.Height / 2, - node.Bounds.Width / 2, - node.Bounds.Height / 2); - node[DiRectangleFion.SE] = new QuadNode(node.Bounds.Left + node.Bounds.Width / 2, - node.Bounds.Top + node.Bounds.Height / 2, - node.Bounds.Width / 2, node.Bounds.Height / 2); - - } - } - } - - public void Remove(T quadObject) - { - lock (syncLock) - { - if (sort && objectSortOrder.ContainsKey(quadObject)) - { - objectSortOrder.Remove(quadObject); - } - - if (!objectToNodeLookup.ContainsKey(quadObject)) - throw new KeyNotFoundException("QuadObject not found in dictionary for removal"); - - QuadNode containingNode = objectToNodeLookup[quadObject]; - RemoveQuadObjectFromNode(quadObject); - - if (containingNode.Parent != null) - CheckChildNodes(containingNode.Parent); - } - } - - - - private void CheckChildNodes(QuadNode node) - { - lock (syncLock) - { - if (GetQuadObjectCount(node) <= maxObjectsPerLeaf) - { - // Move child objects into this node, and delete sub nodes - List subChildObjects = GetChildObjects(node); - foreach (T childObject in subChildObjects) - { - if (!node.Objects.Contains(childObject)) - { - RemoveQuadObjectFromNode(childObject); - AddQuadObjectToNode(node, childObject); - } - } - if (node[DiRectangleFion.NW] != null) - { - node[DiRectangleFion.NW].Parent = null; - node[DiRectangleFion.NW] = null; - } - if (node[DiRectangleFion.NE] != null) - { - node[DiRectangleFion.NE].Parent = null; - node[DiRectangleFion.NE] = null; - } - if (node[DiRectangleFion.SW] != null) - { - node[DiRectangleFion.SW].Parent = null; - node[DiRectangleFion.SW] = null; - } - if (node[DiRectangleFion.SE] != null) - { - node[DiRectangleFion.SE].Parent = null; - node[DiRectangleFion.SE] = null; - } - - if (node.Parent != null) - CheckChildNodes(node.Parent); - else - { - // Its the root node, see if we're down to one quadrant, with none in local storage - if so, ditch the other three - int numQuadrantsWithObjects = 0; - QuadNode nodeWithObjects = null; - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null && GetQuadObjectCount(childNode) > 0) - { - numQuadrantsWithObjects++; - nodeWithObjects = childNode; - if (numQuadrantsWithObjects > 1) break; - } - } - if (numQuadrantsWithObjects == 1) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != nodeWithObjects) - childNode.Parent = null; - } - root = nodeWithObjects; - } - } - } - } - } - - - private List GetChildObjects(QuadNode node) - { - lock (syncLock) - { - List results = new List(); - results.AddRange(node.quadObjects); - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - results.AddRange(GetChildObjects(childNode)); - } - return results; - } - } - - public int GetQuadObjectCount() - { - lock (syncLock) - { - if (root == null) - return 0; - int count = GetQuadObjectCount(root); - return count; - } - } - - private int GetQuadObjectCount(QuadNode node) - { - lock (syncLock) - { - int count = node.Objects.Count; - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - count += GetQuadObjectCount(childNode); - } - } - return count; - } - } - - public int GetQuadNodeCount() - { - lock (syncLock) - { - if (root == null) - return 0; - int count = GetQuadNodeCount(root, 1); - return count; - } - } - - private int GetQuadNodeCount(QuadNode node, int count) - { - lock (syncLock) - { - if (node == null) return count; - - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - count++; - } - return count; - } - } - - public List GetAllNodes() - { - lock (syncLock) - { - List results = new List(); - if (root != null) - { - results.Add(root); - GetChildNodes(root, results); - } - return results; - } - } - - private void GetChildNodes(QuadNode node, ICollection results) - { - lock (syncLock) - { - foreach (QuadNode childNode in node.Nodes) - { - if (childNode != null) - { - results.Add(childNode); - GetChildNodes(childNode, results); - } - } - } - } - - public sealed class QuadNode - { - private static int _id = 0; - public readonly int ID = _id++; - - public QuadNode Parent { get; internal set; } - - private QuadNode[] _nodes = new QuadNode[4]; - public QuadNode this[DiRectangleFion diRectangleFion] - { - get - { - switch (diRectangleFion) - { - case DiRectangleFion.NW: - return _nodes[0]; - case DiRectangleFion.NE: - return _nodes[1]; - case DiRectangleFion.SW: - return _nodes[2]; - case DiRectangleFion.SE: - return _nodes[3]; - default: - return null; - } - } - set - { - switch (diRectangleFion) - { - case DiRectangleFion.NW: - _nodes[0] = value; - break; - case DiRectangleFion.NE: - _nodes[1] = value; - break; - case DiRectangleFion.SW: - _nodes[2] = value; - break; - case DiRectangleFion.SE: - _nodes[3] = value; - break; - } - if (value != null) - value.Parent = this; - } - } - - public ReadOnlyCollection Nodes { get; set; } - internal List quadObjects = new(); - public Box2 Bounds { get; internal set; } - public ReadOnlyCollection Objects { get; set; } - - public bool HasChildNodes() - { - return _nodes[0] != null; - } - - public QuadNode(Box2 bounds) - { - Bounds = bounds; - Nodes = new ReadOnlyCollection(_nodes); - Objects = new ReadOnlyCollection(quadObjects); - } - - public QuadNode(float x, float y, float width, float height) - : this(Box2.FromDimensions(x, y, width, height)) - { - - } - } - } - - public enum DiRectangleFion : byte - { - NW = 0, - NE = 1, - SW = 2, - SE = 3 - } - - [NotContentImplementable] - public interface IQuadObject - { - Box2 Bounds { get; } - } -} From 9c010a1218b7670bf48fd67c836776f26254b94f Mon Sep 17 00:00:00 2001 From: Velken <8467292+Velken@users.noreply.github.com> Date: Tue, 30 Jun 2026 23:42:00 -0300 Subject: [PATCH 097/178] Add .ftl file upload support (#6473) --- RELEASE-NOTES.md | 1 + .../Localization/LocalizationManager.cs | 18 ++++++++++++++++++ .../Upload/SharedNetworkResourceManager.cs | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ad428787253..6f409b9e64d 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -42,6 +42,7 @@ END TEMPLATE--> * Adds `AttachedAudioDespawnedEvent`, which is raised against the parent of a despawning `AudioComponent`. * Added a DictionaryEquals extension method to check equality between two dictionaries. +* Added support for uploading .ftl files. Format is: //.ftl - Example: /TestUpload/en-US.ftl , you can have multiple files, so long they are on different subfolders, they will all be loaded. ### Bugfixes diff --git a/Robust.Shared/Localization/LocalizationManager.cs b/Robust.Shared/Localization/LocalizationManager.cs index b7d3ea8f817..f3e17c726ac 100644 --- a/Robust.Shared/Localization/LocalizationManager.cs +++ b/Robust.Shared/Localization/LocalizationManager.cs @@ -27,6 +27,7 @@ namespace Robust.Shared.Localization internal abstract partial class LocalizationManager : ILocalizationManagerInternal { protected static readonly ResPath LocaleDirPath = new("/Locale"); + protected static readonly ResPath UploadedDirPath = new("/Uploaded"); [Dependency] private IConfigurationManager _configuration = default!; [Dependency] private IResourceManager _res = default!; @@ -403,6 +404,20 @@ public List GetFoundCultures() result.Add(CultureInfo.GetCultureInfo(cultureName, predefinedOnly: false)); } + var uploadedFiles = _res.ContentFindFiles(UploadedDirPath) + .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)); + + foreach (var file in uploadedFiles) + { + var cultureName = Path.GetFileNameWithoutExtension(file.Filename); + if (CultureInfo.GetCultures(CultureTypes.AllCultures).Any(c => c.Name.Equals(cultureName, StringComparison.InvariantCultureIgnoreCase))) + { + var culture = CultureInfo.GetCultureInfo(cultureName, predefinedOnly: false); + if (!result.Contains(culture)) + result.Add(culture); + } + } + return result; } @@ -485,10 +500,13 @@ private void _initData(IResourceManager resourceManager, CultureInfo culture, Fl { // Load data from .ftl files. // Data is loaded from /Locale//* + // and from /Uploaded/**/Locale//* var root = LocaleDirPath / culture.Name; var files = resourceManager.ContentFindFiles(root) + .Concat(resourceManager.ContentFindFiles(UploadedDirPath) + .Where(c => c.CanonPath.Contains($"/Locale/{culture.Name}/", StringComparison.InvariantCultureIgnoreCase))) .Where(c => c.Filename.EndsWith(".ftl", StringComparison.InvariantCultureIgnoreCase)) .ToArray(); diff --git a/Robust.Shared/Upload/SharedNetworkResourceManager.cs b/Robust.Shared/Upload/SharedNetworkResourceManager.cs index d3937a1ac59..cc61f7e792a 100644 --- a/Robust.Shared/Upload/SharedNetworkResourceManager.cs +++ b/Robust.Shared/Upload/SharedNetworkResourceManager.cs @@ -7,6 +7,7 @@ using Robust.Shared.Asynchronous; using Robust.Shared.ContentPack; using Robust.Shared.IoC; +using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Network.Transfer; @@ -38,6 +39,7 @@ public abstract partial class SharedNetworkResourceManager : IDisposable, IPostI [Dependency] protected IResourceManager ResourceManager = default!; [Dependency] protected ITransferManager TransferManager = default!; [Dependency] protected ILogManager LogManager = default!; + [Dependency] protected ILocalizationManager LocalizationManager = default!; [Dependency] private ITaskManager _taskManager = default!; protected ISawmill Sawmill = default!; @@ -112,9 +114,13 @@ protected virtual void ValidateUpload(uint size) protected async Task> IngestFileStream(Stream stream) { var list = new List<(ResPath Relative, byte[] Data)>(); + var anyLoc = false; await foreach (var (relative, data) in ReadTransferStream(stream).ConfigureAwait(false)) { + if (relative.Extension == "ftl") + anyLoc = true; + Sawmill.Verbose($"Storing uploaded file: {relative} ({ByteHelpers.FormatBytes(data.Length)})"); _taskManager.RunOnMainThread(() => { @@ -123,6 +129,14 @@ protected virtual void ValidateUpload(uint size) list.Add((relative, data)); } + if (anyLoc) + { + _taskManager.RunOnMainThread(() => + { + LocalizationManager.ReloadLocalizations(); + }); + } + return list; } From e458c13e1076cc7578d8fc772f72ed87a33e1ff0 Mon Sep 17 00:00:00 2001 From: Hayden Date: Tue, 30 Jun 2026 20:55:04 -0600 Subject: [PATCH 098/178] Basic Tracy support (#6686) --- Directory.Packages.props | 1 + RELEASE-NOTES.md | 2 +- .../GameStates/ClientGameStateManager.cs | 4 + Robust.Client/Robust.Client.csproj | 1 + Robust.Server/Robust.Server.csproj | 1 + Robust.Shared/CVars.cs | 10 ++ Robust.Shared/Profiling/ProfManager.Tracy.cs | 151 ++++++++++++++++++ Robust.Shared/Profiling/ProfManager.cs | 101 +++++++++++- Robust.Shared/Robust.Shared.csproj | 1 + Robust.Shared/Timing/GameLoop.cs | 12 +- 10 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 Robust.Shared/Profiling/ProfManager.Tracy.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 92e4a7258df..e740d3a3e38 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -69,6 +69,7 @@ + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6f409b9e64d..c7e2ac56f4a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -14,7 +14,7 @@ Don't change the format without looking at the script! ### New features -*None yet* +* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! ### Bugfixes diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index d9edd7a1e5e..9ef5161534e 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -321,6 +321,7 @@ public void ApplyGameState() _prof.WriteValue($"State buffer size", curBufSize); _prof.WriteValue($"State apply count", targetProcessedTick.Value - _timing.LastProcessedTick.Value); + _prof.EmitEntities(_entities.EntityCount); bool processedAny = false; @@ -1037,6 +1038,9 @@ private void CreateNewEntity(EntityState state, GameTick toTick) if (metaState == null) throw new MissingMetadataException(state.NetEntity); + // record the entity we created to the profiler + using var group = _prof.Group($"Create entity {metaState.PrototypeId}"); + var uid = _entities.CreateEntity(metaState.PrototypeId, out var newMeta); _toApply.Add(uid, new(uid, state.NetEntity, newMeta, true, false, GameTick.Zero, state, null, null)); _created.Add(state.NetEntity); diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index c99c8a4f4a9..a23f7d7dd33 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -31,6 +31,7 @@ + diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj index 11ed9746def..4ff270ca482 100644 --- a/Robust.Server/Robust.Server.csproj +++ b/Robust.Server/Robust.Server.csproj @@ -40,6 +40,7 @@ + diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 2d85fd055c9..81a4c153b4a 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -1751,6 +1751,16 @@ protected CVars() /// public static readonly CVarDef ProfEnabled = CVarDef.Create("prof.enabled", false); + /// + /// Enables the Tracy profiling system. Tracing will stay enabled for the entire runtime of the program even if + /// you turn this cvar off. + /// + /// + /// By default, this will listen for Tracy connections on all interfaces! Set the TRACY_ONLY_LOCALHOST + /// env var to 1 if you want to restrict to localhost. + /// + public static readonly CVarDef TracyProfEnabled = CVarDef.Create("prof.tracy.enabled", false); + /// /// Event log buffer size for the profiling system. /// diff --git a/Robust.Shared/Profiling/ProfManager.Tracy.cs b/Robust.Shared/Profiling/ProfManager.Tracy.cs new file mode 100644 index 00000000000..9b8b41fcffd --- /dev/null +++ b/Robust.Shared/Profiling/ProfManager.Tracy.cs @@ -0,0 +1,151 @@ +using System; +using bottlenoselabs.C2CS.Runtime; +using Robust.Shared.Maths; +using static Tracy.PInvoke; + +namespace Robust.Shared.Profiling; + +// Tracy-side partial ProfManager + +public sealed partial class ProfManager +{ + /// + /// Proxy to prof.tracy.enabled CVar. + /// + public bool IsTracyEnabled { get; private set; } + + partial void InitializeTracyCvars() + { + _cfg.OnValueChanged(CVars.TracyProfEnabled, b => IsTracyEnabled = b, true); + } + + private bool _tracyPlotsInitialized; + private long _tracyLastAllocatedBytes; + private CString _tracyPlotManagedHeap; + private CString _tracyPlotAllocRate; + private CString _tracyPlotGen0; + private CString _tracyPlotGen1; + private CString _tracyPlotGen2; + private CString _tracyEntityCount; + + internal unsafe void EmitFrameImage(void* image, ushort width, ushort height, byte offset, bool flip) + { + if (!IsTracyEnabled) + return; + + TracyEmitFrameImage(image, width, height, offset, flip ? 1 : 0); + } + + internal void EmitMemoryPlots(int gcGen0, int gcGen1, int gcGen2) + { + if (!IsTracyEnabled) + return; + + if (!_tracyPlotsInitialized) + InitTracyPlots(); + + var allocated = GC.GetTotalAllocatedBytes(); + + TracyEmitPlotInt(_tracyPlotManagedHeap, GC.GetTotalMemory(false)); + TracyEmitPlotInt(_tracyPlotAllocRate, allocated - _tracyLastAllocatedBytes); + TracyEmitPlotInt(_tracyPlotGen0, gcGen0); + TracyEmitPlotInt(_tracyPlotGen1, gcGen1); + TracyEmitPlotInt(_tracyPlotGen2, gcGen2); + + _tracyLastAllocatedBytes = allocated; + + } + + internal void EmitEntities(int entityCount) + { + if (!IsTracyEnabled) + return; + + if (!_tracyPlotsInitialized) + InitTracyPlots(); + + TracyEmitPlotInt(_tracyEntityCount, entityCount); + } + + private void InitTracyPlots() + { + _tracyPlotsInitialized = true; + _tracyLastAllocatedBytes = GC.GetTotalAllocatedBytes(); + + _tracyPlotManagedHeap = CString.FromString("Managed Heap"); + _tracyPlotAllocRate = CString.FromString("Alloc Rate"); + _tracyPlotGen0 = CString.FromString("GC Gen 0"); + _tracyPlotGen1 = CString.FromString("GC Gen 1"); + _tracyPlotGen2 = CString.FromString("GC Gen 2"); + _tracyEntityCount = CString.FromString("Entities"); + + const int memoryFormat = (int)TracyPlotFormatEnum.TracyPlotFormatMemory; + TracyEmitPlotConfig(_tracyPlotManagedHeap, memoryFormat, step: 0, fill: 1, color: 0); + TracyEmitPlotConfig(_tracyPlotAllocRate, memoryFormat, step: 0, fill: 1, color: 0); + + const int numberFormat = (int)TracyPlotFormatEnum.TracyPlotFormatNumber; + TracyEmitPlotConfig(_tracyPlotGen0, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyPlotGen1, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyPlotGen2, numberFormat, step: 1, fill: 0, color: 0); + TracyEmitPlotConfig(_tracyEntityCount, numberFormat, step: 1, fill: 0, color: 0); + } + + /// + /// Marks the boundary of a continuous Tracy frame. Used by . + /// + private static void EmitFrameMark() + { + TracyEmitFrameMark(null); + } + + /// + /// Creates a for use by Tracy. Also returns the + /// length of the string for interop convenience. + /// + internal static CString GetCString(string? fromString, out ulong cLength) + { + if (fromString == null) + { + cLength = 0; + return new CString(0); + } + + cLength = (ulong)fromString.Length; + return CString.FromString(fromString); + } + + private static TracyProfilerZone BeginTracyZone(string name, int lineNumber, Color? color, string? filePath, string? memberName) + { + using var fileStr = GetCString(filePath, out var fileLn); + using var memberStr = GetCString(memberName, out var memberLn); + using var nameStr = GetCString(name, out var nameLn); + var srcLocId = TracyAllocSrclocName((uint)lineNumber, fileStr, fileLn, memberStr, memberLn, nameStr, nameLn, (uint) (color?.ToArgb() ?? 0)); + var context = TracyEmitZoneBeginAlloc(srcLocId, 1); + return new TracyProfilerZone(context); + } +} + +internal readonly struct TracyProfilerZone : IDisposable +{ + private readonly TracyCZoneCtx _context; + + private uint Id => _context.Data.Id; + + private int Active => _context.Data.Active; + + internal TracyProfilerZone(TracyCZoneCtx context) + { + _context = context; + } + + internal void EmitText(string text) + { + using var textStr = ProfManager.GetCString(text, out var textLn); + TracyEmitZoneText(_context, textStr, textLn); + } + + public void Dispose() + { + TracyEmitZoneEnd(_context); + } +} diff --git a/Robust.Shared/Profiling/ProfManager.cs b/Robust.Shared/Profiling/ProfManager.cs index 67684503c76..47cf7ef374d 100644 --- a/Robust.Shared/Profiling/ProfManager.cs +++ b/Robust.Shared/Profiling/ProfManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; @@ -6,6 +6,7 @@ using Robust.Shared.Collections; using Robust.Shared.Configuration; using Robust.Shared.Log; +using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Shared.Profiling; @@ -14,6 +15,8 @@ namespace Robust.Shared.Profiling; // See ProfData.cs for description of profiling data layout. +// Tracy integration lives in ProfManager.Tracy.cs. + public sealed partial class ProfManager { [IoC.Dependency] private IConfigurationManager _cfg = default!; @@ -62,8 +65,11 @@ internal void Initialize() }, true); _cfg.OnValueChanged(CVars.ProfEnabled, b => IsEnabled = b, true); + InitializeTracyCvars(); } + partial void InitializeTracyCvars(); + /// /// Write an index covering the region from to the current write position. /// @@ -135,9 +141,13 @@ public long WriteValue(string text, in ProfValue value) /// /// Make a guarded value for usage with using blocks. /// - public ValueGuard Value(string text) + public ValueGuard Value( + string text, + [CallerLineNumber] int lineNumber = 0, + [CallerFilePath] string? filePath = null, + [CallerMemberName] string? memberName = null) { - return new ValueGuard(this, text); + return new ValueGuard(this, text, lineNumber, filePath, memberName); } /// @@ -191,10 +201,29 @@ public void WriteGroupEnd(long startIndex, string text, in ProfSampler sampler) /// /// Make a guarded group for usage with using blocks. /// - public GroupGuard Group(string name) + /// The name of this group as it will show in the profiler. + /// + /// The color of this zone and all of its children, as it will show in Tracy. This does + /// not affect the in game profiler. + /// + public GroupGuard Group( + string name, + Color? color = null, + [CallerLineNumber] int lineNumber = 0, + [CallerFilePath] string? filePath = null, + [CallerMemberName] string? memberName = null) + { + var start = WriteGroupStart(); + return new GroupGuard(this, start, name, color ?? Color.Black, lineNumber, filePath, memberName); + } + + /// + /// Make a guarded group that emits Tracy frame markers instead of a zone, for usage with using blocks. + /// + internal FrameGuard Frame(string name) { var start = WriteGroupStart(); - return new GroupGuard(this, start, name); + return new FrameGuard(this, start, name); } /// @@ -245,7 +274,51 @@ private ref ProfLog WriteCmd() private readonly string _groupName; private readonly ProfSampler _sampler; - public GroupGuard(ProfManager mgr, long startIndex, string groupName) + private TracyProfilerZone? TracyZone { get; } + + internal GroupGuard( + ProfManager mgr, + long startIndex, + string groupName, + Color? color, + int lineNumber, + string? filePath, + string? memberName) + { + _mgr = mgr; + _startIndex = startIndex; + _groupName = groupName; + _sampler = ProfSampler.StartNew(); + if (_mgr.IsTracyEnabled) + TracyZone = BeginTracyZone(groupName, lineNumber, color, filePath, memberName); + } + + /// + /// Adds text to show up in Tracy for this zone. + /// + /// If Tracy is not enabled, this does nothing. + /// + public void EmitText(string str) + { + TracyZone?.EmitText(str); + } + + public void Dispose() + { + _mgr.WriteGroupEnd(_startIndex, _groupName, _sampler); + if (_mgr.IsTracyEnabled) + TracyZone?.Dispose(); + } + } + + internal readonly struct FrameGuard : IDisposable + { + private readonly ProfManager _mgr; + private readonly long _startIndex; + private readonly string _groupName; + private readonly ProfSampler _sampler; + + public FrameGuard(ProfManager mgr, long startIndex, string groupName) { _mgr = mgr; _startIndex = startIndex; @@ -256,6 +329,8 @@ public GroupGuard(ProfManager mgr, long startIndex, string groupName) public void Dispose() { _mgr.WriteGroupEnd(_startIndex, _groupName, _sampler); + if (_mgr.IsTracyEnabled) + EmitFrameMark(); } } @@ -264,17 +339,27 @@ public void Dispose() private readonly ProfManager _mgr; private readonly string _text; private readonly ProfSampler _sampler; - - public ValueGuard(ProfManager mgr, string text) + private readonly TracyProfilerZone? _tracyZone; + + public ValueGuard( + ProfManager mgr, + string text, + int lineNumber, + string? filePath, + string? memberName) { _mgr = mgr; _text = text; _sampler = ProfSampler.StartNew(); + if (_mgr.IsTracyEnabled) + _tracyZone = BeginTracyZone(_text, lineNumber, Color.Black, filePath, memberName); } public void Dispose() { _mgr.WriteValue(_text, _sampler); + if (_mgr.IsTracyEnabled) + _tracyZone?.Dispose(); } } } diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index cdd7bf9746e..480951f67ee 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -28,6 +28,7 @@ + diff --git a/Robust.Shared/Timing/GameLoop.cs b/Robust.Shared/Timing/GameLoop.cs index 64030fb6d6f..b25383b97f3 100644 --- a/Robust.Shared/Timing/GameLoop.cs +++ b/Robust.Shared/Timing/GameLoop.cs @@ -310,7 +310,7 @@ public void Run() try #endif { - using (_prof.Group("Render")) + using (_prof.Frame("Render")) { Render?.Invoke(this, realFrameEvent); } @@ -325,9 +325,13 @@ public void Run() { using var gc = _prof.Group("GC Overview"); - _prof.WriteValue("Gen 0 Count", ProfData.Int32(GC.CollectionCount(0) - profFrameGcGen0)); - _prof.WriteValue("Gen 1 Count", ProfData.Int32(GC.CollectionCount(1) - profFrameGcGen1)); - _prof.WriteValue("Gen 2 Count", ProfData.Int32(GC.CollectionCount(2) - profFrameGcGen2)); + var gcGen0 = GC.CollectionCount(0); + var gcGen1 = GC.CollectionCount(1); + var gcGen2 = GC.CollectionCount(2); + _prof.WriteValue("Gen 0 Count", ProfData.Int32(gcGen0 - profFrameGcGen0)); + _prof.WriteValue("Gen 1 Count", ProfData.Int32(gcGen1 - profFrameGcGen1)); + _prof.WriteValue("Gen 2 Count", ProfData.Int32(gcGen2 - profFrameGcGen2)); + _prof.EmitMemoryPlots(gcGen0, gcGen1, gcGen2); } _prof.WriteGroupEnd(profFrameGroupStart, "Frame", profFrameSw); From 1322177e624290553297b7cc3e9e236b35fac345 Mon Sep 17 00:00:00 2001 From: Centronias Date: Tue, 30 Jun 2026 20:30:23 -0700 Subject: [PATCH 099/178] IEntitySystem Event Subscription Code Generation (#6227) Co-authored-by: metalgearsloth --- ...EntitySystemSubscriptionsGenerator.targets | 5 + Robust.Client/Audio/AudioSystem.cs | 17 +- Robust.Client/Robust.Client.csproj | 1 + Robust.Roslyn.Shared/Diagnostics.cs | 3 + Robust.Roslyn.Shared/PartialTypeHelper.cs | 34 +++ .../EntitySystemSubscriptionGenerator.cs | 246 ++++++++++++++++++ ...ystemSubscriptionGeneratorErrorAnalyzer.cs | 166 ++++++++++++ .../KnownTypes.cs | 58 +++++ .../Properties/launchSettings.json | 9 + ....EntitySystemSubscriptionsGenerator.csproj | 5 + .../Robust.Shared.IntegrationTests.csproj | 1 + ...ySystemSubscriptionsGeneratorAttributes.cs | 51 ++++ Robust.Shared/GameObjects/EntitySystem.cs | 4 + .../GameObjects/EntitySystemManager.cs | 2 + Robust.Shared/Robust.Shared.csproj | 1 + Robust.UnitTesting/Robust.UnitTesting.csproj | 1 + RobustToolbox.slnx | 5 + 17 files changed, 599 insertions(+), 10 deletions(-) create mode 100644 MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets create mode 100644 Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs create mode 100644 Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs create mode 100644 Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs create mode 100644 Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json create mode 100644 Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj create mode 100644 Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs diff --git a/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets b/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets new file mode 100644 index 00000000000..dd74af58764 --- /dev/null +++ b/MSBuild/Robust.EntitySystemSubscriptionsGenerator.targets @@ -0,0 +1,5 @@ + + + + + diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs index e7944446cfb..d78ad656985 100644 --- a/Robust.Client/Audio/AudioSystem.cs +++ b/Robust.Client/Audio/AudioSystem.cs @@ -118,16 +118,6 @@ public override void Initialize() _physicsQuery = GetEntityQuery(); - SubscribeLocalEvent(OnAudioStartup); - SubscribeLocalEvent(OnAudioShutdown); - SubscribeLocalEvent(OnAudioPaused); - SubscribeLocalEvent(OnAudioState); - - // Replay stuff - SubscribeNetworkEvent(OnGlobalAudio); - SubscribeNetworkEvent(OnEntityAudio); - SubscribeNetworkEvent(OnEntityCoordinates); - Subs.CVar(CfgManager, CVars.AudioEndBuffer, OnAudioBuffer, true); Subs.CVar(CfgManager, CVars.AudioAttenuation, OnAudioAttenuation, true); Subs.CVar(CfgManager, CVars.AudioRaycastLength, OnRaycastLengthChanged, true); @@ -146,6 +136,7 @@ private void OnAudioTickRate(int obj) _audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime); } + [SubcribeLocalEvent] private void OnAudioState(Entity entity, ref AfterAutoHandleStateEvent args) { var component = entity.Comp; @@ -211,6 +202,7 @@ public void SetMasterVolume(float value) _audio.SetMasterGain(value); } + [SubcribeLocalEvent] private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args) { component.Pause(); @@ -222,6 +214,7 @@ protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, component.StartPlaying(); } + [SubcribeLocalEvent] private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args) { if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted) @@ -293,6 +286,7 @@ private void SetupSource(Entity entity, AudioResource audioResou } } + [SubcribeLocalEvent] private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args) { // Breaks with prediction? @@ -775,16 +769,19 @@ private void ApplyAudioParams(AudioParams audioParams, IAudioSource source) source.Looping = audioParams.Loop; } + [SubscribeNetworkEvent] private void OnEntityCoordinates(PlayAudioPositionalMessage ev) { PlayStatic(ev.Specifier, GetCoordinates(ev.Coordinates), ev.AudioParams, false); } + [SubscribeNetworkEvent] private void OnEntityAudio(PlayAudioEntityMessage ev) { PlayEntity(ev.Specifier, GetEntity(ev.NetEntity), ev.AudioParams, false); } + [SubscribeNetworkEvent] private void OnGlobalAudio(PlayAudioGlobalMessage ev) { PlayGlobal(ev.Specifier, ev.AudioParams, false); diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj index a23f7d7dd33..6ee0278b6c4 100644 --- a/Robust.Client/Robust.Client.csproj +++ b/Robust.Client/Robust.Client.csproj @@ -74,6 +74,7 @@ + diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index 3a58ebc3c06..c521ab4832f 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -57,6 +57,9 @@ public static class Diagnostics public const string IdHasDependenciesReadOnly = "RA0051"; public const string IdHasDependenciesPropertyField = "RA0052"; public const string IdExclusiveVirtual = "RA0053"; + public const string IdInvalidAMethodSignatureForGeneratedSubscription = "RA0054"; + public const string IdInvalidContainingTypeForGeneratedSubscription = "RA0055"; + public const string IdNonPartialContainingTypeForGeneratedSubscription = "RA0056"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Roslyn.Shared/PartialTypeHelper.cs b/Robust.Roslyn.Shared/PartialTypeHelper.cs index 119c4619fb8..d47e3bd34ee 100644 --- a/Robust.Roslyn.Shared/PartialTypeHelper.cs +++ b/Robust.Roslyn.Shared/PartialTypeHelper.cs @@ -77,6 +77,11 @@ public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDe return false; } + public string GetQualifiedName() + { + return Namespace == null ? Name : $"{Namespace}.{Name}"; + } + public string GetGeneratedFileName() { var name = Namespace == null ? "" : $"{Namespace}."; @@ -253,4 +258,33 @@ public int GetHashCode(PartialTypeInfo obj) } } } + + /// + /// An for s which considers all fields EXCEPT + /// . This comparer, therefore, considers s constructed from + /// different syntactic parts of one partial to be equal. + /// + public static readonly IEqualityComparer WithoutLocationEqualityComparer = + new WithoutLocationEqualityComparerImpl(); + + private class WithoutLocationEqualityComparerImpl : IEqualityComparer + { + public bool Equals(PartialTypeInfo t, PartialTypeInfo other) + { + return t.Namespace == other.Namespace && + t.Parts.Equals(other.Parts) && + t.IsValid == other.IsValid && + t.IsSealed == other.IsSealed; + } + + public int GetHashCode(PartialTypeInfo t) + { + var hash = new HashCode(); + hash.Add(t.Namespace); + hash.Add(t.Parts); + hash.Add(t.IsValid); + hash.Add(t.IsSealed); + return hash.ToHashCode(); + } + } } diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs new file mode 100644 index 00000000000..5f751673c40 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs @@ -0,0 +1,246 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared; +using Robust.Roslyn.Shared.Helpers; +using static Robust.Shared.EntitySystemSubscriptionsGenerator.KnownTypes; + +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +/// +/// This generator implements EntitySystem.AutoSubscriptions() for all EntitySystems with methods +/// annotated by auto-subscription attributes. In case any attributes are applied to methods incorrectly, this generator +/// just silently ignores them and expects will complain +/// on its behalf (except in the case of attempting to add generated code to a non-partial type -- we complain about +/// that here). +/// +/// +[Generator(LanguageNames.CSharp)] +public class EntitySystemSubscriptionGenerator : IIncrementalGenerator +{ + private static readonly DiagnosticDescriptor NotPartial = new( + Diagnostics.IdNonPartialContainingTypeForGeneratedSubscription, + "Containing class must be declared as Partial", + "Method is declared in type \"{0}\" which is not Partial", + "Usage", + DiagnosticSeverity.Error, + true + ); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var annotatedEntitySystems = Aggregate( + GetEntityTypeCandidatesContainingAnnotatedMethods(context, AllSubscriptionMemberAttributeName), + GetEntityTypeCandidatesContainingAnnotatedMethods(context, NetworkSubscriptionMemberAttributeName), + GetEntityTypeCandidatesContainingAnnotatedMethods(context, LocalSubscriptionMemberAttributeName) + ) // Get all candidate types containing subscription annotated methods + .SelectMany((array, _) => + array.ToImmutableHashSet(PartialTypeInfo.WithoutLocationEqualityComparer)) // Dedupe + .Combine(context.CompilationProvider) + .Select((inputs, cancel) => + { + // For each EntitySystem we've identified as containing subscriptions... + + var (partialTypeInfo, compilation) = inputs; + if (compilation.GetTypeByMetadataName(partialTypeInfo.GetMetadataName()) is not + { } entitySystemType) + return new EntitySystemInfo(partialTypeInfo, []); + + // ... check all methods in the type to see if it's a subscription, assembling subscriptions into an array. + var subs = entitySystemType.GetMembers() + .OfType() + .Select(method => + { + cancel.ThrowIfCancellationRequested(); + return TryParseSubscriptions(method); + }) + .OfType() + .ToImmutableArray(); + + return new EntitySystemInfo(partialTypeInfo, subs); + } + ); + + context.RegisterImplementationSourceOutput( + // Only deal with types that have subscriptions. + annotatedEntitySystems.Where(it => !it.Subscriptions.IsEmpty), + (productionContext, info) => + { + var (partialTypeInfo, subscriptions) = info; + if (partialTypeInfo.CheckPartialDiagnostic(productionContext, NotPartial)) + return; + + var subscriptionsSyntax = new StringBuilder(); + foreach (var method in subscriptions) + { + productionContext.CancellationToken.ThrowIfCancellationRequested(); + var subscriptionMethod = method.Type.ToSubscriptionMethod(); + var typeArgs = string.Join(", ", method.TypeArgs); + subscriptionsSyntax.AppendLine($" {subscriptionMethod}<{typeArgs}>({method.MethodName});"); + } + + var builder = new StringBuilder(@" +// + +using Robust.Shared.GameObjects; +using JetBrains.Annotations; + +"); + partialTypeInfo.WriteHeader(builder); + builder.AppendLine($@" +{{ + /// + [MustCallBase] + public override void AutoSubscriptions() + {{ + base.AutoSubscriptions(); + +{subscriptionsSyntax} + }} +}} +"); + partialTypeInfo.WriteFooter(builder); + + productionContext.AddSource(partialTypeInfo.GetGeneratedFileName(), builder.ToString()); + } + ); + } + + /// Returns s for all types in the compilation that contain methods with the given + /// attribute. + private static IncrementalValuesProvider GetEntityTypeCandidatesContainingAnnotatedMethods( + IncrementalGeneratorInitializationContext context, + string attributeName + ) + { + return context.SyntaxProvider.ForAttributeWithMetadataName( + attributeName, + (node, _) => node is MethodDeclarationSyntax, + (ctx, _) => + { + if (ctx.TargetSymbol is not IMethodSymbol symbol || + ctx.TargetNode is not MethodDeclarationSyntax { Parent: TypeDeclarationSyntax parentSyntax }) + return null; + + return PartialTypeInfo.FromSymbol(symbol.ContainingType, parentSyntax); + }) + .Where(it => it is not null) + .Select((it, _) => it ?? throw new("Unreachable")); + } + + /// Tries to parse 's signature as an even subscription, returning the information required + /// to make the subscription function call in the generated code. Returns null if the method is not a + /// subscription, or is a subscription and its signature is invalid. + private static SubscriptionInfo? TryParseSubscriptions(IMethodSymbol method) + { + return TryParseSubscription( + method, + AllSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? TryParseEntitySessionEventHandler(m) + ) ?? TryParseSubscription( + method, + NetworkSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? TryParseEntitySessionEventHandler(m) + ) ?? TryParseSubscription( + method, + LocalSubscriptionMemberAttributeName, + m => TryParseEntityEventHandler(m) ?? + TryParseEntitySessionEventHandler(m) ?? + TryParseComponentEventHandler(m) ?? + TryParseEntityEventRefHandler(m) + ); + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntityEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntityEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 1 || + method.Parameters[0].Type is not INamedTypeSymbol eventType) + return null; + + return [eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntitySessionEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntitySessionEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 2 || + method.Parameters[0].Type is not INamedTypeSymbol eventType || + !TypeSymbolHelper.ShittyTypeMatch( + method.Parameters[1].Type, + EntitySessionEventArgsTypeName + )) + return null; + + return [eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.EntityEventRefHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseEntityEventRefHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 2 || + method.Parameters[0].Type is not INamedTypeSymbol entityType || + method.Parameters[1].Type is not INamedTypeSymbol eventType || + method.Parameters[1].RefKind != RefKind.Ref) + return null; + + if (entityType.OriginalDefinition.ToDisplayString() != EntityTypeName || + entityType.TypeArguments is not [INamedTypeSymbol componentType] || + !TypeSymbolHelper.ImplementsInterface(componentType, IComponentTypeName)) + return null; + + return [componentType.ToString(), eventType.ToString()]; + } + + /// Tries to parse 's signature as Robust.Shared.GameObjects.ComponentEventHandler. + /// The type argument syntax to include in the subscription function call. + public static ImmutableArray? TryParseComponentEventHandler(IMethodSymbol method) + { + if (method.Parameters.Length != 3 || + method.Parameters[0].Type is not INamedTypeSymbol entityUidType || + method.Parameters[1].Type is not INamedTypeSymbol componentType || + method.Parameters[2].Type is not INamedTypeSymbol eventType || + !TypeSymbolHelper.ShittyTypeMatch(entityUidType, EntityUidTypeName) || + !TypeSymbolHelper.ImplementsInterface(componentType, IComponentTypeName)) + return null; + + return [componentType.ToString(), eventType.ToString()]; + } + + private static SubscriptionInfo? TryParseSubscription( + IMethodSymbol method, + string annotationName, + Func?> parseFunc + ) + { + if (annotationName.ToSubscriptionType() is not { } subType || + !AttributeHelper.HasAttribute(method, annotationName, out _) || + parseFunc(method) is not { } parameters) + return null; + + return new SubscriptionInfo(method.Name, subType, parameters); + } + + /// Aggregates all of the s across all the given providers into a single array value + /// provided by the returned provider. + private static IncrementalValueProvider> Aggregate( + IncrementalValuesProvider first, + params IncrementalValuesProvider[] more + ) + { + return more.Aggregate( + first.Collect(), + (acc, valuesProvider) => + acc.Combine(valuesProvider.Collect()) + .Select((values, _) => values.Left.AddRange(values.Right)) + ); + } + + private record struct EntitySystemInfo(PartialTypeInfo Type, EquatableArray Subscriptions); + + private record struct SubscriptionInfo(string MethodName, SubscriptionType Type, EquatableArray TypeArgs); +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs new file mode 100644 index 00000000000..42b65ec3efc --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGeneratorErrorAnalyzer.cs @@ -0,0 +1,166 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Robust.Roslyn.Shared; + +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +/// +/// This analyzer ensures that all methods annotated with the relevant subscription attributes are:
    +///
  • In an EntitySystem
  • +///
  • Have the correct signature for their subscription type
  • +///
+/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class EntitySystemSubscriptionGeneratorErrorAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor BadMethodSignature = new( + Diagnostics.IdInvalidAMethodSignatureForGeneratedSubscription, + "Invalid method signature", + "Method signature is incompatible with required delegate type(s) for \"{0}\". Compatible types are: {1}.", + "Usage", + DiagnosticSeverity.Error, + true + ); + + private static readonly DiagnosticDescriptor NotEntitySystem = new( + Diagnostics.IdInvalidContainingTypeForGeneratedSubscription, + $"Method not in {KnownTypes.EntitySystemTypeName}", + $"Method is declared in type \"{{0}}\" which does not extend {KnownTypes.EntitySystemTypeName}", + "Usage", + DiagnosticSeverity.Error, + true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = + [BadMethodSignature, NotEntitySystem]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis( + GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics + ); + + EnsureAnnotatedSubscriptionMethodsAreInAnEntitySystem(context); + EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(context); + } + + private static void EnsureAnnotatedSubscriptionMethodsAreInAnEntitySystem(AnalysisContext context) + { + List attributeNames = + [ + KnownTypes.AllSubscriptionMemberAttributeName, + KnownTypes.NetworkSubscriptionMemberAttributeName, + KnownTypes.LocalSubscriptionMemberAttributeName, + ]; + + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName(KnownTypes.EntitySystemTypeName) is not { } entitySystemType) + return; + + var attributeSymbols = attributeNames + .Select(attributeName => c.Compilation.GetTypeByMetadataName(attributeName)) + .OfType() + .ToList(); + + c.RegisterSymbolStartAction( + c => + { + if (!c.Symbol.GetAttributes() + .Select(it => it.AttributeClass) + .Intersect(attributeSymbols, SymbolEqualityComparer.IncludeNullability) + .Any() || + IsSubtypeOf(c.Symbol.ContainingType, entitySystemType)) + return; + + c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( + NotEntitySystem, + c.Symbol.Locations[0], + c.Symbol.ContainingType?.Name ?? "" + ))); + }, + SymbolKind.Method + ); + }); + } + + private static bool IsSubtypeOf(ITypeSymbol subtype, INamedTypeSymbol supertype) + { + return SymbolEqualityComparer.Default.Equals(subtype.BaseType, supertype) || + (subtype.BaseType is not null && IsSubtypeOf(subtype.BaseType, supertype)); + } + + private static void EnsureAnnotatedSubscriptionMethodsHaveCorrectSignatures(AnalysisContext context) + { + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.AllSubscriptionMemberAttributeName, + m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, + KnownTypes.NonComponentSubscriptionHandlerTypes + ); + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.NetworkSubscriptionMemberAttributeName, + m => (EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m)) is not null, + KnownTypes.NonComponentSubscriptionHandlerTypes + ); + EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + context, + KnownTypes.LocalSubscriptionMemberAttributeName, + m => ( + EntitySystemSubscriptionGenerator.TryParseEntityEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntitySessionEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseComponentEventHandler(m) ?? + EntitySystemSubscriptionGenerator.TryParseEntityEventRefHandler(m) + ) is not null, + string.Join(", ", + KnownTypes.NonComponentSubscriptionHandlerTypes, + KnownTypes.ComponentSubscriptionHandlerTypes) + ); + } + + /// Checks that any methods annotated with have the correct signature as + /// determined by . If not, a + /// diagnostic is emitted, describing how the signature should instead conform to + /// . + private static void EnsureAnnotatedSubscriptionMethodHasCorrectSignature( + AnalysisContext context, + string annotationName, + Func hasCorrectParameters, + string acceptableHandlerTypes + ) + { + context.RegisterCompilationStartAction(c => + { + if (c.Compilation.GetTypeByMetadataName(annotationName) is not { } annotationSymbol) + return; + + c.RegisterSymbolStartAction( + c => + { + // The `symbolKind` arg to `RegisterSymbolStartAction` should make this never fail. + if (c.Symbol is not IMethodSymbol symbol) + throw new Exception($"Expected {nameof(IMethodSymbol)} but got {c.Symbol.GetType().FullName}"); + + if (!symbol.GetAttributes() + .Select(it => it.AttributeClass) + .Contains(annotationSymbol, SymbolEqualityComparer.IncludeNullability) || + hasCorrectParameters(symbol)) + return; + + c.RegisterSymbolEndAction(c => c.ReportDiagnostic(Diagnostic.Create( + BadMethodSignature, + symbol.Locations[0], + annotationName, + acceptableHandlerTypes + ))); + }, + SymbolKind.Method + ); + }); + } +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs new file mode 100644 index 00000000000..aec57705327 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs @@ -0,0 +1,58 @@ +namespace Robust.Shared.EntitySystemSubscriptionsGenerator; + +public static class KnownTypes +{ + public const string EntitySystemTypeName = "Robust.Shared.GameObjects.EntitySystem"; + public const string EntityUidTypeName = "Robust.Shared.GameObjects.EntityUid"; + public const string EntityTypeName = "Robust.Shared.GameObjects.Entity"; + public const string EntitySessionEventArgsTypeName = "Robust.Shared.GameObjects.EntitySessionEventArgs"; + public const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent"; + + public const string LocalSubscriptionMemberAttributeName = + "Robust.Shared.Analyzers.LocalEventSubscriptionAttribute"; + + public const string NetworkSubscriptionMemberAttributeName = + "Robust.Shared.Analyzers.NetworkEventSubscriptionAttribute"; + + public const string AllSubscriptionMemberAttributeName = "Robust.Shared.Analyzers.EventSubscriptionAttribute"; + + public static readonly string ComponentSubscriptionHandlerTypes = string.Join( + ", ", + "Robust.Shared.GameObjects.ComponentEventHandler", + "Robust.Shared.GameObjects.ComponentEventRefHandler", + "Robust.Shared.GameObjects.EntityEventRefHandler" + ); + + public static readonly string NonComponentSubscriptionHandlerTypes = string.Join( + ", ", + "Robust.Shared.GameObjects.EntityEventHandler", + "Robust.Shared.GameObjects.EntityEventRefHandler", + "Robust.Shared.GameObjects.EntitySessionEventHandler" + ); + + public static SubscriptionType? ToSubscriptionType(this string annotation) + { + return annotation switch + { + AllSubscriptionMemberAttributeName => SubscriptionType.All, + NetworkSubscriptionMemberAttributeName => SubscriptionType.Network, + LocalSubscriptionMemberAttributeName => SubscriptionType.Local, + _ => null + }; + } + + public static string ToSubscriptionMethod(this SubscriptionType type) => type switch + { + SubscriptionType.All => "SubscribeAllEvent", + SubscriptionType.Network => "SubscribeNetworkEvent", + SubscriptionType.Local => "SubscribeLocalEvent", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; +} + +public enum SubscriptionType +{ + All, + Network, + Local, +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json b/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json new file mode 100644 index 00000000000..91bdf53a218 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Entity System Subscriptions Generator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../../Content.Shared/Content.Shared.csproj" + } + } +} diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj b/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj new file mode 100644 index 00000000000..076e7a492d3 --- /dev/null +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/Robust.Shared.EntitySystemSubscriptionsGenerator.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj index 8d287bd2917..a5bf54a7f2c 100644 --- a/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj +++ b/Robust.Shared.IntegrationTests/Robust.Shared.IntegrationTests.csproj @@ -36,4 +36,5 @@ + diff --git a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs new file mode 100644 index 00000000000..9587cb23f7c --- /dev/null +++ b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs @@ -0,0 +1,51 @@ +using System; +using Robust.Shared.GameObjects; + +namespace Robust.Shared.Analyzers; + +// These annotations direct the operation of `Robust.Shared.EntitySystemSubscriptionsGenerator`'s +// `EntitySystemSubscriptionGenerator` and `EntitySystemSubscriptionGeneratorErrorAnalyser`. + +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a EntitySystem.SubscribeLocalEvent call generated, using the method as the handler, +/// with the event type (and component, as relevant) inferred from the method signature. +///
+/// For this to work, the annotated method must be compatible with one of the following delegate types: +///
    +///
  • +///
  • +///
  • +///
  • +///
+///
+/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeLocalEvent. +[AttributeUsage(AttributeTargets.Method)] +public sealed class SubcribeLocalEventAttribute : Attribute; + +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a EntitySystem.SubscribeNetworkEvent call generated, using the method as the handler, +/// with the event type inferred from the method signature. +///
+/// For this to work, the annotated method must be compatible with one of the following delegate types: +///
    +///
  • +///
  • +///
+///
+/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeNetworkEvent. +[AttributeUsage(AttributeTargets.Method)] +public sealed class SubscribeNetworkEventAttribute : Attribute; + +/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with +/// this attribute will have a EntitySystem.SubscribeAllEvent call generated, using the method as the handler, +/// with the event type inferred from the method signature. +///
+/// For this to work, the annotated method must be compatible with one of the following delegate types: +///
    +///
  • +///
  • +///
+///
+/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeAllEvent. +[AttributeUsage(AttributeTargets.Method)] +public sealed class EventSubscriptionAttribute : Attribute; diff --git a/Robust.Shared/GameObjects/EntitySystem.cs b/Robust.Shared/GameObjects/EntitySystem.cs index 00254839d37..8881fece6e4 100644 --- a/Robust.Shared/GameObjects/EntitySystem.cs +++ b/Robust.Shared/GameObjects/EntitySystem.cs @@ -94,6 +94,10 @@ protected EntitySystem() [MustCallBase(true)] public virtual void Initialize() { } + /// + [MustCallBase(true)] + public virtual void AutoSubscriptions() { } + /// /// /// Not ran on the client if prediction is disabled and diff --git a/Robust.Shared/GameObjects/EntitySystemManager.cs b/Robust.Shared/GameObjects/EntitySystemManager.cs index c22fb8b7523..05cb5708ba4 100644 --- a/Robust.Shared/GameObjects/EntitySystemManager.cs +++ b/Robust.Shared/GameObjects/EntitySystemManager.cs @@ -213,6 +213,8 @@ public void Initialize(bool discover = true) { var system = (IEntitySystem)SystemDependencyCollection.ResolveType(systemType); system.Initialize(); + if (system is EntitySystem entitySystem) + entitySystem.AutoSubscriptions(); SystemLoaded?.Invoke(this, new SystemChangedArgs(system)); } diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj index 480951f67ee..8750448cb22 100644 --- a/Robust.Shared/Robust.Shared.csproj +++ b/Robust.Shared/Robust.Shared.csproj @@ -63,4 +63,5 @@ + diff --git a/Robust.UnitTesting/Robust.UnitTesting.csproj b/Robust.UnitTesting/Robust.UnitTesting.csproj index 81d0d4fe5c8..f9077a4eb28 100644 --- a/Robust.UnitTesting/Robust.UnitTesting.csproj +++ b/Robust.UnitTesting/Robust.UnitTesting.csproj @@ -28,4 +28,5 @@
+ diff --git a/RobustToolbox.slnx b/RobustToolbox.slnx index 49ea4914b00..5dd38516a24 100644 --- a/RobustToolbox.slnx +++ b/RobustToolbox.slnx @@ -35,6 +35,7 @@ + @@ -83,6 +84,10 @@ + + + + From aeadf07a0d2d6d1cff6707b2b56c652cd99fcdd0 Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Wed, 1 Jul 2026 13:35:37 +1000 Subject: [PATCH 100/178] Version: 279.0.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 40 +++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index ebe81eaeda3..7e1e1e159ae 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 278.0.0 + 279.0.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c7e2ac56f4a..b6422e3d6a7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,14 +35,11 @@ END TEMPLATE--> ### Breaking changes -* Validate Box2i inputs to ensure no negative-sized boxes. -* Removed QuadTree due to lack of maintenance, test coverage, and usage. +*None yet* ### New features -* Adds `AttachedAudioDespawnedEvent`, which is raised against the parent of a despawning `AudioComponent`. -* Added a DictionaryEquals extension method to check equality between two dictionaries. -* Added support for uploading .ftl files. Format is: //.ftl - Example: /TestUpload/en-US.ftl , you can have multiple files, so long they are on different subfolders, they will all be loaded. +* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! ### Bugfixes @@ -57,6 +54,39 @@ END TEMPLATE--> *None yet* +## 279.0.0 + +### Breaking changes + +* Validate Box2i inputs to ensure no negative-sized boxes. +* Removed QuadTree due to lack of maintenance, test coverage, and usage. +* Partivally reverted the additional mouse cursors on button hovers. +* Changed SpawnAtPosition EntityCoordinates overload to use the attached entity's rotation, and also added a rotation override argument. +* Reduced the default ReallyBeIdle tick count from 25 to 5. + +### New features + +* Added DictionaryEquals helper method to compare elements to determine if 2 dictionaries are identical. +* Tracy integration is now supported for profiling. +* SubscribeLocalEvent and SubscribeNetworkEvent can now be replaced with the similarly named attributes on methods. +* Adds `AttachedAudioDespawnedEvent`, which is raised against the parent of a despawning `AudioComponent`. +* Added a DictionaryEquals extension method to check equality between two dictionaries. +* Added support for uploading .ftl files. Format is: //.ftl - Example: /TestUpload/en-US.ftl , you can have multiple files, so long they are on different subfolders, they will all be loaded. + +### Bugfixes + +* Fix IsHardCollidable mask check. +* Fix Robust.Benchmarks not compiling in some instances. +* Fix ApplyLinearImpulse not correctly using world-space. +* Fix OnClientRequestFull throwing an error when logging deleted entities. + +### Internal + +* Unnecessary prototypemanager dependencies were removed from engine systems. +* Added a 30m timeout to engine test workflows. +* Cleaned up ContainerSystems and PlayerManagers code-files. + + ## 278.0.0 ### Breaking changes From 792f795fbed1ef950885335babe3cfe33f11959b Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:07:41 +1000 Subject: [PATCH 101/178] Sourcegen eventsub fixes (#6702) --- RELEASE-NOTES.md | 4 ++-- Robust.Client/Audio/AudioSystem.cs | 8 ++++---- .../KnownTypes.cs | 4 ++-- .../EntitySystemSubscriptionsGeneratorAttributes.cs | 6 +++++- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b6422e3d6a7..81757f46966 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,11 +39,11 @@ END TEMPLATE--> ### New features -* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! +*None yet* ### Bugfixes -*None yet* +* Fix SubscribeLocalEvent name and added `MeansImplicitUse` attribute to the sourcegenned eventbus methods. ### Other diff --git a/Robust.Client/Audio/AudioSystem.cs b/Robust.Client/Audio/AudioSystem.cs index d78ad656985..0eb1f65bdc3 100644 --- a/Robust.Client/Audio/AudioSystem.cs +++ b/Robust.Client/Audio/AudioSystem.cs @@ -136,7 +136,7 @@ private void OnAudioTickRate(int obj) _audioFrameTimeRemaining = MathF.Min(_audioFrameTimeRemaining, _audioFrameTime); } - [SubcribeLocalEvent] + [SubscribeLocalEvent] private void OnAudioState(Entity entity, ref AfterAutoHandleStateEvent args) { var component = entity.Comp; @@ -202,7 +202,7 @@ public void SetMasterVolume(float value) _audio.SetMasterGain(value); } - [SubcribeLocalEvent] + [SubscribeLocalEvent] private void OnAudioPaused(EntityUid uid, AudioComponent component, ref EntityPausedEvent args) { component.Pause(); @@ -214,7 +214,7 @@ protected override void OnAudioUnpaused(EntityUid uid, AudioComponent component, component.StartPlaying(); } - [SubcribeLocalEvent] + [SubscribeLocalEvent] private void OnAudioStartup(EntityUid uid, AudioComponent component, ComponentStartup args) { if (!Timing.ApplyingState && !Timing.IsFirstTimePredicted) @@ -286,7 +286,7 @@ private void SetupSource(Entity entity, AudioResource audioResou } } - [SubcribeLocalEvent] + [SubscribeLocalEvent] private void OnAudioShutdown(EntityUid uid, AudioComponent component, ComponentShutdown args) { // Breaks with prediction? diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs index aec57705327..ad6606d6cbd 100644 --- a/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs +++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/KnownTypes.cs @@ -9,10 +9,10 @@ public static class KnownTypes public const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent"; public const string LocalSubscriptionMemberAttributeName = - "Robust.Shared.Analyzers.LocalEventSubscriptionAttribute"; + "Robust.Shared.Analyzers.SubscribeLocalEventAttribute"; public const string NetworkSubscriptionMemberAttributeName = - "Robust.Shared.Analyzers.NetworkEventSubscriptionAttribute"; + "Robust.Shared.Analyzers.SubscribeNetworkEventAttribute"; public const string AllSubscriptionMemberAttributeName = "Robust.Shared.Analyzers.EventSubscriptionAttribute"; diff --git a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs index 9587cb23f7c..3c920aa0b7c 100644 --- a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs +++ b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs @@ -1,4 +1,5 @@ using System; +using JetBrains.Annotations; using Robust.Shared.GameObjects; namespace Robust.Shared.Analyzers; @@ -20,7 +21,8 @@ namespace Robust.Shared.Analyzers; ///
/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeLocalEvent. [AttributeUsage(AttributeTargets.Method)] -public sealed class SubcribeLocalEventAttribute : Attribute; +[MeansImplicitUse] +public sealed class SubscribeLocalEventAttribute : Attribute; /// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with /// this attribute will have a EntitySystem.SubscribeNetworkEvent call generated, using the method as the handler, @@ -34,6 +36,7 @@ public sealed class SubcribeLocalEventAttribute : Attribute; ///
/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeNetworkEvent. [AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] public sealed class SubscribeNetworkEventAttribute : Attribute; /// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with @@ -48,4 +51,5 @@ public sealed class SubscribeNetworkEventAttribute : Attribute; ///
/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeAllEvent. [AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] public sealed class EventSubscriptionAttribute : Attribute; From fb70123a98afb3cb313ed0e24dc46075b683e93d Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Wed, 1 Jul 2026 14:08:06 +1000 Subject: [PATCH 102/178] Version: 279.0.1 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 7e1e1e159ae..198c64765d7 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 279.0.0 + 279.0.1 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 81757f46966..33e5b382f60 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,11 +39,11 @@ END TEMPLATE--> ### New features -*None yet* +* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! ### Bugfixes -* Fix SubscribeLocalEvent name and added `MeansImplicitUse` attribute to the sourcegenned eventbus methods. +*None yet* ### Other @@ -54,6 +54,13 @@ END TEMPLATE--> *None yet* +## 279.0.1 + +### Bugfixes + +* Fix SubscribeLocalEvent name and added `MeansImplicitUse` attribute to the sourcegenned eventbus methods. + + ## 279.0.0 ### Breaking changes From 80560814fcede4daaeb0aaa498a9767f4197e2ba Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:55:44 +1000 Subject: [PATCH 103/178] UiBox2i validation (#6665) * UIBox2i changes See Box2i + Box2 PRs. * RN * pipe bomb --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- RELEASE-NOTES.md | 2 +- Robust.Shared.Maths.Tests/UIBox2i_Test.cs | 340 +++++++++++--------- Robust.Shared.Maths/UIBox2i.cs | 365 ++++++++++++++-------- 3 files changed, 444 insertions(+), 263 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 33e5b382f60..3452613dab2 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Validate UIBox2i inputs ### New features diff --git a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs index 604d2c40363..f83843c3b7e 100644 --- a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs @@ -1,116 +1,152 @@ using NUnit.Framework; -namespace Robust.Shared.Maths.Tests +namespace Robust.Shared.Maths.Tests; + +[Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] +[TestFixture] +[TestOf(typeof(UIBox2i))] +internal sealed class UIBox2i_Test { - [Parallelizable(ParallelScope.All | ParallelScope.Fixtures)] - [TestFixture] - [TestOf(typeof(UIBox2i))] - internal sealed class UIBox2i_Test + private static IEnumerable<(int left, int top, int right, int bottom)> Sources => + [ + (0, 0, 0, 0), + (0, 0, 0, 10), + (0, 0, 10, 0), + (0, 0, 10, 10), + (0, -10, 0, 0), + (0, -10, 0, 10), + (0, -10, 10, 0), + (0, -10, 10, 10), + (-10, 0, 0, 0), + (-10, 0, 0, 10), + (-10, 0, 10, 0), + (-10, 0, 10, 10), + (-10, -10, 0, 0), + (-10, -10, 0, 10), + (-10, -10, 10, 0), + (-10, -10, 10, 10) + ]; + + private static IEnumerable<(int x, int y)> SmallTranslations => + [ + (0, 1), + (1, 0), + (1, 1), + (0, -1), + (1, -1), + (-1, 0), + (-1, 1), + (-1, -1) + ]; + + private static IEnumerable<(int x, int y)> LargeTranslations => + [ + (0, 20), + (20, 0), + (20, 20), + (0, -20), + (20, -20), + (-20, 0), + (-20, 20), + (-20, -20) + ]; + + private static IEnumerable<(UIBox2i a, UIBox2i b, UIBox2i? expected)> Intersections => + [ + (new UIBox2i(0, 0, 5, 5), new UIBox2i(2, 2, 4, 4), new UIBox2i(2, 2, 4, 4)), + (new UIBox2i(0, 0, 5, 5), new UIBox2i(3, 3, 7, 7), new UIBox2i(3, 3, 5, 5)), + (new UIBox2i(2, 0, 5, 5), new UIBox2i(0, 3, 4, 7), new UIBox2i(2, 3, 4, 5)), + (new UIBox2i(2, 0, 5, 5), new UIBox2i(6, 6, 10, 10), null) + ]; + + [Test] + public void Box2iVectorConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) { - private static IEnumerable<(int left, int top, int right, int bottom)> Sources => new (int, int, int, int)[] - { - (0, 0, 0, 0), - (0, 0, 0, 10), - (0, 0, 10, 0), - (0, 0, 10, 10), - (0, -10, 0, 0), - (0, -10, 0, 10), - (0, -10, 10, 0), - (0, -10, 10, 10), - (-10, 0, 0, 0), - (-10, 0, 0, 10), - (-10, 0, 10, 0), - (-10, 0, 10, 10), - (-10, -10, 0, 0), - (-10, -10, 0, 10), - (-10, -10, 10, 0), - (-10, -10, 10, 10) - }; - - private static IEnumerable<(int x, int y)> SmallTranslations => new (int, int)[] - { - (0, 1), - (1, 0), - (1, 1), - (0, -1), - (1, -1), - (-1, 0), - (-1, 1), - (-1, -1) - }; - - private static IEnumerable<(int x, int y)> LargeTranslations => new (int, int)[] - { - (0, 20), - (20, 0), - (20, 20), - (0, -20), - (20, -20), - (-20, 0), - (-20, 20), - (-20, -20) - }; - - private static IEnumerable<(UIBox2i a, UIBox2i b, UIBox2i? expected)> Intersections => - new (UIBox2i, UIBox2i, UIBox2i?)[] - { - (new UIBox2i(0, 0, 5, 5), new UIBox2i(2, 2, 4, 4), new UIBox2i(2, 2, 4, 4)), - (new UIBox2i(0, 0, 5, 5), new UIBox2i(3, 3, 7, 7), new UIBox2i(3, 3, 5, 5)), - (new UIBox2i(2, 0, 5, 5), new UIBox2i(0, 3, 4, 7), new UIBox2i(2, 3, 4, 5)), - (new UIBox2i(2, 0, 5, 5), new UIBox2i(6, 6, 10, 10), null), - }; - - [Test] - public void Box2iVectorConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; - var box = new UIBox2i(new Vector2i(left, top), new Vector2i(right, bottom)); + var (left, top, right, bottom) = test; + var box = new UIBox2i(new Vector2i(left, top), new Vector2i(right, bottom)); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(right)); Assert.That(box.Bottom, Is.EqualTo(bottom)); } + } - [Test] - public void Box2iEdgesConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; - var box = new UIBox2i(left, top, right, bottom); + [Test] + public void Box2iEdgesConstructor([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; + var box = new UIBox2i(left, top, right, bottom); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(right)); Assert.That(box.Bottom, Is.EqualTo(bottom)); } + } + + [Test] + public void Box2iValidatesConstruction() + { + Assert.Multiple(() => + { + Assert.Throws(() => new UIBox2i(3, 4, -1, -2)); + Assert.Throws(() => new UIBox2i(new Vector2i(3, 4), new Vector2i(-1, -2))); + }); + } - [Test] - public void Box2iCornerVectorProperties([ValueSource(nameof(Sources))] (int, int, int, int) test) + [Test] + public void Box2iValidatesProperties() + { + var box = new UIBox2i(-1, -2, 3, 4); + + using (Assert.EnterMultipleScope()) { - var (left, top, right, bottom) = test; - var box = new UIBox2i(left, top, right, bottom); + Assert.Throws(() => box.Left = 4); + Assert.Throws(() => box.Top = 5); + Assert.Throws(() => box.Right = -2); + Assert.Throws(() => box.Bottom = -3); + Assert.Throws(() => box.TopLeft = new Vector2i(4, 0)); + Assert.Throws(() => box.BottomRight = new Vector2i(0, -3)); + } + } - var br = new Vector2i(right, bottom); - var tl = new Vector2i(left, top); - var tr = new Vector2i(right, top); - var bl = new Vector2i(left, bottom); + [Test] + public void Box2iCornerVectorProperties([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; + var box = new UIBox2i(left, top, right, bottom); + + var br = new Vector2i(right, bottom); + var tl = new Vector2i(left, top); + var tr = new Vector2i(right, top); + var bl = new Vector2i(left, bottom); + using (Assert.EnterMultipleScope()) + { Assert.That(box.BottomRight, Is.EqualTo(br)); Assert.That(box.TopLeft, Is.EqualTo(tl)); Assert.That(box.TopRight, Is.EqualTo(tr)); Assert.That(box.BottomLeft, Is.EqualTo(bl)); } + } - [Test] - public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var width = Math.Abs(left - right); - var height = Math.Abs(top - bottom); + var width = Math.Abs(left - right); + var height = Math.Abs(top - bottom); - var box = UIBox2i.FromDimensions(left, top, width, height); + var box = UIBox2i.FromDimensions(left, top, width, height); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(left + width)); @@ -119,18 +155,21 @@ public void Box2iFromDimensionsInt([ValueSource(nameof(Sources))] (int, int, int Assert.That(box.Width, Is.EqualTo(width)); Assert.That(box.Height, Is.EqualTo(height)); } + } - [Test] - public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var width = Math.Abs(left - right); - var height = Math.Abs(top - bottom); - var size = new Vector2i(width, height); + var width = Math.Abs(left - right); + var height = Math.Abs(top - bottom); + var size = new Vector2i(width, height); - var box = UIBox2i.FromDimensions(new Vector2i(left, top), size); + var box = UIBox2i.FromDimensions(new Vector2i(left, top), size); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Left, Is.EqualTo(left)); Assert.That(box.Top, Is.EqualTo(top)); Assert.That(box.Right, Is.EqualTo(left + width)); @@ -138,95 +177,113 @@ public void Box2iFromDimensionsVectors([ValueSource(nameof(Sources))] (int, int, Assert.That(box.Size, Is.EqualTo(size)); } + } - [Test] - public void Box2iNotContainsSelfOpen() - { - var box = new UIBox2i(-1, -1, 1, 1); + [Test] + public void Box2iNotContainsSelfOpen() + { + var box = new UIBox2i(-1, -1, 1, 1); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(box.BottomLeft, false), Is.False); Assert.That(box.Contains(box.TopLeft, false), Is.False); Assert.That(box.Contains(box.TopRight, false), Is.False); Assert.That(box.Contains(box.BottomRight, false), Is.False); } + } - [Test] - public void Box2iContainsSelfClosed() - { - var box = new UIBox2i(-1, -1, 1, 1); + [Test] + public void Box2iContainsSelfClosed() + { + var box = new UIBox2i(-1, -1, 1, 1); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(box.BottomLeft)); Assert.That(box.Contains(box.TopLeft)); Assert.That(box.Contains(box.TopRight)); Assert.That(box.Contains(box.BottomRight)); + } - var bl = box.BottomLeft; - var tl = box.TopLeft; - var tr = box.TopRight; - var br = box.BottomRight; + var bl = box.BottomLeft; + var tl = box.TopLeft; + var tr = box.TopRight; + var br = box.BottomRight; + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(bl.X, bl.Y)); Assert.That(box.Contains(tl.X, tl.Y)); Assert.That(box.Contains(tr.X, tr.Y)); Assert.That(box.Contains(br.X, br.Y)); } + } - [Test] - public void Box2iContains([ValueSource(nameof(SmallTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iContains([ValueSource(nameof(SmallTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-2, -2, 2, 2); + var box = new UIBox2i(-2, -2, 2, 2); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(x, y)); Assert.That(box.Contains(vec)); Assert.That(box.Contains(vec, false)); } + } - [Test] - public void Box2iNotContains([ValueSource(nameof(LargeTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iNotContains([ValueSource(nameof(LargeTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-2, -2, 2, 2); + var box = new UIBox2i(-2, -2, 2, 2); + using (Assert.EnterMultipleScope()) + { Assert.That(box.Contains(x, y), Is.False); Assert.That(box.Contains(vec), Is.False); Assert.That(box.Contains(vec, false), Is.False); } + } - [Test] - public void Box2iTranslated([ValueSource(nameof(LargeTranslations))] - (int, int) test) - { - var (x, y) = test; - var vec = new Vector2i(x, y); + [Test] + public void Box2iTranslated([ValueSource(nameof(LargeTranslations))] (int, int) test) + { + var (x, y) = test; + var vec = new Vector2i(x, y); - var box = new UIBox2i(-1, -1, 1, 1); - var scaledBox = box.Translated(vec); + var box = new UIBox2i(-1, -1, 1, 1); + var scaledBox = box.Translated(vec); + using (Assert.EnterMultipleScope()) + { Assert.That(scaledBox.Left, Is.EqualTo(box.Left + x)); Assert.That(scaledBox.Top, Is.EqualTo(box.Top + y)); Assert.That(scaledBox.Bottom, Is.EqualTo(box.Bottom + y)); Assert.That(scaledBox.Right, Is.EqualTo(box.Right + x)); } + } - [Test] - public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test) - { - var (left, top, right, bottom) = test; + [Test] + public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test) + { + var (left, top, right, bottom) = test; - var controlBox = new UIBox2i(left, top, right, bottom); - var differentBox = new UIBox2i(-3, -3, 3, 3); - var sameBox = new UIBox2i(left, top, right, bottom); - Object sameBoxAsObject = sameBox; - UIBox2i? nullBox = null; - Vector2i notBox = new Vector2i(left, top); + var controlBox = new UIBox2i(left, top, right, bottom); + var differentBox = new UIBox2i(-3, -3, 3, 3); + var sameBox = new UIBox2i(left, top, right, bottom); + Object sameBoxAsObject = sameBox; + UIBox2i? nullBox = null; + Vector2i notBox = new Vector2i(left, top); + using (Assert.EnterMultipleScope()) + { Assert.That(controlBox.Equals(controlBox)); Assert.That(controlBox.Equals(differentBox), Is.False); Assert.That(controlBox.Equals(sameBox)); @@ -234,13 +291,16 @@ public void Box2iEquals([ValueSource(nameof(Sources))] (int, int, int, int) test Assert.That(controlBox.Equals(nullBox), Is.False); Assert.That(controlBox.Equals(notBox), Is.False); } + } - [Test] - public void UIBox2iIntersection( - [ValueSource(nameof(Intersections))] (UIBox2i a, UIBox2i b, UIBox2i? expected) value) - { - var (a, b, expected) = value; + [Test] + public void UIBox2iIntersection( + [ValueSource(nameof(Intersections))] (UIBox2i a, UIBox2i b, UIBox2i? expected) value) + { + var (a, b, expected) = value; + using (Assert.EnterMultipleScope()) + { // This should be a symmetric operation. Assert.That(a.Intersection(b), Is.EqualTo(expected)); Assert.That(b.Intersection(a), Is.EqualTo(expected)); diff --git a/Robust.Shared.Maths/UIBox2i.cs b/Robust.Shared.Maths/UIBox2i.cs index 2ccadf89b11..41cc573748e 100644 --- a/Robust.Shared.Maths/UIBox2i.cs +++ b/Robust.Shared.Maths/UIBox2i.cs @@ -4,165 +4,286 @@ using System.Runtime.InteropServices; using Robust.Shared.Utility; -namespace Robust.Shared.Maths +namespace Robust.Shared.Maths; + +[Serializable] +[StructLayout(LayoutKind.Explicit)] +public struct UIBox2i : IEquatable, ISpanFormattable { - [Serializable] - [StructLayout(LayoutKind.Explicit)] - public struct UIBox2i : IEquatable, ISpanFormattable - { - [FieldOffset(sizeof(int) * 0)] public int Left; - [FieldOffset(sizeof(int) * 1)] public int Top; - [FieldOffset(sizeof(int) * 2)] public int Right; - [FieldOffset(sizeof(int) * 3)] public int Bottom; - - [FieldOffset(sizeof(int) * 0)] public Vector2i TopLeft; - [FieldOffset(sizeof(int) * 2)] public Vector2i BottomRight; - - public readonly Vector2i TopRight => new(Right, Top); - public readonly Vector2i BottomLeft => new(Left, Bottom); - public readonly int Width => Math.Abs(Right - Left); - public readonly int Height => Math.Abs(Top - Bottom); - public readonly Vector2i Size => new(Width, Height); - public readonly Vector2 Center => new Vector2(Left + Right, Top + Bottom) / 2f; - public UIBox2i(Vector2i topLeft, Vector2i bottomRight) - { - Unsafe.SkipInit(out this); + [FieldOffset(sizeof(int) * 0)] internal int _left; + [FieldOffset(sizeof(int) * 1)] internal int _top; + [FieldOffset(sizeof(int) * 2)] internal int _right; + [FieldOffset(sizeof(int) * 3)] internal int _bottom; - TopLeft = topLeft; - BottomRight = bottomRight; - } + [FieldOffset(sizeof(int) * 0)] internal Vector2i _topLeft; + [FieldOffset(sizeof(int) * 2)] internal Vector2i _bottomRight; - public UIBox2i(int left, int top, int right, int bottom) + public int Left + { + readonly get => _left; + set { - Unsafe.SkipInit(out this); + if (value > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "Left cannot be greater than Right."); - Left = left; - Right = right; - Top = top; - Bottom = bottom; + _left = value; } + } - public static UIBox2i FromDimensions(int left, int top, int width, int height) + public int Top + { + readonly get => _top; + set { - return new(left, top, left + width, top + height); - } + if (value > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "Top cannot be greater than Bottom."); - public static UIBox2i FromDimensions(Vector2i position, Vector2i size) - { - return FromDimensions(position.X, position.Y, size.X, size.Y); + _top = value; } + } - public readonly bool Contains(int x, int y) + public int Right + { + readonly get => _right; + set { - return Contains(new Vector2i(x, y)); - } + if (value < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "Right cannot be less than Left."); - public readonly bool Contains(Vector2i point, bool closedRegion = true) - { - var xOk = closedRegion - ? point.X >= Left ^ point.X > Right - : point.X > Left ^ point.X >= Right; - var yOk = closedRegion - ? point.Y >= Top ^ point.Y > Bottom - : point.Y > Top ^ point.Y >= Bottom; - return xOk && yOk; + _right = value; } + } - /// Returns a UIBox2 translated by the given amount. - public readonly UIBox2i Translated(Vector2i point) + public int Bottom + { + readonly get => _bottom; + set { - return new(Left + point.X, Top + point.Y, Right + point.X, Bottom + point.Y); - } + if (value < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "Bottom cannot be less than Top."); - /// - /// Calculates the "intersection" of this and another box. - /// Basically, the smallest region that fits in both boxes. - /// - /// The box to calculate the intersection with. - /// - /// null if there is no intersection, otherwise the smallest region that fits in both boxes. - /// - public readonly UIBox2i? Intersection(in UIBox2i other) - { - if (!Intersects(other)) - { - return null; - } - - return new UIBox2i( - Vector2i.ComponentMax(TopLeft, other.TopLeft), - Vector2i.ComponentMin(BottomRight, other.BottomRight)); + _bottom = value; } + } - public readonly bool Intersects(in UIBox2i other) + public Vector2i TopLeft + { + readonly get => _topLeft; + set { - return other.Bottom >= this.Top && other.Top <= this.Bottom && other.Right >= this.Left && - other.Left <= this.Right; - } + if (value.X > _right) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.X cannot be greater than Right."); - // override object.Equals - public override readonly bool Equals(object? obj) - { - if (obj is UIBox2i box) - { - return Equals(box); - } + if (value.Y > _bottom) + throw new ArgumentOutOfRangeException(nameof(value), value, "TopLeft.Y cannot be greater than Bottom."); - return false; + _topLeft = value; } + } - public readonly bool Equals(UIBox2i other) + public Vector2i BottomRight + { + readonly get => _bottomRight; + set { - return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; - } + if (value.X < _left) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.X cannot be less than Left."); - // override object.GetHashCode - public override readonly int GetHashCode() - { - var code = Left.GetHashCode(); - code = (code * 929) ^ Right.GetHashCode(); - code = (code * 929) ^ Top.GetHashCode(); - code = (code * 929) ^ Bottom.GetHashCode(); - return code; - } + if (value.Y < _top) + throw new ArgumentOutOfRangeException(nameof(value), value, "BottomRight.Y cannot be less than Top."); - public static explicit operator UIBox2i(UIBox2 box) - { - return new((int) box.Left, (int) box.Top, (int) box.Right, (int) box.Bottom); + _bottomRight = value; } + } - public static implicit operator UIBox2(UIBox2i box) - { - return new(box.Left, box.Top, box.Right, box.Bottom); - } + public readonly Vector2i TopRight => new(Right, Top); - public static UIBox2i operator +(UIBox2i box, (int lo, int to, int ro, int bo) offsets) - { - var (lo, to, ro, bo) = offsets; + public readonly Vector2i BottomLeft => new(Left, Bottom); - return new UIBox2i(box.Left + lo, box.Top + to, box.Right + ro, box.Bottom + bo); - } + public readonly int Width => Right - Left; - public override readonly string ToString() - { - return $"({Left}, {Top}, {Right}, {Bottom})"; - } + public readonly int Height => Bottom - Top; + + public readonly Vector2i Size => new(Width, Height); + + public readonly Vector2 Center => new Vector2(_left + _right, _top + _bottom) / 2f; + + private static void Validate(int left, int top, int right, int bottom) + { + if (left > right) + throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); + + if (top > bottom) + throw new ArgumentException("Top cannot be greater than Bottom.", nameof(top)); + } + + public UIBox2i(Vector2i topLeft, Vector2i bottomRight) + { + Unsafe.SkipInit(out this); + + Validate(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); + + _topLeft = topLeft; + _bottomRight = bottomRight; + } + + public UIBox2i(int left, int top, int right, int bottom) + { + Unsafe.SkipInit(out this); + + Validate(left, top, right, bottom); + + _left = left; + _right = right; + _top = top; + _bottom = bottom; + } + + /// + /// Creates a UIBox2i with no bounds validation applied, use at your own risk. + /// + internal static UIBox2i DangerousCreate(int left, int top, int right, int bottom) + { + Unsafe.SkipInit(out UIBox2i box); + box._left = left; + box._right = right; + box._top = top; + box._bottom = bottom; + return box; + } + + public static UIBox2i FromDimensions(int left, int top, int width, int height) + { + return new UIBox2i(left, top, left + width, top + height); + } + + public static UIBox2i FromDimensions(Vector2i position, Vector2i size) + { + return FromDimensions(position.X, position.Y, size.X, size.Y); + } + + public readonly bool Contains(int x, int y) + { + return Contains(new Vector2i(x, y)); + } + + public readonly bool Contains(Vector2i point, bool closedRegion = true) + { + var xOk = closedRegion + ? point.X >= Left ^ point.X > Right + : point.X > Left ^ point.X >= Right; + var yOk = closedRegion + ? point.Y >= Top ^ point.Y > Bottom + : point.Y > Top ^ point.Y >= Bottom; + return xOk && yOk; + } + + /// Returns a UIBox2 translated by the given amount. + public readonly UIBox2i Translated(Vector2i point) + { + return new UIBox2i(Left + point.X, Top + point.Y, Right + point.X, Bottom + point.Y); + } - public readonly string ToString(string? format, IFormatProvider? formatProvider) + /// + /// Calculates the "intersection" of this and another box. + /// Basically, the smallest region that fits in both boxes. + /// + /// The box to calculate the intersection with. + /// + /// null if there is no intersection, otherwise the smallest region that fits in both boxes. + /// + public readonly UIBox2i? Intersection(in UIBox2i other) + { + if (!Intersects(other)) { - return ToString(); + return null; } - public readonly bool TryFormat( - Span destination, - out int charsWritten, - ReadOnlySpan format, - IFormatProvider? provider) + return new UIBox2i( + Vector2i.ComponentMax(TopLeft, other.TopLeft), + Vector2i.ComponentMin(BottomRight, other.BottomRight)); + } + + public readonly bool Intersects(in UIBox2i other) + { + return other._bottom >= _top + && other._top <= _bottom + && other._right >= _left + && other._left <= _right; + } + + // override object.Equals + public readonly override bool Equals(object? obj) + { + if (obj is UIBox2i box) { - return FormatHelpers.TryFormatInto( - destination, - out charsWritten, - $"({Left}, {Top}, {Right}, {Bottom})"); + return Equals(box); } + + return false; + } + + public readonly bool Equals(UIBox2i other) + { + return other.Left == Left && other.Right == Right && other.Bottom == Bottom && other.Top == Top; + } + + // override object.GetHashCode + public readonly override int GetHashCode() + { + var code = Left.GetHashCode(); + code = (code * 929) ^ Right.GetHashCode(); + code = (code * 929) ^ Top.GetHashCode(); + code = (code * 929) ^ Bottom.GetHashCode(); + return code; + } + + public static explicit operator UIBox2i(UIBox2 box) + { + return new UIBox2i((int) box.Left, (int) box.Top, (int) box.Right, (int) box.Bottom); + } + + public static implicit operator UIBox2(UIBox2i box) + { + return new UIBox2(box.Left, box.Top, box.Right, box.Bottom); + } + + public static bool operator ==(UIBox2i a, UIBox2i b) + { + return a.Equals(b); + } + + public static bool operator !=(UIBox2i a, UIBox2i b) + { + return !a.Equals(b); + } + + public static UIBox2i operator +(UIBox2i box, (int lo, int to, int ro, int bo) offsets) + { + var (lo, to, ro, bo) = offsets; + + return new UIBox2i(box.Left + lo, box.Top + to, box.Right + ro, box.Bottom + bo); + } + + public readonly override string ToString() + { + return $"({Left}, {Top}, {Right}, {Bottom})"; + } + + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString(); + } + + public readonly bool TryFormat( + Span destination, + out int charsWritten, + ReadOnlySpan format, + IFormatProvider? provider) + { + return FormatHelpers.TryFormatInto( + destination, + out charsWritten, + $"({Left}, {Top}, {Right}, {Bottom})"); } } From e3851fa3104be3f68b57d1c18150b00ef815b8c7 Mon Sep 17 00:00:00 2001 From: Hayden Date: Wed, 1 Jul 2026 20:02:46 -0600 Subject: [PATCH 104/178] Fix template (#6703) Oops --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 3452613dab2..f0979fb06cc 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -14,7 +14,7 @@ Don't change the format without looking at the script! ### New features -* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! +*None yet* ### Bugfixes From f2625bf5c5dba922184b66783f1ca6cf7279d8cb Mon Sep 17 00:00:00 2001 From: TemporalOroboros Date: Wed, 1 Jul 2026 19:32:17 -0700 Subject: [PATCH 105/178] Removes IMapManager (#6584) * Move MapManager queries to SharedMapSystem Moves all variants of FindGridsIntersecting/TryFindGridAt to SharedMapSystem Hollows out the MapManager methods and converts them into relays to SharedMapSystem * Move CreateGrid to SharedMapSystem Moves the functionality for CreateGrid and its variants to SharedMapSystem Hollows out the MapManager methods and converts them into relays for the SharedMapSystem methods Obsoletes them too Also moves over the GetAllMapGrids and GetAllGrids methods * Move RaiseOnTileChanged to SharedMapSystem Also moves the SuppressOnTileChanged flag member to SharedMapSystem Hollows out and obsoletes the MapManager versions * Move default value constants to SharedMapSystem * Converts map pausing events into LocalizedEntityCommands * Move MapManager related delegates/structs to SharedMapSystem Moves the GridCreationOptions struct to the same namespace as SharedMapSystem and converts it into a record struct Move the GridCallback delegates to the same namespace as SharedMapSystem * Move CullDeletionHistory to SharedMapSystem Well, that was less painful than I thought it would be * Actually obsolete the NetworkedMapManager method * Rename file * Doc comments for new SharedMapSystem methods * Doc comment * Doc comments * Fix access * Purge all references to IMapManagerInternal * Cull easy IMapManager references * The rest * Purge IMapManager * Private internal FindGridsIntersecting method * please die * 41 * notes --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- RELEASE-NOTES.md | 1 + .../Transform/RecursiveMoveBenchmark.cs | 3 +- .../Components/TransformComponentTests.cs | 9 +- Robust.Client/BaseClient.cs | 3 - Robust.Client/ClientIoC.cs | 3 - Robust.Client/Console/Commands/Debug.cs | 3 +- .../Debugging/DebugAnchoringSystem.cs | 3 +- Robust.Client/Debugging/DebugPhysicsSystem.cs | 14 +- .../Debugging/Overlays/TileDebugOverlay.cs | 5 +- .../GameController/GameController.cs | 4 +- .../EntitySystems/DebugLightTreeSystem.cs | 8 +- .../GridChunkBoundsDebugSystem.cs | 13 +- .../Graphics/Clyde/Clyde.GridRendering.cs | 11 +- Robust.Client/Graphics/Clyde/Clyde.cs | 1 - Robust.Client/Physics/GridFixtureSystem.cs | 9 +- Robust.Client/Placement/IPlacementManager.cs | 1 - Robust.Client/Placement/Modes/AlignTileAny.cs | 2 +- Robust.Client/Placement/PlacementManager.cs | 2 - Robust.Client/Placement/PlacementMode.cs | 5 +- .../DebugMonitorControls/DebugCoordsPanel.cs | 5 +- .../Editors/VVPropEditorEntityCoordinates.cs | 1 - .../GameObjects/Components/Container_Test.cs | 6 +- .../GameObjects/Components/Transform_Test.cs | 6 +- .../GameStates/DetachedParentTest.cs | 7 +- .../GameStates/MissingParentTest.cs | 1 - .../GameStates/PvsChunkTest.cs | 3 +- .../GameStates/PvsReEntryTest.cs | 1 - .../GameStates/PvsSystemTests.cs | 3 +- .../RobustServerSimulation.cs | 7 - Robust.Server/BaseServer.cs | 3 - Robust.Server/Physics/GridFixtureSystem.cs | 3 +- Robust.Server/Placement/PlacementManager.cs | 13 +- Robust.Server/ServerIoC.cs | 3 - .../EntityLookup_Test.cs | 31 +-- .../AutoIncludeSerializationTest.cs | 3 +- .../EntitySerialization/CategorizationTest.cs | 5 +- .../EntitySerialization/MapMergeTest.cs | 3 +- .../OrphanSerializationTest.cs | 5 +- .../GameObjects/ContainerTests.cs | 1 - .../GameObjects/DeferredEntityDeletionTest.cs | 1 - .../Systems/AnchoredSystemTests.cs | 9 +- .../GameObjects/TransformComponent_Tests.cs | 3 +- .../GameState/DeletionNetworkingTests.cs | 5 +- .../Map/EntityCoordinates_Tests.cs | 25 +- .../Map/GridCollision_Test.cs | 5 +- .../Map/GridContraction_Test.cs | 6 +- .../Map/GridFixtures_Tests.cs | 5 +- .../Map/GridMerge_Tests.cs | 9 +- .../Map/GridRotation_Tests.cs | 6 +- .../Map/GridSplit_Tests.cs | 43 ++- .../Map/MapGridMap_Tests.cs | 8 +- .../Map/MapGrid_Tests.cs | 27 +- .../Map/MapManager_Tests.cs | 83 ------ .../Map/MapPauseTests.cs | 13 +- .../Map/Query_Tests.cs | 5 +- .../Map/SingleTileRemoveTest.cs | 3 +- .../Physics/BroadphaseNetworkingTest.cs | 5 +- .../Physics/Broadphase_Test.cs | 19 +- .../Physics/CollisionWake_Test.cs | 3 +- .../Physics/GridDeletion_Test.cs | 8 +- .../Physics/GridMovement_Test.cs | 3 +- .../Physics/GridReparentVelocity_Test.cs | 4 +- .../Physics/JointDeletion_Test.cs | 1 - .../Physics/MapVelocity_Test.cs | 10 +- .../Physics/RayCast_Test.cs | 2 +- .../Physics/RecursiveUpdateTest.cs | 6 +- .../Physics/Stack_Test.cs | 1 - .../Prototypes/HotReloadTest.cs | 2 - .../Spawning/EntitySpawnHelpersTest.cs | 4 - .../TransformTests/GridTraversalTest.cs | 3 +- .../ScriptGlobalsShared.cs | 2 - .../ComponentTrees/ComponentTreeSystem.cs | 3 +- Robust.Shared/Console/Commands/MapCommands.cs | 3 +- .../Console/Commands/TeleportCommands.cs | 3 +- .../EntitySerialization/MapChunkSerializer.cs | 13 +- .../Transform/TransformComponent.cs | 4 +- Robust.Shared/GameObjects/EntityManager.cs | 3 +- .../Systems/EntityLookup.Queries.cs | 8 +- .../EntityLookupSystem.ComponentQueries.cs | 6 +- .../GameObjects/Systems/EntityLookupSystem.cs | 1 - .../Systems/SharedGridTraversalSystem.cs | 4 +- .../Systems/SharedMapSystem.Coordinates.cs | 2 +- .../Systems/SharedMapSystem.Grid.Queries.cs | 3 +- .../GameObjects/Systems/SharedMapSystem.cs | 2 - .../SharedTransformSystem.Component.cs | 12 +- .../Systems/SharedTransformSystem.cs | 1 - Robust.Shared/Map/Components/MapComponent.cs | 4 +- Robust.Shared/Map/CoordinatesExtensions.cs | 8 +- Robust.Shared/Map/EntityCoordinates.cs | 7 - Robust.Shared/Map/IMapManager.cs | 259 ------------------ Robust.Shared/Map/IMapManagerInternal.cs | 20 -- Robust.Shared/Map/MapId.cs | 1 - .../Map/MapManager.GridCollection.cs | 100 ------- Robust.Shared/Map/MapManager.MapCollection.cs | 85 ------ Robust.Shared/Map/MapManager.Pause.cs | 40 --- Robust.Shared/Map/MapManager.Queries.cs | 195 ------------- Robust.Shared/Map/MapManager.cs | 61 ----- Robust.Shared/Map/NetworkedMapManager.cs | 21 -- .../Physics/Systems/SharedBroadphaseSystem.cs | 13 +- Robust.UnitTesting/IIntegrationInstance.cs | 1 - Robust.UnitTesting/Pool/TestPair.Helpers.cs | 2 +- Robust.UnitTesting/RobustIntegrationTest.cs | 2 - Robust.UnitTesting/RobustUnitTest.cs | 3 - 103 files changed, 207 insertions(+), 1222 deletions(-) delete mode 100644 Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs delete mode 100644 Robust.Shared/Map/IMapManager.cs delete mode 100644 Robust.Shared/Map/IMapManagerInternal.cs delete mode 100644 Robust.Shared/Map/MapManager.GridCollection.cs delete mode 100644 Robust.Shared/Map/MapManager.MapCollection.cs delete mode 100644 Robust.Shared/Map/MapManager.Pause.cs delete mode 100644 Robust.Shared/Map/MapManager.Queries.cs delete mode 100644 Robust.Shared/Map/MapManager.cs delete mode 100644 Robust.Shared/Map/NetworkedMapManager.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f0979fb06cc..1c3acc246a0 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -36,6 +36,7 @@ END TEMPLATE--> ### Breaking changes * Validate UIBox2i inputs +* IMapManager has been completely nuked from the codebase. Almost all of its content-facing functionality was ported to `SharedMapSystem` in https://github.com/space-wizards/RobustToolbox/pull/6579 beforehand. ### New features diff --git a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs index 15acf565239..1f67c1a5483 100644 --- a/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs +++ b/Robust.Benchmarks/Transform/RecursiveMoveBenchmark.cs @@ -47,7 +47,6 @@ public void GlobalSetup() Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait(); - var mapMan = server.ResolveDependency(); _entMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -92,7 +91,7 @@ public void GlobalSetup() server.WaitPost(() => { var map = server.ResolveDependency().CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); var grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); _gridCoords = new EntityCoordinates(grid, .5f, .5f); diff --git a/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs b/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs index 0f5a3e3c289..1b774095a6f 100644 --- a/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs +++ b/Robust.Client.IntegrationTests/GameObjects/Components/TransformComponentTests.cs @@ -19,12 +19,13 @@ private static (ISimulation, EntityUid gridA, EntityUid gridB) SimulationFactor .NewSimulation() .InitializeInstance(); - var mapId = sim.Resolve().System().CreateMap(); - var mapManager = sim.Resolve(); + var entMan = sim.Resolve(); + var mapSys = entMan.System(); + var mapId = mapSys.CreateMap(); // Adds two grids to use in tests. - var gridA = mapManager.CreateGridEntity(mapId); - var gridB = mapManager.CreateGridEntity(mapId); + var gridA = mapSys.CreateGridEntity(mapId); + var gridB = mapSys.CreateGridEntity(mapId); return (sim, gridA, gridB); } diff --git a/Robust.Client/BaseClient.cs b/Robust.Client/BaseClient.cs index 428ab9859ce..4c8dbf89e2e 100644 --- a/Robust.Client/BaseClient.cs +++ b/Robust.Client/BaseClient.cs @@ -26,7 +26,6 @@ public sealed partial class BaseClient : IBaseClient, IPostInjectInit [Dependency] private IPlayerManager _playMan = default!; [Dependency] private IClientNetConfigurationManager _configManager = default!; [Dependency] private IClientEntityManager _entityManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IDiscordRichPresence _discord = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private IClientGameStateManager _gameStates = default!; @@ -247,7 +246,6 @@ private void OnNetDisconnect(object? sender, NetDisconnectedArgs args) private void GameStartedSetup() { _entityManager.Startup(); - _mapManager.Startup(); _timing.ResetSimTime(_timeBase); _timing.Paused = false; @@ -259,7 +257,6 @@ private void GameStoppedReset() _gameStates.Reset(); _playMan.Shutdown(); _entityManager.Shutdown(); - _mapManager.Shutdown(); _discord.ClearPresence(); Reset(); } diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs index 3df84845048..7a2ca8e14fe 100644 --- a/Robust.Client/ClientIoC.cs +++ b/Robust.Client/ClientIoC.cs @@ -66,9 +66,6 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle deps.Register(); deps.Register(); deps.Register(); - deps.Register(); - deps.Register(); - deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Client/Console/Commands/Debug.cs b/Robust.Client/Console/Commands/Debug.cs index 3d8c0ed0a0a..1d21498e123 100644 --- a/Robust.Client/Console/Commands/Debug.cs +++ b/Robust.Client/Console/Commands/Debug.cs @@ -718,7 +718,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) internal sealed partial class ChunkInfoCommand : LocalizedEntityCommands { - [Dependency] private IMapManager _map = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IInputManager _input = default!; [Dependency] private SharedMapSystem _mapSystem = default!; @@ -729,7 +728,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { var mousePos = _eye.PixelToMap(_input.MouseScreenPosition); - if (!_map.TryFindGridAt(mousePos, out var gridUid, out var grid)) + if (!_mapSystem.TryFindGridAt(mousePos, out var gridUid, out var grid)) { shell.WriteLine("No grid under your mouse cursor."); return; diff --git a/Robust.Client/Debugging/DebugAnchoringSystem.cs b/Robust.Client/Debugging/DebugAnchoringSystem.cs index 0446aa7b676..75c6d07b7e7 100644 --- a/Robust.Client/Debugging/DebugAnchoringSystem.cs +++ b/Robust.Client/Debugging/DebugAnchoringSystem.cs @@ -17,7 +17,6 @@ public sealed partial class DebugAnchoringSystem : EntitySystem { [Dependency] private IEyeManager _eyeManager = default!; [Dependency] private IInputManager _inputManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IUserInterfaceManager _userInterface = default!; [Dependency] private MapSystem _mapSystem = default!; @@ -64,7 +63,7 @@ public override void FrameUpdate(float frameTime) var mouseSpot = _inputManager.MouseScreenPosition; var spot = _eyeManager.PixelToMap(mouseSpot); - if (!_mapManager.TryFindGridAt(spot, out var gridUid, out var grid)) + if (!_mapSystem.TryFindGridAt(spot, out var gridUid, out var grid)) { _label.Text = string.Empty; _hovered = null; diff --git a/Robust.Client/Debugging/DebugPhysicsSystem.cs b/Robust.Client/Debugging/DebugPhysicsSystem.cs index d34944a415d..df1e357be7c 100644 --- a/Robust.Client/Debugging/DebugPhysicsSystem.cs +++ b/Robust.Client/Debugging/DebugPhysicsSystem.cs @@ -84,7 +84,7 @@ public sealed partial class DebugPhysicsSystem : SharedDebugPhysicsSystem [Dependency] private IOverlayManager _overlay = default!; [Dependency] private IEyeManager _eye = default!; [Dependency] private IInputManager _input = default!; - [Dependency] private IMapManager _map = default!; + [Dependency] private SharedMapSystem _map = default!; [Dependency] private IPlayerManager _player = default!; [Dependency] private IResourceCache _resourceCache = default!; @@ -103,13 +103,13 @@ public PhysicsDebugFlags Flags EntityManager, _eye, _input, - _map, _player, _resourceCache, this, _entityLookup, _physics, - _transform)); + _transform, + _map)); if (value == PhysicsDebugFlags.None) _overlay.RemoveOverlay(typeof(PhysicsDebugOverlay)); @@ -203,12 +203,12 @@ internal sealed class PhysicsDebugOverlay : Overlay private readonly IEntityManager _entityManager; private readonly IEyeManager _eyeManager; private readonly IInputManager _inputManager; - private readonly IMapManager _mapManager; private readonly IPlayerManager _playerManager; private readonly DebugPhysicsSystem _debugPhysicsSystem; private readonly EntityLookupSystem _lookup; private readonly SharedPhysicsSystem _physicsSystem; private readonly SharedTransformSystem _transformSystem; + private readonly SharedMapSystem _mapSystem; public override OverlaySpace Space => OverlaySpace.WorldSpace | OverlaySpace.ScreenSpace; @@ -219,17 +219,17 @@ internal sealed class PhysicsDebugOverlay : Overlay private HashSet _drawnJoints = new(); private List> _grids = new(); - public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IMapManager mapManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem, SharedTransformSystem transformSystem) + public PhysicsDebugOverlay(IEntityManager entityManager, IEyeManager eyeManager, IInputManager inputManager, IPlayerManager playerManager, IResourceCache cache, DebugPhysicsSystem system, EntityLookupSystem lookup, SharedPhysicsSystem physicsSystem, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) { _entityManager = entityManager; _eyeManager = eyeManager; _inputManager = inputManager; - _mapManager = mapManager; _playerManager = playerManager; _debugPhysicsSystem = system; _lookup = lookup; _physicsSystem = physicsSystem; _transformSystem = transformSystem; + _mapSystem = mapSystem; _font = new VectorFont(cache.GetResource("/EngineFonts/NotoSans/NotoSans-Regular.ttf"), 10); } @@ -293,7 +293,7 @@ private void DrawWorld(DrawingHandleWorld worldHandle, OverlayDrawArgs args) } _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, viewBounds, ref _grids); + _mapSystem.FindGridsIntersecting(mapId, viewBounds, ref _grids); foreach (var grid in _grids) { diff --git a/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs b/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs index 74779c8eccc..d3f1df1639c 100644 --- a/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs +++ b/Robust.Client/Debugging/Overlays/TileDebugOverlay.cs @@ -25,7 +25,6 @@ public abstract partial class TileDebugOverlay : Overlay, IPostInjectInit { [Dependency] protected IEntityManager Entity = default!; [Dependency] protected IEyeManager Eye = default!; - [Dependency] protected IMapManager MapMan = default!; [Dependency] protected IInputManager Input = default!; [Dependency] protected IUserInterfaceManager Ui = default!; [Dependency] protected IResourceCache Cache = default!; @@ -59,7 +58,7 @@ protected internal override void Draw(in OverlayDrawArgs args) if (args.Viewport.Eye?.Position.MapId is not {} map || map == MapId.Nullspace) return; - MapMan.FindGridsIntersecting(map, args.WorldBounds, ref Grids); + Map.FindGridsIntersecting(map, args.WorldBounds, ref Grids); foreach (var grid in Grids) { @@ -108,7 +107,7 @@ protected virtual void DrawTooltip(DrawingHandleScreen handle) var coords = viewport.PixelToMap(mousePos.Position); - if (!MapMan.TryFindGridAt(coords, out var grid, out var comp)) + if (!Map.TryFindGridAt(coords, out var grid, out var comp)) return; var local = Map.WorldToLocal(grid, comp, coords.Position); diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 1071ea0df48..52f29e12bcd 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -61,7 +61,6 @@ internal sealed partial class GameController : IGameControllerInternal [Dependency] private IXamlHotReloadManager _xamlHotReloadManager = default!; [Dependency] private IPrototypeManager _prototypeManager = default!; [Dependency] private IClientNetManager _networkManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IStateManager _stateManager = default!; [Dependency] private IUserInterfaceManagerInternal _userInterfaceManager = default!; [Dependency] private IBaseClient _client = default!; @@ -148,7 +147,7 @@ internal bool StartupContinue(DisplayMode displayMode) { DebugTools.AssertNotNull(_resourceManifest); - _loadscr.Initialize(42); + _loadscr.Initialize(41); _loadscr.BeginLoadingSection("Init graphics", dontRender: true); _clyde.InitializePostWindowing(); @@ -236,7 +235,6 @@ internal bool StartupContinue(DisplayMode displayMode) _loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init"); _loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager); _loadscr.LoadingStep(_entityManager.Initialize, _entityManager); - _loadscr.LoadingStep(_mapManager.Initialize, _mapManager); _loadscr.LoadingStep(_gameStateManager.Initialize, _gameStateManager); _loadscr.LoadingStep(_placementManager.Initialize, _placementManager); _loadscr.LoadingStep(_viewVariablesManager.Initialize, _viewVariablesManager); diff --git a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs index cc41a5482ee..c50fa091e60 100644 --- a/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/DebugLightTreeSystem.cs @@ -27,8 +27,6 @@ public bool Enabled { _lightOverlay = new DebugLightOverlay( EntityManager.System(), - IoCManager.Resolve(), - IoCManager.Resolve(), EntityManager.System()); overlayManager.AddOverlay(_lightOverlay); @@ -46,18 +44,14 @@ public bool Enabled private sealed class DebugLightOverlay : Overlay { private EntityLookupSystem _lookup; - private IEyeManager _eyeManager; - private IMapManager _mapManager; private LightTreeSystem _trees; public override OverlaySpace Space => OverlaySpace.WorldSpace; - public DebugLightOverlay(EntityLookupSystem lookup, IEyeManager eyeManager, IMapManager mapManager, LightTreeSystem trees) + public DebugLightOverlay(EntityLookupSystem lookup, LightTreeSystem trees) { _lookup = lookup; - _eyeManager = eyeManager; - _mapManager = mapManager; _trees = trees; } diff --git a/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs b/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs index 1a60f7bd9e7..761244b9b5b 100644 --- a/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/GridChunkBoundsDebugSystem.cs @@ -4,7 +4,6 @@ using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Physics; @@ -15,8 +14,6 @@ namespace Robust.Client.GameObjects { public sealed partial class GridChunkBoundsDebugSystem : EntitySystem { - [Dependency] private IEyeManager _eyeManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] private TransformSystem _transform = default!; [Dependency] private SharedMapSystem _map = default!; @@ -37,8 +34,6 @@ public bool Enabled DebugTools.Assert(_overlay == null); _overlay = new GridChunkBoundsOverlay( EntityManager, - _eyeManager, - _mapManager, _transform, _map); @@ -58,8 +53,6 @@ public bool Enabled internal sealed class GridChunkBoundsOverlay : Overlay { private readonly IEntityManager _entityManager; - private readonly IEyeManager _eyeManager; - private readonly IMapManager _mapManager; private readonly SharedTransformSystem _transformSystem; private readonly SharedMapSystem _mapSystem; @@ -67,11 +60,9 @@ internal sealed class GridChunkBoundsOverlay : Overlay private List> _grids = new(); - public GridChunkBoundsOverlay(IEntityManager entManager, IEyeManager eyeManager, IMapManager mapManager, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) + public GridChunkBoundsOverlay(IEntityManager entManager, SharedTransformSystem transformSystem, SharedMapSystem mapSystem) { _entityManager = entManager; - _eyeManager = eyeManager; - _mapManager = mapManager; _transformSystem = transformSystem; _mapSystem = mapSystem; } @@ -84,7 +75,7 @@ protected internal override void Draw(in OverlayDrawArgs args) var fixturesQuery = _entityManager.GetEntityQuery(); _grids.Clear(); - _mapManager.FindGridsIntersecting(currentMap, viewport, ref _grids); + _mapSystem.FindGridsIntersecting(currentMap, viewport, ref _grids); foreach (var grid in _grids) { var worldMatrix = _transformSystem.GetWorldMatrix(grid); diff --git a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs index 933eb05b774..d47ba808ec6 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.GridRendering.cs @@ -52,19 +52,18 @@ private void RenderTileEdgesChanges(bool value) private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBounds, IEye eye) { var mapId = eye.Position.MapId; - if (!_mapManager.MapExists(mapId)) + if (!_mapSystem.MapExists(mapId)) { // fall back to nullspace map mapId = MapId.Nullspace; } _grids.Clear(); - _mapManager.FindGridsIntersecting(mapId, worldBounds, ref _grids); + _mapSystem.FindGridsIntersecting(mapId, worldBounds, ref _grids); var requiresFlush = true; GLShaderProgram gridProgram = default!; var gridOverlays = GetOverlaysForSpace(OverlaySpace.WorldSpaceGrids); - var mapSystem = _entityManager.System(); foreach (var mapGrid in _grids) { @@ -86,7 +85,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun } gridProgram.SetUniform(UniIModelMatrix, _transformSystem.GetWorldMatrix(mapGrid)); - var enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + var enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); // Handle base texture updates. while (enumerator.MoveNext(out var chunk)) @@ -123,7 +122,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun // Handle edge sprites. if (_drawTileEdges) { - enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); while (enumerator.MoveNext(out var chunk)) { var datum = data[chunk.Indices]; @@ -132,7 +131,7 @@ private void _drawGrids(Viewport viewport, Box2 worldAABB, Box2Rotated worldBoun } } - enumerator = mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); + enumerator = _mapSystem.GetMapChunks(mapGrid.Owner, mapGrid.Comp, worldBounds); // Draw chunks while (enumerator.MoveNext(out var chunk)) diff --git a/Robust.Client/Graphics/Clyde/Clyde.cs b/Robust.Client/Graphics/Clyde/Clyde.cs index fc03dd5ed80..9cad353b87b 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.cs @@ -37,7 +37,6 @@ internal sealed partial class Clyde : IClydeInternal, IPostInjectInit, IEntityEv [Dependency] private IClydeTileDefinitionManager _tileDefinitionManager = default!; [Dependency] private ILightManager _lightManager = default!; [Dependency] private ILogManager _logManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IOverlayManager _overlayManager = default!; [Dependency] private IResourceCache _resourceCache = default!; [Dependency] private IResourceManager _resManager = default!; diff --git a/Robust.Client/Physics/GridFixtureSystem.cs b/Robust.Client/Physics/GridFixtureSystem.cs index 9907f81ef08..ac42b077369 100644 --- a/Robust.Client/Physics/GridFixtureSystem.cs +++ b/Robust.Client/Physics/GridFixtureSystem.cs @@ -13,7 +13,6 @@ namespace Robust.Client.Physics internal sealed partial class GridFixtureSystem : SharedGridFixtureSystem { [Dependency] private IOverlayManager _overlay = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedMapSystem _map = default!; @@ -29,7 +28,7 @@ public bool EnableDebug if (_enableDebug) { - var overlay = new GridSplitNodeOverlay(_mapManager, this, _transform, _map); + var overlay = new GridSplitNodeOverlay(this, _transform, _map); _overlay.AddOverlay(overlay); RaiseNetworkEvent(new RequestGridNodesMessage()); } @@ -72,14 +71,12 @@ private sealed class GridSplitNodeOverlay : Overlay { public override OverlaySpace Space => OverlaySpace.WorldSpace; - private readonly IMapManager _mapManager; private readonly GridFixtureSystem _system; private readonly SharedTransformSystem _transform; private readonly SharedMapSystem _map; - public GridSplitNodeOverlay(IMapManager mapManager, GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map) + public GridSplitNodeOverlay(GridFixtureSystem system, SharedTransformSystem transform, SharedMapSystem map) { - _mapManager = mapManager; _system = system; _transform = transform; _map = map; @@ -91,7 +88,7 @@ protected internal override void Draw(in OverlayDrawArgs args) var state = (_system, _transform, args.WorldBounds, worldHandle); - _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state, + _map.FindGridsIntersecting(args.MapId, args.WorldBounds, ref state, (EntityUid uid, MapGridComponent grid, ref (GridFixtureSystem system, SharedTransformSystem transform, Box2Rotated worldBounds, DrawingHandleWorld worldHandle) tuple) => { diff --git a/Robust.Client/Placement/IPlacementManager.cs b/Robust.Client/Placement/IPlacementManager.cs index 15b8f09167d..756521b2e1c 100644 --- a/Robust.Client/Placement/IPlacementManager.cs +++ b/Robust.Client/Placement/IPlacementManager.cs @@ -24,7 +24,6 @@ public interface IPlacementManager IEntityManager EntityManager { get; } IEyeManager EyeManager { get; } - IMapManager MapManager { get; } /// /// The direction to spawn the entity in (presently exposed for EntitySpawnWindow UI) diff --git a/Robust.Client/Placement/Modes/AlignTileAny.cs b/Robust.Client/Placement/Modes/AlignTileAny.cs index ff51aa89eb0..9523b229514 100644 --- a/Robust.Client/Placement/Modes/AlignTileAny.cs +++ b/Robust.Client/Placement/Modes/AlignTileAny.cs @@ -17,7 +17,7 @@ public override void AlignPlacementMode(ScreenCoordinates mouseScreen) // Go over diagonal size so when placing in a line it doesn't stop snapping. const float searchBoxSize = 2f; // size of search box in meters - MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(searchBoxSize, pManager.EntityManager, pManager.MapManager); + MouseCoords = ScreenToCursorGrid(mouseScreen).AlignWithClosestGridTile(searchBoxSize, pManager.EntityManager); var gridId = pManager.EntityManager.System().GetGrid(MouseCoords); diff --git a/Robust.Client/Placement/PlacementManager.cs b/Robust.Client/Placement/PlacementManager.cs index f2828a08ee7..b5ec4a6c9a3 100644 --- a/Robust.Client/Placement/PlacementManager.cs +++ b/Robust.Client/Placement/PlacementManager.cs @@ -33,7 +33,6 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I [Dependency] internal IPlayerManager PlayerManager = default!; [Dependency] internal IResourceCache ResourceCache = default!; [Dependency] private IReflectionManager _reflectionManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IGameTiming _time = default!; [Dependency] private IEyeManager _eyeManager = default!; [Dependency] internal IInputManager InputManager = default!; @@ -49,7 +48,6 @@ public sealed partial class PlacementManager : IPlacementManager, IDisposable, I public IEntityManager EntityManager => _entityManager; public IEyeManager EyeManager => _eyeManager; - public IMapManager MapManager => _mapManager; private ISawmill _sawmill = default!; diff --git a/Robust.Client/Placement/PlacementMode.cs b/Robust.Client/Placement/PlacementMode.cs index 8ca0de2bd48..810ec1f0220 100644 --- a/Robust.Client/Placement/PlacementMode.cs +++ b/Robust.Client/Placement/PlacementMode.cs @@ -201,7 +201,7 @@ public TileRef GetTileRef(EntityCoordinates coordinates) return gridUidOpt is { } gridUid && gridUid.IsValid() ? pManager.EntityManager.System().GetTileRef(gridUid, pManager.EntityManager.GetComponent(gridUid), MouseCoords) : new TileRef(gridUidOpt ?? EntityUid.Invalid, - MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System()), Tile.Empty); + MouseCoords.ToVector2i(pManager.EntityManager, pManager.EntityManager.System()), Tile.Empty); } public TextureResource GetSprite(string key) @@ -267,7 +267,8 @@ protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords) { var mapCoords = pManager.EyeManager.PixelToMap(coords.Position); var transformSys = pManager.EntityManager.System(); - if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out _)) + var mapSys = pManager.EntityManager.System(); + if (!mapSys.TryFindGridAt(mapCoords, out var gridUid, out _)) { return transformSys.ToCoordinates(mapCoords); } diff --git a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs index 86ff546fb2f..f4d643d564d 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugMonitorControls/DebugCoordsPanel.cs @@ -19,7 +19,6 @@ internal sealed partial class DebugCoordsPanel : PanelContainer [Dependency] private IInputManager _inputManager = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private IClyde _displayManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IBaseClient _baseClient = default!; private readonly StringBuilder _textBuilder = new(); @@ -76,7 +75,7 @@ protected override void FrameUpdate(FrameEventArgs args) var mapSystem = _entityManager.System(); var xformSystem = _entityManager.System(); - if (_mapManager.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid)) + if (mapSystem.TryFindGridAt(mouseWorldMap, out var mouseGridUid, out var mouseGrid)) { mouseGridPos = mapSystem.MapToGrid(mouseGridUid, mouseWorldMap); tile = mapSystem.GetTileRef(mouseGridUid, mouseGrid, mouseGridPos); @@ -86,7 +85,7 @@ protected override void FrameUpdate(FrameEventArgs args) mouseGridPos = new EntityCoordinates(mapSystem.GetMapOrInvalid(mouseWorldMap.MapId), mouseWorldMap.Position); tile = new TileRef(EntityUid.Invalid, - mouseGridPos.ToVector2i(_entityManager, _mapManager, xformSystem), Tile.Empty); + mouseGridPos.ToVector2i(_entityManager, xformSystem), Tile.Empty); } } } diff --git a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs index 91620f5b544..840cd81de8a 100644 --- a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs +++ b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs @@ -63,7 +63,6 @@ protected override Control MakeUI(object? value) void OnEntered(LineEdit.LineEditEventArgs e) { var gridVal = EntityUid.Parse(gridId.Text); - var mapManager = IoCManager.Resolve(); var xVal = float.Parse(x.Text, CultureInfo.InvariantCulture); var yVal = float.Parse(y.Text, CultureInfo.InvariantCulture); diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs index 87f39f7a05c..175551d45d5 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Container_Test.cs @@ -182,9 +182,11 @@ public void BaseContainer_InsertMap_False() public void BaseContainer_InsertGrid_False() { var sim = SimulationFactory(); - var containerSys = sim.Resolve().GetEntitySystem(); + var entMan = sim.Resolve(); + var mapSys = entMan.System(); + var containerSys = entMan.System(); - var grid = sim.Resolve().CreateGridEntity(new MapId(1)).Owner; + var grid = mapSys.CreateGridEntity(new MapId(1)).Owner; var entity = sim.SpawnEntity(null,_coords); var container = containerSys.MakeContainer(entity, "dummy"); diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs index 14f54d46477..6dcf87edb0c 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs @@ -19,7 +19,6 @@ internal sealed class Transform_Test : RobustUnitTest public override UnitTestProject Project => UnitTestProject.Server; private IEntityManager EntityManager = default!; - private IMapManager MapManager = default!; private SharedTransformSystem XformSystem => EntityManager.System(); const string Prototypes = @" @@ -47,7 +46,6 @@ public void Setup() IoCManager.Resolve().GenerateNetIds(); EntityManager = IoCManager.Resolve(); - MapManager = IoCManager.Resolve(); IoCManager.Resolve().Initialize(); var manager = IoCManager.Resolve(); @@ -60,8 +58,8 @@ public void Setup() mapSys.CreateMap(out MapA); mapSys.CreateMap(out MapB); - GridA = MapManager.CreateGridEntity(MapA); - GridB = MapManager.CreateGridEntity(MapB); + GridA = mapSys.CreateGridEntity(MapA); + GridB = mapSys.CreateGridEntity(MapB); //NOTE: The grids have not moved, so we can assert worldpos == localpos for the test } diff --git a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs index 2570fee2d5e..4ca55516e0c 100644 --- a/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/DetachedParentTest.cs @@ -26,7 +26,6 @@ public async Task TestDetachedParent() var mapSys = server.System(); var xformSys = server.System(); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -72,7 +71,7 @@ await server.WaitPost(() => map = mapSys.CreateMap(out mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); gridCoords = new EntityCoordinates(gridEnt, .5f, .5f); mapCoords = new EntityCoordinates(map, 200, 200); @@ -268,7 +267,7 @@ await server.WaitPost(() => await server.WaitPost(() => { map2 = mapSys.CreateMap(out mapId2); - var gridEnt = mapMan.CreateGridEntity(mapId2); + var gridEnt = mapSys.CreateGridEntity(mapId2); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid2Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid2 = gridEnt.Owner; @@ -341,7 +340,7 @@ await server.WaitPost(() => await server.WaitPost(() => { map3 = mapSys.CreateMap(out mapId3); - var gridEnt = mapMan.CreateGridEntity(mapId3); + var gridEnt = mapSys.CreateGridEntity(mapId3); mapSys.SetTile(gridEnt.Owner, gridEnt.Comp, Vector2i.Zero, new Tile(1)); var grid3Coords = new EntityCoordinates(gridEnt, .5f, .5f); grid3 = gridEnt.Owner; diff --git a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs index 069001bcae9..afaf366fc5e 100644 --- a/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/MissingParentTest.cs @@ -20,7 +20,6 @@ public async Task TestMissingParent() await using var pair = await StartConnectedPair(); var (client, server) = pair; - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); diff --git a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs index 3a94114343f..e4e92f9e779 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsChunkTest.cs @@ -19,7 +19,6 @@ public async Task TestGridMapChange() await using var pair = await StartConnectedPair(); var (client, server) = pair; - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -63,7 +62,7 @@ await server.WaitPost(() => mapCoords = new(map1, default); map2 = server.System().CreateMap(); - var gridComp = mapMan.CreateGridEntity(map2); + var gridComp = mapSys.CreateGridEntity(map2); grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); var gridCoords = new EntityCoordinates(grid, .5f, .5f); diff --git a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs index d92aa95fbcd..6344a734db0 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsReEntryTest.cs @@ -25,7 +25,6 @@ public async Task TestLossyReEntry() await using var pair = await StartConnectedPair(); var (client, server) = pair; - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); diff --git a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs index a6fcb607f46..d592ab07821 100644 --- a/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs +++ b/Robust.Server.IntegrationTests/GameStates/PvsSystemTests.cs @@ -22,7 +22,6 @@ public async Task TestMultipleIndexChange() await using var pair = await StartConnectedPair(); var (client, server) = pair; - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -42,7 +41,7 @@ public async Task TestMultipleIndexChange() await server.WaitPost(() => { map = server.System().CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = maps.CreateGridEntity(mapId); maps.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid = gridComp.Owner; }); diff --git a/Robust.Server.Testing/RobustServerSimulation.cs b/Robust.Server.Testing/RobustServerSimulation.cs index 66eb92d861d..5b6591eabd4 100644 --- a/Robust.Server.Testing/RobustServerSimulation.cs +++ b/Robust.Server.Testing/RobustServerSimulation.cs @@ -248,9 +248,6 @@ public ISimulation InitializeInstance() container.Register(); container.Register(); container.Register(); - container.Register(); - container.Register(); - container.Register(); container.Register(); container.Register(); container.Register(); @@ -341,11 +338,7 @@ public ISimulation InitializeInstance() _systemDelegate?.Invoke(entitySystemMan); - var mapManager = container.Resolve(); - mapManager.Initialize(); - entityMan.Startup(); - mapManager.Startup(); container.Resolve().Initialize(true); container.Resolve().Initialize(); diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 7275de37265..65c1d2b221e 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -80,7 +80,6 @@ internal sealed partial class BaseServer : IBaseServerInternal, IPostInjectInit [Dependency] private IRobustSerializer _serializer = default!; [Dependency] private IGameTiming _time = default!; [Dependency] private IResourceManagerInternal _resources = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private ITimerManager _timerManager = default!; [Dependency] private IServerGameStateManager _stateManager = default!; [Dependency] private IServerNetManager _network = default!; @@ -379,7 +378,6 @@ public bool Start(ServerOptions options, Func? logHandlerFactory = _log.GetSawmill("res")); _entityManager.Initialize(); - _mapManager.Initialize(); _serialization.Initialize(); @@ -397,7 +395,6 @@ public bool Start(ServerOptions options, Func? logHandlerFactory = IoCManager.Resolve().Initialize(); _consoleHost.Initialize(); _entityManager.Startup(); - _mapManager.Startup(); _stateManager.Initialize(); _replay.Initialize(); diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs index 873e2e7c31e..159a4ca15c1 100644 --- a/Robust.Server/Physics/GridFixtureSystem.cs +++ b/Robust.Server/Physics/GridFixtureSystem.cs @@ -25,7 +25,6 @@ namespace Robust.Server.Physics /// public sealed partial class GridFixtureSystem : SharedGridFixtureSystem { - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private IConGroupController _conGroup = default!; [Dependency] private EntityLookupSystem _lookup = default!; @@ -262,7 +261,7 @@ private void CheckSplits(EntityUid uid, HashSet dirtyNodes) for (var i = 0; i < grids.Count - 1; i++) { var group = grids[i]; - var newGrid = _mapManager.CreateGridEntity(mapId); + var newGrid = _maps.CreateGridEntity(mapId); var newGridUid = newGrid.Owner; var newGridXform = _xformQuery.GetComponent(newGridUid); newGrids[i] = newGridUid; diff --git a/Robust.Server/Placement/PlacementManager.cs b/Robust.Server/Placement/PlacementManager.cs index a7868c8730a..f4d426f669b 100644 --- a/Robust.Server/Placement/PlacementManager.cs +++ b/Robust.Server/Placement/PlacementManager.cs @@ -28,7 +28,6 @@ public sealed partial class PlacementManager : IPlacementManager [Dependency] private IPlayerManager _playerManager = default!; [Dependency] private IPrototypeManager _prototype = default!; [Dependency] private IServerEntityManager _entityManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private ILogManager _logManager = default!; private EntityLookupSystem _lookup => _entityManager.System(); @@ -190,27 +189,29 @@ private void PlaceNewTile(int tileType, EntityCoordinates coordinates, NetUserId { if (!coordinates.IsValid(_entityManager)) return; + var mapSystem = _maps; + MapGridComponent? grid; EntityUid gridId = coordinates.EntityId; if (_entityManager.TryGetComponent(coordinates.EntityId, out grid) - || _mapManager.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid)) + || mapSystem.TryFindGridAt(_xformSystem.ToMapCoordinates(coordinates), out gridId, out grid)) { - _maps.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); + mapSystem.SetTile(gridId, grid, coordinates, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId); _entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent); } else if (tileType != 0) // create a new grid { - var newGrid = _mapManager.CreateGridEntity(_xformSystem.GetMapId(coordinates)); + var newGrid = mapSystem.CreateGridEntity(_xformSystem.GetMapId(coordinates)); var newGridXform = new Entity( newGrid.Owner, _entityManager.GetComponent(newGrid)); _xformSystem.SetWorldPosition(newGridXform, coordinates.Position - newGrid.Comp.TileSizeHalfVector); // assume bottom left tile origin - var tilePos = _maps.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position); - _maps.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); + var tilePos = mapSystem.WorldToTile(newGrid.Owner, newGrid.Comp, coordinates.Position); + mapSystem.SetTile(newGrid.Owner, newGrid.Comp, tilePos, new Tile(tileType, rotationMirroring: (byte)(direction + (mirrored ? 4 : 0)))); var placementEraseEvent = new PlacementTileEvent(tileType, coordinates, placingUserId); _entityManager.EventBus.RaiseEvent(EventSource.Local, placementEraseEvent); diff --git a/Robust.Server/ServerIoC.cs b/Robust.Server/ServerIoC.cs index 2c26ca5df06..9bda629e414 100644 --- a/Robust.Server/ServerIoC.cs +++ b/Robust.Server/ServerIoC.cs @@ -56,9 +56,6 @@ internal static void RegisterIoC(IDependencyCollection deps) deps.Register(); deps.Register(); deps.Register(); - deps.Register(); - deps.Register(); - deps.Register(); deps.Register(); deps.Register(); deps.Register(); diff --git a/Robust.Shared.IntegrationTests/EntityLookup_Test.cs b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs index cd06309a60f..a6e4b60aa33 100644 --- a/Robust.Shared.IntegrationTests/EntityLookup_Test.cs +++ b/Robust.Shared.IntegrationTests/EntityLookup_Test.cs @@ -65,9 +65,9 @@ private EntityUid GetPhysicsEntity(IEntityManager entManager, MapCoordinates spa return ent; } - private Entity SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager, IMapManager mapManager) + private Entity SetupGrid(MapId mapId, SharedMapSystem mapSystem, IEntityManager entManager) { - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); entManager.System().SetLocalPosition(grid.Owner, new Vector2(10f, 10f)); mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); return grid; @@ -88,11 +88,10 @@ public void TestEntityAnyIntersecting(bool physics, MapCoordinates spawnPos, Box var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -113,11 +112,10 @@ public void TestEntityAnyLocalIntersecting(bool physics, MapCoordinates spawnPos var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -141,11 +139,10 @@ public void TestEntityGridLocalIntersecting(bool physics, MapCoordinates spawnPo var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -170,11 +167,10 @@ public void TestEntityGridTileIntersecting(bool physics, MapCoordinates spawnPos var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -221,11 +217,10 @@ public void TestGridIntersecting(bool physics, MapCoordinates spawnPos, MapCoord var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -247,11 +242,10 @@ public void TestGridInRange(bool physics, MapCoordinates spawnPos, MapCoordinate var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -295,11 +289,10 @@ public void TestGridAnyIntersecting(bool physics, MapCoordinates spawnPos, Box2 var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -323,11 +316,10 @@ public void TestGridLocalIntersecting(bool physics, MapCoordinates spawnPos, Box var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); mapSystem.CreateMap(spawnPos.MapId); - var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager, mapManager); + var grid = SetupGrid(spawnPos.MapId, mapSystem, entManager); if (physics) GetPhysicsEntity(entManager, spawnPos); @@ -354,12 +346,11 @@ public void TestAnchoring() var lookup = server.Resolve().GetEntitySystem(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); var transformSystem = entManager.System(); var mapId = server.CreateMap().MapId; - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var theMapSpotBeingUsed = new Box2(Vector2.Zero, Vector2.One); mapSystem.SetTile(grid, new Vector2i(), new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs index 3e4b12c25d4..f049e1dfd37 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/AutoIncludeSerializationTest.cs @@ -34,7 +34,6 @@ public async Task TestAutoIncludeSerialization() var entMan = server.EntMan; var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var mapPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_map.yml"); var gridPath = new ResPath($"{nameof(AutoIncludeSerializationTest)}_grid.yml"); @@ -55,7 +54,7 @@ public async Task TestAutoIncludeSerialization() await server.WaitPost(() => { var mapUid = mapSys.CreateMap(out mapId); - var gridUid = mapMan.CreateGridEntity(mapId); + var gridUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridUid, Vector2i.Zero, new Tile(tDef.TileId)); var onGridUid = entMan.SpawnEntity(null, new EntityCoordinates(gridUid, 0.5f, 0.5f)); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs index 5b48ec6c1e6..aee367cbd32 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/CategorizationTest.cs @@ -35,7 +35,6 @@ public async Task TestCategorization() var meta = server.System(); var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var path = new ResPath($"{nameof(TestCategorization)}.yml"); @@ -56,8 +55,8 @@ await server.WaitPost(() => { mapA = mapSys.CreateMap(out var mapIdA); mapB = mapSys.CreateMap(out var mapIdB); - var gridEntA = mapMan.CreateGridEntity(mapIdA); - var gridEntB = mapMan.CreateGridEntity(mapIdB); + var gridEntA = mapSys.CreateGridEntity(mapIdA); + var gridEntB = mapSys.CreateGridEntity(mapIdB); mapSys.SetTile(gridEntA, Vector2i.Zero, new Tile(tDef.TileId)); mapSys.SetTile(gridEntB, Vector2i.Zero, new Tile(tDef.TileId)); gridA = gridEntA.Owner; diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs index 97f2d94202f..aad38625cf4 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/MapMergeTest.cs @@ -35,7 +35,6 @@ public async Task TestMapMerge() var entMan = server.EntMan; var mapSys = server.System(); var loader = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var mapPath = new ResPath($"{nameof(TestMapMerge)}_map.yml"); @@ -52,7 +51,7 @@ public async Task TestMapMerge() await server.WaitPost(() => { var mapUid = mapSys.CreateMap(out mapId, runMapInit: false); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridEnt, Vector2i.Zero, new Tile(tDef.TileId)); var entUid = entMan.SpawnEntity(null, new MapCoordinates(10, 10, mapId)); map = Get(mapUid, entMan); diff --git a/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs index 2cbb7f1db82..518abaf559f 100644 --- a/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs +++ b/Robust.Shared.IntegrationTests/EntitySerialization/OrphanSerializationTest.cs @@ -129,7 +129,6 @@ public async Task TestOrphanedGridSerialization() var mapSys = server.System(); var loader = server.System(); var xform = server.System(); - var mapMan = server.ResolveDependency(); var tileMan = server.ResolveDependency(); var pathA = new ResPath($"{nameof(TestOrphanedGridSerialization)}_A.yml"); var pathB = new ResPath($"{nameof(TestOrphanedGridSerialization)}_B.yml"); @@ -150,12 +149,12 @@ await server.WaitPost(() => var mapUid = mapSys.CreateMap(out mapId); map = Get(mapUid, entMan); - var gridAUid = mapMan.CreateGridEntity(mapId); + var gridAUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridAUid, Vector2i.Zero, new Tile(tDef.TileId)); gridA = Get(gridAUid, entMan); xform.SetLocalPosition(gridA.Owner, new(100, 100)); - var gridBUid = mapMan.CreateGridEntity(mapId); + var gridBUid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridBUid, Vector2i.Zero, new Tile(tDef.TileId)); gridB = Get(gridBUid, entMan); diff --git a/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs index 28b38f3f78b..8b52fba8d78 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/ContainerTests.cs @@ -161,7 +161,6 @@ public async Task TestContainerExpectedEntityDeleted() var clientTime = client.ResolveDependency(); var clientNetManager = client.ResolveDependency(); - var sMapManager = server.ResolveDependency(); var sEntManager = server.ResolveDependency(); var sPlayerManager = server.ResolveDependency(); var serverTime = server.ResolveDependency(); diff --git a/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs index 5bb02e61d2c..1b354c9d81c 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/DeferredEntityDeletionTest.cs @@ -39,7 +39,6 @@ public async Task TestDeferredEntityDeletion() await server.WaitAssertion(() => { - var mapMan = IoCManager.Resolve(); entMan = IoCManager.Resolve(); var sys = entMan.EntitySysManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs index 9f04c1f2d55..d3abfc2e0e1 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs @@ -38,14 +38,14 @@ private static (ISimulation, Entity grid, MapCoordinates, Shar }) .InitializeInstance(); - var mapManager = sim.Resolve(); + var mapSystem = sim.System(); var testMapId = sim.CreateMap().MapId; var coords = new MapCoordinates(new Vector2(7, 7), testMapId); // Add grid 1, as the default grid to anchor things to. - var grid = mapManager.CreateGridEntity(testMapId); + var grid = mapSystem.CreateGridEntity(testMapId); - return (sim, grid, coords, sim.System(), sim.System()); + return (sim, grid, coords, sim.System(), mapSystem); } // An entity is anchored to the tile it is over on the target grid. @@ -160,9 +160,8 @@ public void OnInitAnchored_AddedToLookup() var mapSys = sim.System(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().MapId; - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); var coordinates = new MapCoordinates(new Vector2(7, 7), mapId); var pos = mapSys.TileIndicesFor(grid, coordinates); mapSys.SetTile(grid, pos, new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs index aeb951a1155..01f0559e625 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs @@ -54,12 +54,11 @@ public void AttachToGridOrMap() var server = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = server.Resolve(); - var mapManager = server.Resolve(); var mapSystem = entManager.System(); var xformSystem = entManager.System(); mapSystem.CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(grid, new Vector2i(0, 0), new Tile(1)); xformSystem.SetLocalPosition(grid, new Vector2(0f, 100f)); diff --git a/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs index 48b050625d0..86c4de18a61 100644 --- a/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs +++ b/Robust.Shared.IntegrationTests/GameState/DeletionNetworkingTests.cs @@ -28,7 +28,6 @@ public async Task DeletionNetworkingTest() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); @@ -61,13 +60,13 @@ async Task RunTicks() await server.WaitPost(() => { mapSys.CreateMap(out var mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid1 = gridComp.Owner; xformSys.SetLocalPosition(grid1, new Vector2(-2,0)); grid1Net = sEntMan.GetNetEntity(grid1); - gridComp = mapMan.CreateGridEntity(mapId); + gridComp = mapSys.CreateGridEntity(mapId); mapSys.SetTile(gridComp, Vector2i.Zero, new Tile(1)); grid2 = gridComp.Owner; xformSys.SetLocalPosition(grid2, new Vector2(2,0)); diff --git a/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs index f43ccb44fcd..c2c3035dab4 100644 --- a/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/EntityCoordinates_Tests.cs @@ -46,7 +46,6 @@ public void IsValid_InvalidEntId_False() public void IsValid_EntityDeleted_False() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); var mapEntity = entityManager.System().CreateMap(out var mapId); var newEnt = entityManager.CreateEntityUninitialized(null, new MapCoordinates(Vector2.Zero, mapId)); @@ -122,11 +121,11 @@ public void GetGridId_Map() public void GetGridId_Grid() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero)); @@ -152,11 +151,11 @@ public void GetMapId_Map() public void GetMapId_Grid() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(gridEnt, Vector2.Zero)); @@ -168,11 +167,11 @@ public void GetMapId_Grid() public void GetParent() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); @@ -190,11 +189,11 @@ public void GetParent() public void TryGetParent() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); @@ -241,11 +240,11 @@ public void ToMap_MoveGrid(float x1, float y1, float x2, float y2) var entPos = new Vector2(x2, y2); var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, entPos)); var newXform = entityManager.GetComponent(newEnt); @@ -263,11 +262,11 @@ public void ToMap_MoveGrid(float x1, float y1, float x2, float y2) public void WithEntityId() { var entityManager = IoCManager.Resolve(); - var mapManager = IoCManager.Resolve(); + var mapSystem = entityManager.System(); var xformSys = entityManager.System(); var mapEnt = entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var newEnt = entityManager.CreateEntityUninitialized(null, new EntityCoordinates(grid, Vector2.Zero)); var newEntXform = entityManager.GetComponent(newEnt); diff --git a/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs index b3726f19964..f2c6294ef17 100644 --- a/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridCollision_Test.cs @@ -19,7 +19,6 @@ public async Task TestGridsCollide() await server.WaitIdleAsync(); - var mapManager = server.ResolveDependency(); var entManager = server.ResolveDependency(); var physSystem = server.ResolveDependency().GetEntitySystem(); var mapSystem = server.ResolveDependency().GetEntitySystem(); @@ -35,8 +34,8 @@ public async Task TestGridsCollide() await server.WaitPost(() => { entManager.System().CreateMap(out mapId); - gridId1 = mapManager.CreateGridEntity(mapId); - gridId2 = mapManager.CreateGridEntity(mapId); + gridId1 = mapSystem.CreateGridEntity(mapId); + gridId2 = mapSystem.CreateGridEntity(mapId); gridEnt1 = gridId1.Value.Owner; gridEnt2 = gridId2.Value.Owner; physics1 = entManager.GetComponent(gridEnt1.Value); diff --git a/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs index a2eb87253f6..3256bd6abd6 100644 --- a/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs +++ b/Robust.Shared.IntegrationTests/Map/GridContraction_Test.cs @@ -17,13 +17,12 @@ public async Task TestGridDeletes() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEntity = grid.Owner; for (var i = 0; i < 10; i++) @@ -56,13 +55,12 @@ public async Task TestGridNoDeletes() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.System(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); for (var i = 0; i < 10; i++) { diff --git a/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs index f7374c83b6e..34c12cd20b1 100644 --- a/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridFixtures_Tests.cs @@ -29,8 +29,8 @@ public void TestGridFixtureDeletion() var server = RobustServerSimulation.NewSimulation().InitializeInstance(); var map = server.CreateMap(); var entManager = server.Resolve(); - var grid = server.Resolve().CreateGridEntity(map.MapId); var mapSystem = entManager.System(); + var grid = mapSystem.CreateGridEntity(map.MapId); var fixtures = entManager.GetComponent(grid); mapSystem.SetTiles(grid, new List<(Vector2i GridIndices, Tile Tile)>() @@ -57,14 +57,13 @@ public async Task TestGridFixtures() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var physSystem = server.ResolveDependency().GetEntitySystem(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); // Should be nothing if grid empty Assert.That(entManager.TryGetComponent(grid, out PhysicsComponent? gridBody)); diff --git a/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs index 0ff7aa9c4c9..fc70be68010 100644 --- a/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridMerge_Tests.cs @@ -40,14 +40,13 @@ private ISimulation GetSim() public void Merge(Vector2i offset, Angle angle, Box2 bounds) { var sim = GetSim(); - var mapManager = sim.Resolve(); var entMan = sim.Resolve(); var mapSystem = entMan.System(); var gridFixtures = entMan.System(); var mapId = sim.CreateMap().MapId; - var grid1 = mapManager.CreateGridEntity(mapId); - var grid2 = mapManager.CreateGridEntity(mapId); + var grid1 = mapSystem.CreateGridEntity(mapId); + var grid2 = mapSystem.CreateGridEntity(mapId); var tiles = new List<(Vector2i, Tile)>(); for (var y = 0; y < 3; y++) @@ -58,11 +57,11 @@ public void Merge(Vector2i offset, Angle angle, Box2 bounds) mapSystem.SetTiles(grid1, tiles); mapSystem.SetTiles(grid2, tiles); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); gridFixtures.Merge(grid1.Owner, grid2.Owner, offset, angle, grid1.Comp, grid2.Comp); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); Assert.That(grid1.Comp.LocalAABB, Is.EqualTo(bounds)); } diff --git a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs index d73e86ff9b2..c3ca74f10e5 100644 --- a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs @@ -24,14 +24,13 @@ public async Task TestLocalWorldConversions() await server.WaitIdleAsync(); var entMan = server.ResolveDependency(); - var mapMan = server.ResolveDependency(); var mapSystem = entMan.System(); var transformSystem = entMan.System(); await server.WaitAssertion(() => { mapSystem.CreateMap(out var mapId); - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; var coordinates = new EntityCoordinates(gridEnt, new Vector2(10, 0)); @@ -65,13 +64,12 @@ public async Task TestChunkRotations() await server.WaitIdleAsync(); var entMan = server.ResolveDependency(); - var mapMan = server.ResolveDependency(); var mapSystem = entMan.System(); await server.WaitAssertion(() => { mapSystem.CreateMap(out var mapId); - var grid = mapMan.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridEnt = grid.Owner; /* Test for map chunk rotations */ diff --git a/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs index 447c4ea9473..beca059b306 100644 --- a/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridSplit_Tests.cs @@ -31,11 +31,10 @@ private ISimulation GetSim() public void NoSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); var grid = gridEnt.Comp; grid.CanSplit = false; @@ -44,14 +43,14 @@ public void NoSplit() mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); grid.CanSplit = true; mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -60,20 +59,19 @@ public void NoSplit() public void SimpleSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -82,10 +80,9 @@ public void SimpleSplit() public void DonutSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { @@ -95,16 +92,16 @@ public void DonutSplit() } } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, Vector2i.One, Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 2), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); mapSystem.DeleteMap(mapId); } @@ -113,10 +110,9 @@ public void DonutSplit() public void TriSplit() { var sim = GetSim(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); for (var x = 0; x < 3; x++) { @@ -125,10 +121,10 @@ public void TriSplit() mapSystem.SetTile(gridEnt, Vector2i.One, new Tile(1)); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); mapSystem.SetTile(gridEnt, new Vector2i(1, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(3)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(3)); mapSystem.DeleteMap(mapId); } @@ -141,11 +137,10 @@ public void ReparentSplit() { var sim = GetSim(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; - var gridEnt = mapManager.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); var grid = gridEnt.Comp; for (var x = 0; x < 4; x++) @@ -153,7 +148,7 @@ public void ReparentSplit() mapSystem.SetTile(gridEnt, new Vector2i(x, 0), new Tile(1)); } - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(1)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(1)); var dummy = entManager.SpawnEntity(null, new EntityCoordinates(gridEnt, new Vector2(3.5f, 0.5f))); var dummyXform = entManager.GetComponent(dummy); @@ -164,9 +159,9 @@ public void ReparentSplit() Assert.That(anchoredXform.Anchored); mapSystem.SetTile(gridEnt, new Vector2i(2, 0), Tile.Empty); - Assert.That(mapManager.GetAllGrids(mapId).Count(), Is.EqualTo(2)); + Assert.That(mapSystem.GetAllGrids(mapId).Count(), Is.EqualTo(2)); - var newGrid = mapManager.GetAllGrids(mapId).First(x => x.Comp != grid); + var newGrid = mapSystem.GetAllGrids(mapId).First(x => x.Comp != grid); var newGridXform = entManager.GetComponent(newGrid.Owner); Assert.Multiple(() => diff --git a/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs index 7c6fef1c58f..782cb987de2 100644 --- a/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapGridMap_Tests.cs @@ -21,16 +21,15 @@ public void FindGrids() var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var mapId = sim.CreateMap().MapId; List> grids = []; - mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); + mapSystem.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); Assert.That(grids, Is.Empty); entManager.AddComponent(mapSystem.GetMapOrInvalid(mapId)); - mapManager.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); + mapSystem.FindGridsIntersecting(mapId, Box2.UnitCentered, ref grids); Assert.That(grids, Has.Count.EqualTo(1)); } @@ -43,11 +42,10 @@ public void AddGridCompToMap() var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var mapId = sim.CreateMap().MapId; - mapManager.CreateGridEntity(mapId); + mapSystem.CreateGridEntity(mapId); Assert.DoesNotThrow(() => { diff --git a/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs index 1ba57f5825e..0aea19113e2 100644 --- a/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapGrid_Tests.cs @@ -30,12 +30,11 @@ private static ISimulation SimulationFactory() public void GetTileRefCoords() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1)); var result = mapSystem.GetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1)); @@ -52,13 +51,12 @@ public void GetTileRefCoords() public void BoundsExpansion() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); transformSystem.SetWorldPosition(grid, new Vector2(3, 5)); mapSystem.SetTile(grid, new Vector2i(-1, -2), new Tile(1)); @@ -80,13 +78,12 @@ public void BoundsExpansion() public void BoundsContract() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var transformSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); transformSystem.SetWorldPosition(grid, new Vector2(3, 5)); @@ -108,12 +105,11 @@ public void BoundsContract() public void GridTileToChunkIndices() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var result = mapSystem.GridTileToChunkIndices(grid.Comp, new Vector2i(-9, -1)); @@ -127,12 +123,11 @@ public void GridTileToChunkIndices() public void ToLocalCentered() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var result = mapSystem.GridTileToLocal(grid.Owner, grid.Comp, new Vector2i(0, 0)).Position; @@ -144,12 +139,11 @@ public void ToLocalCentered() public void TryGetTileRefNoTile() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); var foundTile = mapSystem.TryGetTileRef(grid.Owner, grid.Comp, new Vector2i(-9, -1), out var tileRef) ; @@ -162,12 +156,11 @@ public void TryGetTileRefNoTile() public void TryGetTileRefTileExists() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(-9, -1), new Tile(typeId: 1, flags: 1, variant: 1)); @@ -183,12 +176,11 @@ public void TryGetTileRefTileExists() public void PointCollidesWithGrid() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1)); @@ -201,12 +193,11 @@ public void PointCollidesWithGrid() public void PointNotCollideWithGrid() { var sim = SimulationFactory(); - var mapMan = sim.Resolve(); var mapSystem = sim.Resolve().System(); var mapId = sim.CreateMap().MapId; var gridOptions = new GridCreateOptions(); gridOptions.ChunkSize = 8; - var grid = mapMan.CreateGridEntity(mapId, gridOptions); + var grid = mapSystem.CreateGridEntity(mapId, gridOptions); mapSystem.SetTile(grid, new Vector2i(19, 23), new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs b/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs deleted file mode 100644 index 12d4a4604e4..00000000000 --- a/Robust.Shared.IntegrationTests/Map/MapManager_Tests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Numerics; -using NUnit.Framework; -using Robust.Shared.GameObjects; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.UnitTesting.Server; - -namespace Robust.UnitTesting.Shared.Map -{ - [TestFixture, TestOf(typeof(MapManager))] - internal sealed class MapManagerTests - { - private static ISimulation SimulationFactory() - { - var sim = RobustServerSimulation - .NewSimulation() - .InitializeInstance(); - - return sim; - } - - /// - /// When the map manager is restarted, the maps are deleted. - /// - [Test] - public void Restart_ExistingMap_IsRemoved() - { - var sim = SimulationFactory(); - var mapMan = sim.Resolve(); - var entMan = sim.Resolve(); - var mapSys = entMan.System(); - - var mapID = sim.CreateMap().MapId; - - mapMan.Restart(); - - Assert.That(mapSys.MapExists(mapID), Is.False); - } - - /// - /// When the map manager is restarted, the grids are removed. - /// - [Test] - public void Restart_ExistingGrid_IsRemoved() - { - var sim = SimulationFactory(); - var mapMan = sim.Resolve(); - var entMan = sim.Resolve(); - - var mapID = sim.CreateMap().MapId; - var grid = mapMan.CreateGridEntity(mapID); - - mapMan.Restart(); - - Assert.That(entMan.HasComponent(grid), Is.False); - } - - /// - /// When entities are flushed check nullsapce is also culled. - /// - [Test] - public void Restart_NullspaceMap_IsEmptied() - { - var sim = SimulationFactory(); - var entMan = sim.Resolve(); - var oldEntity = entMan.CreateEntityUninitialized(null, MapCoordinates.Nullspace); - entMan.InitializeEntity(oldEntity); - entMan.FlushEntities(); - Assert.That(entMan.Deleted(oldEntity), Is.True); - } - - [Test] - public void Restart_MapEntity_IsRemoved() - { - var sim = SimulationFactory(); - var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); - var entity = entMan.System().CreateMap(); - mapMan.Restart(); - Assert.That((!entMan.EntityExists(entity) ? EntityLifeStage.Deleted : entMan.GetComponent(entity).EntityLifeStage) >= EntityLifeStage.Deleted, Is.True); - } - } -} diff --git a/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs b/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs index 3627df9311b..55effd21968 100644 --- a/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs +++ b/Robust.Shared.IntegrationTests/Map/MapPauseTests.cs @@ -26,7 +26,6 @@ public void Paused_NotIncluded_NotInQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -47,7 +46,6 @@ public void UnPaused_NotIncluded_InQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -68,7 +66,6 @@ public void Paused_Included_InQuery() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -89,7 +86,6 @@ public void Paused_AddEntity_IsPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -108,7 +104,6 @@ public void UnPaused_AddEntity_IsNotPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); // arrange var mapId = sim.CreateMap().Uid; @@ -127,14 +122,14 @@ public void Paused_AddGrid_GridPaused() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); + var mapSys = entMan.System(); // arrange var mapId = sim.CreateMap().MapId; - entMan.System().SetPaused(mapId, true); + mapSys.SetPaused(mapId, true); // act - var newGrid = mapMan.CreateGridEntity(mapId); + var newGrid = mapSys.CreateGridEntity(mapId); // assert var metaData = entMan.GetComponent(newGrid); @@ -199,7 +194,6 @@ public void Paused_UnpauseMap_UnpausedEntities() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().Uid; entMan.System().SetPaused(mapId, true); @@ -219,7 +213,6 @@ public void Unpaused_PauseMap_PausedEntities() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var mapMan = sim.Resolve(); var mapId = sim.CreateMap().Uid; entMan.System().SetPaused(mapId, false); diff --git a/Robust.Shared.IntegrationTests/Map/Query_Tests.cs b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs index ed6b1f369ea..1686c5ceb54 100644 --- a/Robust.Shared.IntegrationTests/Map/Query_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/Query_Tests.cs @@ -57,12 +57,11 @@ public void TestBox2GridIntersection(Vector2 position, float radians, Box2 world var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.System(); var xformSystem = entManager.System(); var map = mapSystem.CreateMap(); - var grid = mapManager.CreateGridEntity(map); + var grid = mapSystem.CreateGridEntity(map); for (var i = 0; i < 10; i++) { @@ -72,7 +71,7 @@ public void TestBox2GridIntersection(Vector2 position, float radians, Box2 world xformSystem.SetWorldRotation(grid.Owner, radians); var grids = new List>(); - mapManager.FindGridsIntersecting(map, worldAABB, ref grids); + mapSystem.FindGridsIntersecting(map, worldAABB, ref grids); Assert.That(grids.Count > 0, Is.EqualTo(result)); } diff --git a/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs index ada8ffa7d49..6ceda8406f9 100644 --- a/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs +++ b/Robust.Shared.IntegrationTests/Map/SingleTileRemoveTest.cs @@ -28,7 +28,6 @@ public async Task TestRemoveSingleTile() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); @@ -74,7 +73,7 @@ public async Task TestRemoveSingleTile() await server.WaitPost(() => { sMap = sys.CreateMap(out var mapId); - var comp = mapMan.CreateGridEntity(mapId); + var comp = sys.CreateGridEntity(mapId); grid = (comp.Owner, comp); sys.SetTile(grid, grid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 1)); var coords = new EntityCoordinates(grid, 0.5f, 0.5f); diff --git a/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs index db24213e235..026d64066a1 100644 --- a/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/BroadphaseNetworkingTest.cs @@ -33,7 +33,6 @@ public async Task TestBroadphaseNetworking() await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var cEntMan = client.ResolveDependency(); var netMan = client.ResolveDependency(); @@ -60,7 +59,7 @@ public async Task TestBroadphaseNetworking() await server.WaitPost(() => { map1 = mapSystem.CreateMap(out var mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1)); grid1 = gridEnt.Owner; }); @@ -130,7 +129,7 @@ await server.WaitPost(() => { // Create grid map2 = mapSystem.CreateMap(out var mapId); - var gridEnt = mapMan.CreateGridEntity(mapId); + var gridEnt = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(gridEnt, Vector2i.Zero, new Tile(1)); grid2 = gridEnt.Owner; diff --git a/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs index cb87f2dbaab..2f90b231d25 100644 --- a/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Broadphase_Test.cs @@ -77,12 +77,11 @@ public void ReparentSundries() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (mapEnt, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); Assert.That(entManager.HasComponent(grid)); @@ -111,14 +110,13 @@ public void ReparentBroadphase() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var fixturesSystem = entManager.EntitySysManager.GetEntitySystem(); var physicsSystem = entManager.EntitySysManager.GetEntitySystem(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (mapEnt, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); var gridUid = grid.Owner; mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); @@ -163,13 +161,12 @@ public void GridMapUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSys = entManager.System(); var xformSys = entManager.System(); var (map1, mapId1) = sim.CreateMap(); var (map2, _) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId1); + var grid = mapSys.CreateGridEntity(mapId1); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); var mapBroadphase1 = entManager.GetComponent(map1); @@ -194,14 +191,13 @@ public void BroadphaseRecursiveUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var system = entManager.EntitySysManager; var physicsSystem = system.GetEntitySystem(); var lookup = system.GetEntitySystem(); var mapSys = entManager.System(); var (map, mapId) = sim.CreateMap(); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSys.CreateGridEntity(mapId); mapSys.SetTile(grid, Vector2i.Zero, new Tile(1)); var gridBroadphase = entManager.GetComponent(grid); @@ -244,7 +240,6 @@ public void EntMapChangeRecursiveUpdate() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var system = entManager.EntitySysManager; var lookup = system.GetEntitySystem(); var xforms = system.GetEntitySystem(); @@ -257,9 +252,9 @@ public void EntMapChangeRecursiveUpdate() var (mapB, mapBId) = sim.CreateMap(); // setup grids - var gridAComp = mapManager.CreateGridEntity(mapAId); - var gridBComp = mapManager.CreateGridEntity(mapBId); - var gridCComp = mapManager.CreateGridEntity(mapAId); + var gridAComp = mapSys.CreateGridEntity(mapAId); + var gridBComp = mapSys.CreateGridEntity(mapBId); + var gridCComp = mapSys.CreateGridEntity(mapAId); var gridA = gridAComp.Owner; var gridB = gridBComp.Owner; var gridC = gridCComp.Owner; diff --git a/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs index a4109827a8d..65b824afc7f 100644 --- a/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/CollisionWake_Test.cs @@ -41,7 +41,6 @@ public async Task TestCollisionWakeGrid() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var mapSystem = entManager.System(); var transformSystem = entManager.System(); @@ -56,7 +55,7 @@ public async Task TestCollisionWakeGrid() await server.WaitPost(() => { mapSystem.CreateMap(out mapId); - grid = mapManager.CreateGridEntity(mapId); + grid = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); entityOne = entManager.SpawnEntity("CollisionWakeTestItem", new MapCoordinates(Vector2.One * 2f, mapId)); diff --git a/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs index 20b35cdf73c..54214964a33 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridDeletion_Test.cs @@ -27,8 +27,8 @@ public async Task GridDeletionTest() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); - var physSystem = server.ResolveDependency().GetEntitySystem(); + var mapSystem = entManager.System(); + var physSystem = entManager.System(); PhysicsComponent physics = default!; @@ -38,7 +38,7 @@ public async Task GridDeletionTest() await server.WaitAssertion(() => { entManager.System().CreateMap(out mapId); - grid = mapManager.CreateGridEntity(mapId); + grid = mapSystem.CreateGridEntity(mapId); physics = entManager.GetComponent(grid); physSystem.SetBodyType(grid, BodyType.Dynamic, body: physics); @@ -55,7 +55,7 @@ await server.WaitAssertion(() => List> grids = []; // So if gridtree is fucky then this SHOULD throw. - mapManager.FindGridsIntersecting(mapId, + mapSystem.FindGridsIntersecting(mapId, new Box2(new Vector2(float.MinValue, float.MinValue), new Vector2(float.MaxValue, float.MaxValue)), ref grids); }); diff --git a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs index 692fad565df..6f6601c8955 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs @@ -25,7 +25,6 @@ public async Task TestFindGridContacts() // Checks that FindGridContacts succesfully overlaps a grid + map broadphase physics body var systems = server.ResolveDependency(); var fixtureSystem = systems.GetEntitySystem(); - var mapManager = server.ResolveDependency(); var entManager = server.ResolveDependency(); var physSystem = systems.GetEntitySystem(); var transformSystem = entManager.EntitySysManager.GetEntitySystem(); @@ -34,7 +33,7 @@ public async Task TestFindGridContacts() await server.WaitAssertion(() => { entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); // Setup 1 body on grid, 1 body off grid, and assert that it's all gucci. mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); diff --git a/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs index a8c2d11f2c2..7c8bcf9cbde 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridReparentVelocity_Test.cs @@ -19,7 +19,6 @@ internal sealed class GridReparentVelocity_Test private ISimulation _sim = default!; private IEntitySystemManager _systems = default!; private IEntityManager _entManager = default!; - private IMapManager _mapManager = default!; private FixtureSystem _fixtureSystem = default!; private SharedMapSystem _mapSystem = default!; private SharedPhysicsSystem _physSystem = default!; @@ -38,7 +37,6 @@ public void FixtureSetup() _systems = _sim.Resolve(); _entManager = _sim.Resolve(); - _mapManager = _sim.Resolve(); _fixtureSystem = _systems.GetEntitySystem(); _mapSystem = _systems.GetEntitySystem(); _physSystem = _systems.GetEntitySystem(); @@ -50,7 +48,7 @@ public void Setup() _mapUid = _mapSystem.CreateMap(out _mapId); // Spawn a 1x1 grid centered at (0.5, 0.5), ensure it's movable and its velocity has no damping. - var gridEnt = _mapManager.CreateGridEntity(_mapId); + var gridEnt = _mapSystem.CreateGridEntity(_mapId); var gridPhys = _entManager.GetComponent(gridEnt); _physSystem.SetSleepingAllowed(gridEnt, gridPhys, false); _physSystem.SetBodyType(gridEnt, BodyType.Dynamic, body: gridPhys); diff --git a/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs index e7ebeb642dd..97631aa532f 100644 --- a/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/JointDeletion_Test.cs @@ -23,7 +23,6 @@ public async Task JointDeletionTest() await server.WaitIdleAsync(); var entManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var susManager = server.ResolveDependency(); var jointSystem = susManager.GetEntitySystem(); var broadphase = susManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs index 4067f53d130..23434da3b01 100644 --- a/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/MapVelocity_Test.cs @@ -34,8 +34,8 @@ public async Task TestMapVelocities() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var system = entityManager.EntitySysManager; + var mapSystem = entityManager.System(); var physicsSys = system.GetEntitySystem(); var xformSystem = system.GetEntitySystem(); var traversal = entityManager.System(); @@ -44,8 +44,8 @@ public async Task TestMapVelocities() await server.WaitAssertion(() => { entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); - var grid2 = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); + var grid2 = mapSystem.CreateGridEntity(mapId); var gridUidA = grid.Owner; Assert.That(entityManager.TryGetComponent(gridUidA, out var gridPhysics)); @@ -106,8 +106,8 @@ public async Task TestNestedParentVelocities() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var system = entityManager.EntitySysManager; + var mapSystem = entityManager.System(); var physicsSys = system.GetEntitySystem(); var xformSystem = system.GetEntitySystem(); var traversal = entityManager.System(); @@ -116,7 +116,7 @@ public async Task TestNestedParentVelocities() await server.WaitAssertion(() => { entityManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var gridUid = grid.Owner; Assert.That(entityManager.TryGetComponent(gridUid, out var gridPhysics)); diff --git a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs index 1672d94c3ec..ee531eb2163 100644 --- a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs @@ -120,7 +120,7 @@ private void Setup(ISimulation sim, out MapId mapId) sim.System().CreateMap(out mapId); - var grid = sim.Resolve().CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); for (var i = 0; i < 3; i++) { diff --git a/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs index 4b061376888..10781409e90 100644 --- a/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs +++ b/Robust.Shared.IntegrationTests/Physics/RecursiveUpdateTest.cs @@ -21,13 +21,12 @@ public void ContainerRecursiveUpdateTest() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var xforms = entManager.System(); var mapSystem = entManager.System(); var containers = entManager.System(); var mapId = sim.CreateMap().MapId; - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var guid = grid.Owner; mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); Assert.That(entManager.HasComponent(guid)); @@ -161,7 +160,6 @@ public void RecursiveMoveTest() { var sim = RobustServerSimulation.NewSimulation().InitializeInstance(); var entManager = sim.Resolve(); - var mapManager = sim.Resolve(); var mapSystem = entManager.EntitySysManager.GetEntitySystem(); var transforms = entManager.EntitySysManager.GetEntitySystem(); var lookup = entManager.EntitySysManager.GetEntitySystem(); @@ -216,7 +214,7 @@ public void RecursiveMoveTest() Assert.That(ents, Does.Contain(child)); // Try again, but this time with a parent change. - var grid = mapManager.CreateGridEntity(mapId); + var grid = mapSystem.CreateGridEntity(mapId); var guid = grid.Owner; mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); var gridBroadphase = entManager.GetComponent(guid); diff --git a/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs index eb90f663401..568aeb28251 100644 --- a/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/Stack_Test.cs @@ -155,7 +155,6 @@ public async Task TestCircleStack() await server.WaitIdleAsync(); var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var entitySystemManager = server.ResolveDependency(); var fixtureSystem = entitySystemManager.GetEntitySystem(); var physSystem = entitySystemManager.GetEntitySystem(); diff --git a/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs index 22fa7147303..713ee695edf 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/HotReloadTest.cs @@ -34,7 +34,6 @@ internal sealed class HotReloadTest : OurRobustUnitTest - type: {HotReloadTestComponentTwoId}"; private PrototypeManager _prototypes = default!; - private IMapManager _maps = default!; private IEntityManager _entities = default!; protected override Type[]? ExtraComponents => new[] {typeof(HotReloadTestOneComponent), typeof(HotReloadTestTwoComponent)}; @@ -48,7 +47,6 @@ public void Setup() _prototypes.LoadString(InitialPrototypes); _prototypes.ResolveResults(); - _maps = IoCManager.Resolve(); _entities = IoCManager.Resolve(); } diff --git a/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs index 3453141b83c..91e96400c01 100644 --- a/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs +++ b/Robust.Shared.IntegrationTests/Spawning/EntitySpawnHelpersTest.cs @@ -24,9 +24,6 @@ public abstract partial class EntitySpawnHelpersTest : RobustIntegrationTest protected SharedTransformSystem Xforms = default!; protected SharedContainerSystem Container = default!; - // Even if unused, content / downstream tests might use this class, so removal would be a breaking change? - protected IMapManager MapMan = default!; - protected EntityUid Map; protected MapId MapId; protected EntityUid Parent; // entity parented to the map. @@ -44,7 +41,6 @@ protected async Task Setup() { Server = StartServer(); await Server.WaitIdleAsync(); - MapMan = Server.ResolveDependency(); EntMan = Server.ResolveDependency(); MapSys = EntMan.System(); Xforms = EntMan.System(); diff --git a/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs index 92401b30d83..eac38f87795 100644 --- a/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs +++ b/Robust.Shared.IntegrationTests/TransformTests/GridTraversalTest.cs @@ -16,7 +16,6 @@ public async Task TestSpawnTraversal() var server = StartServer(); await server.WaitIdleAsync(); - var mapMan = server.ResolveDependency(); var sEntMan = server.ResolveDependency(); var xforms = sEntMan.System(); var mapSys = sEntMan.System(); @@ -29,7 +28,7 @@ public async Task TestSpawnTraversal() await server.WaitPost(() => { map = sEntMan.System().CreateMap(out mapId); - var gridComp = mapMan.CreateGridEntity(mapId); + var gridComp = mapSys.CreateGridEntity(mapId); grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); var gridCentre = new EntityCoordinates(grid, .5f, .5f); diff --git a/Robust.Shared.Scripting/ScriptGlobalsShared.cs b/Robust.Shared.Scripting/ScriptGlobalsShared.cs index f384b772c0e..801df76606d 100644 --- a/Robust.Shared.Scripting/ScriptGlobalsShared.cs +++ b/Robust.Shared.Scripting/ScriptGlobalsShared.cs @@ -33,8 +33,6 @@ public abstract partial class ScriptGlobalsShared : IInvocationContext public IEntitySystemManager esm => _esm; [Dependency] private IPrototypeManager _prot = null!; public IPrototypeManager prot => _prot; - [Dependency] private IMapManager _map = null!; - public IMapManager map => _map; [Dependency] private IDependencyCollection _dependencies = null!; public IDependencyCollection dependencies => _dependencies; [Dependency] private ToolshedManager _shed = null!; diff --git a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs index eaae8fab41e..5c42e361671 100644 --- a/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs +++ b/Robust.Shared/ComponentTrees/ComponentTreeSystem.cs @@ -25,7 +25,6 @@ public abstract partial class ComponentTreeSystem : EntitySyst { [Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!; [Dependency] protected SharedTransformSystem XformSystem = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private SharedMapSystem _mapSystem = default!; private readonly Queue> _updateQueue = new(); @@ -312,7 +311,7 @@ protected virtual Box2 ExtractAabb(in ComponentTreeEntry entry) var state = (EntityManager, trees); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _mapSystem.FindGridsIntersecting(mapId, worldAABB, ref state, (EntityUid uid, MapGridComponent grid, ref (EntityManager EntityManager, ValueList<(EntityUid, TTreeComp)> trees) tuple) => { diff --git a/Robust.Shared/Console/Commands/MapCommands.cs b/Robust.Shared/Console/Commands/MapCommands.cs index 1c679f414f3..58eb49de4d4 100644 --- a/Robust.Shared/Console/Commands/MapCommands.cs +++ b/Robust.Shared/Console/Commands/MapCommands.cs @@ -157,7 +157,6 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) internal sealed partial class ListMapsCommand : LocalizedEntityCommands { [Dependency] private IEntityManager _entManager = default!; - [Dependency] private IMapManager _map = default!; [Dependency] private SharedMapSystem _mapSystem = default!; public override string Command => "lsmap"; @@ -180,7 +179,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) _mapSystem.IsInitialized(mapUid), _mapSystem.IsPaused(mapId), _entManager.GetNetEntity(mapUid), - string.Join(",", _map.GetAllGrids(mapId).Select(grid => grid.Owner))); + string.Join(",", _mapSystem.GetAllGrids(mapId).Select(grid => grid.Owner))); } // Trim the newline diff --git a/Robust.Shared/Console/Commands/TeleportCommands.cs b/Robust.Shared/Console/Commands/TeleportCommands.cs index c028ae2a897..e7e5fd8d447 100644 --- a/Robust.Shared/Console/Commands/TeleportCommands.cs +++ b/Robust.Shared/Console/Commands/TeleportCommands.cs @@ -17,7 +17,6 @@ namespace Robust.Shared.Console.Commands; internal sealed partial class TeleportCommand : LocalizedEntityCommands { - [Dependency] private IMapManager _map = default!; [Dependency] private IEntityManager _entityManager = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private SharedMapSystem _mapSystem = default!; @@ -53,7 +52,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) return; } - if (_map.TryFindGridAt(mapId, position, out var gridUid, out var grid)) + if (_mapSystem.TryFindGridAt(mapId, position, out var gridUid, out var grid)) { var gridPos = Vector2.Transform(position, _transform.GetInvWorldMatrix(gridUid)); diff --git a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs index 0b3bb6c1c6a..c5df77dfadb 100644 --- a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs +++ b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; @@ -41,8 +42,8 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode using var stream = new MemoryStream(tileBytes); using var reader = new BinaryReader(stream); - var mapManager = dependencies.Resolve(); - mapManager.SuppressOnTileChanged = true; + var mapSystem = dependencies.Resolve().System(); + mapSystem.SuppressOnTileChanged = true; ushort size = 16; @@ -112,7 +113,7 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode } chunk.SuppressCollisionRegeneration = false; - mapManager.SuppressOnTileChanged = false; + mapSystem.SuppressOnTileChanged = false; return chunk; } @@ -192,8 +193,8 @@ public MapChunk CreateCopy( SerializationHookContext hookCtx, ISerializationContext? context = null) { - var mapManager = dependencies.Resolve(); - mapManager.SuppressOnTileChanged = true; + var mapSystem = dependencies.Resolve().System(); + mapSystem.SuppressOnTileChanged = true; var chunk = new MapChunk(source.X, source.Y, source.ChunkSize) { SuppressCollisionRegeneration = true @@ -207,7 +208,7 @@ public MapChunk CreateCopy( } } - mapManager.SuppressOnTileChanged = false; + mapSystem.SuppressOnTileChanged = false; chunk.SuppressCollisionRegeneration = false; return chunk; diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index b94210a3fb7..206ca55fb53 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -103,8 +103,6 @@ public Matrix3x2 InvLocalMatrix [ViewVariables] internal readonly HashSet _children = new(); - [Dependency] private IMapManager _mapManager = default!; - /// /// Returns the index of the map which this object is on /// @@ -378,7 +376,7 @@ public bool Anchored { _anchored = value; } - else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out _, out var grid)) + else if (value && !_anchored && _entMan.EntitySysManager.GetEntitySystem().TryFindGridAt(MapPosition, out _, out var grid)) { _anchored = _entMan.EntitySysManager.GetEntitySystem().AnchorEntity(Owner, this, grid); } diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 2df50e9dbf9..dec2756581a 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -37,7 +37,6 @@ public abstract partial class EntityManager : IEntityManager [IoC.Dependency] protected IPrototypeManager PrototypeManager = default!; [IoC.Dependency] protected ILogManager LogManager = default!; [IoC.Dependency] private IEntitySystemManager _entitySystemManager = default!; - [IoC.Dependency] private IMapManager _mapManager = default!; [IoC.Dependency] private IGameTiming _gameTiming = default!; [IoC.Dependency] private ISerializationManager _serManager = default!; [IoC.Dependency] private ProfManager _prof = default!; @@ -360,7 +359,7 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo throw new ArgumentException($"Attempted to spawn entity on an invalid map. Coordinates: {coordinates}"); EntityCoordinates coords; - if (_mapManager.TryFindGridAt(coordinates, out var gridUid, out var grid) + if (_mapSystem.TryFindGridAt(coordinates, out var gridUid, out var grid) && MetaQuery.TryGetComponentInternal(gridUid, out var meta) && meta.EntityLifeStage < EntityLifeStage.Terminating) { diff --git a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs index 9eee6955726..fa7df579f5e 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookup.Queries.cs @@ -94,7 +94,7 @@ private void AddEntitiesIntersecting(MapId mapId, flags); // Need to include maps - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent _, ref EntityQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -245,7 +245,7 @@ private bool AnyEntitiesIntersecting(MapId mapId, flags); // Need to include maps - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent _, ref AnyEntityQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -557,7 +557,7 @@ public void GetEntitiesIntersecting(EntityUid uid, HashSet intersecti var state = (uid, transform, intersecting, _fixturesQuery, this, _physics, flags); // Unfortuantely I can't think of a way to de-dupe this with the other ones as it's slightly different. - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid gridUid, MapGridComponent grid, ref (EntityUid entity, Transform transform, HashSet intersecting, EntityQuery fixturesQuery, EntityLookupSystem lookup, SharedPhysicsSystem physics, LookupFlags flags) state) => @@ -787,7 +787,7 @@ public void FindLookupsIntersecting(MapId mapId, Box2Rotated worldBounds, Compon var state = (callback, _broadQuery); - _mapManager.FindGridsIntersecting(mapId, worldBounds, ref state, + _map.FindGridsIntersecting(mapId, worldBounds, ref state, static (EntityUid uid, MapGridComponent grid, ref (ComponentQueryCallback callback, EntityQuery _broadQuery) tuple) => diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs index 16d33310d35..f579ecc7bc6 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs @@ -458,7 +458,7 @@ public bool AnyComponentsIntersecting(Type type, MapId mapId, T shape, Transf // Get grid entities var state = (this, worldAABB, flags, query, ignored, found: false); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref (EntityLookupSystem system, Box2 worldAABB, @@ -550,7 +550,7 @@ public void GetEntitiesIntersecting(Type type, MapId mapId, T shape, Transfor // Get grid entities var state = new GridQueryState(intersecting, shape, shapeTransform, this, _physics, flags, query); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); @@ -596,7 +596,7 @@ public void GetEntitiesIntersecting(MapId mapId, TShape shape, Transf // Get grid entities var state = new GridQueryState(entities, shape, shapeTransform, this, _physics, flags, query); - _mapManager.FindGridsIntersecting(mapId, worldAABB, ref state, + _map.FindGridsIntersecting(mapId, worldAABB, ref state, static (EntityUid uid, MapGridComponent grid, ref GridQueryState state) => { var localTransform = state.Physics.GetRelativePhysicsTransform(state.Transform, uid); diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index 1d1e90c671d..25e124922f9 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -73,7 +73,6 @@ public record struct WorldAABBEvent public sealed partial class EntityLookupSystem : EntitySystem { [Dependency] private IManifoldManager _manifoldManager = default!; - [Dependency] private IMapManager _mapManager = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _netMan = default!; [Dependency] private SharedContainerSystem _container = default!; diff --git a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs index 5cbdd7f6f63..ef0f8f0108c 100644 --- a/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedGridTraversalSystem.cs @@ -13,7 +13,7 @@ namespace Robust.Shared.GameObjects; ///
public sealed partial class SharedGridTraversalSystem : EntitySystem { - [Dependency] private IMapManagerInternal _mapManager = default!; + [Dependency] private SharedMapSystem _mapSystem = default!; [Dependency] private SharedTransformSystem _transform = default!; [Dependency] private IGameTiming _timing = default!; @@ -97,7 +97,7 @@ public void CheckTraversal(EntityUid entity, TransformComponent xform, EntityUid : Vector2.Transform(xform.LocalPosition, Transform(xform.ParentUid).LocalMatrix); // Change parent if necessary - if (_mapManager.TryFindGridAt(map, mapPos, out var gridUid, out _)) + if (_mapSystem.TryFindGridAt(map, mapPos, out var gridUid, out _)) { // Some minor duplication here with AttachParent but only happens when going on/off grid so not a big deal ATM. if (gridUid != xform.GridUid && !TerminatingOrDeleted(gridUid)) diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs index 7ff85746be3..de2ad55a9d2 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Coordinates.cs @@ -25,7 +25,7 @@ public EntityCoordinates AlignToGrid(EntityCoordinates coordinates) // Check if mappos intersects a grid. var mapPos = _transform.ToMapCoordinates(coordinates); - if (_mapInternal.TryFindGridAt(mapPos, out var gridUid, out gridComponent)) + if (TryFindGridAt(mapPos, out var gridUid, out gridComponent)) { var tile = CoordinatesToTile(gridUid, gridComponent, coordinates); return ToCenterCoordinates(gridUid, tile, gridComponent); diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs index ae3165b0543..4d754b59031 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.Grid.Queries.cs @@ -457,8 +457,7 @@ public IEnumerable GetAllMapGrids(MapId mapId) /// The shape of the region to check. /// The world-local axis aligned bounding box of the region to check. /// The transform, relative to the map, of the region to check. - [Access(typeof(MapManager), Other = AccessPermissions.None)] - public void FindGridsIntersecting( + private void FindGridsIntersecting( EntityUid mapEnt, TShape shape, Box2 worldAABB, diff --git a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs index 94486e8077e..9564f3b849a 100644 --- a/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedMapSystem.cs @@ -22,9 +22,7 @@ public abstract partial class SharedMapSystem : EntitySystem { [Dependency] private ITileDefinitionManager _tileMan = default!; [Dependency] private IGameTiming _timing = default!; - [Dependency] protected IMapManager MapManager = default!; [Dependency] private IManifoldManager _manifolds = default!; - [Dependency] private IMapManagerInternal _mapInternal = default!; [Dependency] private INetManager _netManager = default!; [Dependency] private FixtureSystem _fixtures = default!; [Dependency] private SharedPhysicsSystem _physics = default!; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index cdeeaf29c81..0e8b2ddcfa1 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -283,7 +283,7 @@ private void OnCompInit(EntityUid uid, TransformComponent component, ComponentIn // Entity may not be directly parented to the grid (e.g., spawned using some relative entity coordinates) // in that case, we attempt to attach to a grid. var pos = new MapCoordinates(GetWorldPosition(component), component.MapID); - if (_mapManager.TryFindGridAt(pos, out var gridUid, out gridComp)) + if (_map.TryFindGridAt(pos, out var gridUid, out gridComp)) grid = (gridUid, gridComp); } @@ -1016,7 +1016,7 @@ public void SetMapCoordinates(Entity entity, MapCoordinates { var mapUid = _map.GetMap(coordinates.MapId); if (!_gridQuery.HasComponent(entity) && - _mapManager.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _)) + _map.TryFindGridAt(mapUid, coordinates.Position, out var targetGrid, out _)) { var invWorldMatrix = GetInvWorldMatrix(targetGrid); SetCoordinates((entity.Owner, entity.Comp, MetaData(entity.Owner)), new EntityCoordinates(targetGrid, Vector2.Transform(coordinates.Position, invWorldMatrix))); @@ -1259,7 +1259,7 @@ private void SetWorldPositionRotationInternal(EntityUid uid, Vector2 worldPos, A return; } - if (component.GridUid != uid && _mapManager.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _)) + if (component.GridUid != uid && _map.TryFindGridAt(component.MapUid.Value, worldPos, out var targetGrid, out _)) { var targetGridXform = XformQuery.GetComponent(targetGrid); var invLocalMatrix = targetGridXform.InvLocalMatrix; @@ -1513,7 +1513,7 @@ public bool TryGetMapOrGridCoordinates( return false; var oldPos = GetWorldPosition(xform); - if (_mapManager.TryFindGridAt(map, oldPos, out var gridUid, out _) && !TerminatingOrDeleted(gridUid)) + if (_map.TryFindGridAt(map, oldPos, out var gridUid, out _) && !TerminatingOrDeleted(gridUid)) { coordinates = gridUid == xform.ParentUid ? new EntityCoordinates(gridUid, xform.LocalPosition) @@ -1773,7 +1773,7 @@ public bool SwapPositions(Entity entity1, Entity entity1, Entity(); var gridId = xform.GetGrid(coords); @@ -23,7 +23,7 @@ public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates var mapCoords = xform.ToMapCoordinates(coords); - if (mapManager.TryFindGridAt(mapCoords, out var gridUid, out mapGrid)) + if (mapSystem.TryFindGridAt(mapCoords, out var gridUid, out mapGrid)) { return mapSystem.GridTileToLocal(gridUid, mapGrid, mapSystem.CoordinatesToTile(gridUid, mapGrid, coords)); } @@ -34,7 +34,7 @@ public static EntityCoordinates AlignWithClosestGridTile(this EntityCoordinates // find grids in search box var gridsInArea = new List>(); - mapManager.FindGridsIntersecting(mapCoords.MapId, gridSearchBox, ref gridsInArea); + mapSystem.FindGridsIntersecting(mapCoords.MapId, gridSearchBox, ref gridsInArea); // find closest grid intersecting our search box. gridUid = EntityUid.Invalid; diff --git a/Robust.Shared/Map/EntityCoordinates.cs b/Robust.Shared/Map/EntityCoordinates.cs index 323fe187cbf..4eaae6b7cea 100644 --- a/Robust.Shared/Map/EntityCoordinates.cs +++ b/Robust.Shared/Map/EntityCoordinates.cs @@ -95,18 +95,11 @@ public static EntityCoordinates FromMap(EntityUid entity, MapCoordinates coordin return transformSystem.ToCoordinates(entity, coordinates); } - [Obsolete("Use SharedTransformSystem.ToCoordinates()")] - public static EntityCoordinates FromMap(IMapManager mapManager, MapCoordinates coordinates) - { - return IoCManager.Resolve().System().ToCoordinates(coordinates); - } - /// /// Converts this set of coordinates to Vector2i. /// public Vector2i ToVector2i( IEntityManager entityManager, - IMapManager mapManager, SharedTransformSystem transformSystem) { if(!IsValid(entityManager)) diff --git a/Robust.Shared/Map/IMapManager.cs b/Robust.Shared/Map/IMapManager.cs deleted file mode 100644 index fad26a165af..00000000000 --- a/Robust.Shared/Map/IMapManager.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; - -namespace Robust.Shared.Map -{ - /// - /// This manages all the grids and maps in the world. Largely superseded by . - /// - [NotContentImplementable] - public interface IMapManager - { - public const bool Approximate = SharedMapSystem.Approximate; - public const bool IncludeMap = SharedMapSystem.IncludeMap; - - /// - /// Should the OnTileChanged event be suppressed? This is useful for initially loading the map - /// so that you don't spam an event for each of the million station tiles. - /// - [Obsolete("use SharedMapSystem")] - bool SuppressOnTileChanged { get; set; } - - /// - /// Starts up the map system. - /// - void Initialize(); - - void Shutdown(); - void Startup(); - - void Restart(); - - [Obsolete("Use MapSystem")] - MapId CreateMap(MapId? mapId = null); - - /// - /// Check whether a map with specified ID exists. - /// - /// The map ID to check existence of. - /// True if the map exists, false otherwise. - [Obsolete("Use MapSystem")] - bool MapExists([NotNullWhen(true)] MapId? mapId); - - /// - /// Returns the map entity ID for a given map, or an invalid entity Id if the map does not exist. - /// - [Obsolete("Use MapSystem")] - EntityUid GetMapEntityId(MapId mapId); - - /// - /// Replaces GetMapEntity()'s throw-on-failure semantics. - /// - [Obsolete("Use MapSystem")] - EntityUid GetMapEntityIdOrThrow(MapId mapId); - - [Obsolete("Use MapSystem")] - IEnumerable GetAllMapIds(); - - [Obsolete("Use MapSystem")] - void DeleteMap(MapId mapId); - - // ReSharper disable once MethodOverloadWithOptionalParameter - [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] - MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16); - [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] - MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options); - [Obsolete("Use MapSystem.CreateGridEntity(...).Comp")] - MapGridComponent CreateGrid(MapId currentMapId); - [Obsolete("Use MapSystem")] - Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null); - [Obsolete("Use MapSystem")] - Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null); - - [Obsolete("Use MapSystem")] - IEnumerable GetAllMapGrids(MapId mapId); - - [Obsolete("Use MapSystem")] - IEnumerable> GetAllGrids(MapId mapId); - - #region MapId - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, - ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = Approximate, - bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, - GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, - bool approx = Approximate, - bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, - GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - #endregion - - #region MapEnt - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref TState state, GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - /// - /// Returns true if any grids overlap the specified shapes. - /// - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, - ref List> entities, bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref List> grids, bool approx = Approximate, bool includeMap = IncludeMap) where T : IPhysShape; - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, - GridCallback callback, bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, - bool approx = Approximate, - bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, - GridCallback callback, - bool approx = Approximate, bool includeMap = IncludeMap); - - [Obsolete("Use MapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, - ref List> grids, - bool approx = Approximate, bool includeMap = IncludeMap); - - #endregion - - #region TryFindGridAt - - [Obsolete("Use MapSystem")] - public bool TryFindGridAt( - EntityUid mapEnt, - Vector2 worldPos, - out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - /// - /// Attempts to find the map grid under the map location. - /// - [Obsolete("Use MapSystem")] - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - /// - /// Attempts to find the map grid under the map location. - /// - [Obsolete("Use MapSystem")] - public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid); - - #endregion - - #region Obsolete - - [Obsolete] - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, EntityQuery query, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - return TryFindGridAt(mapId, worldPos, out uid, out grid); - } - - [Obsolete] - public IEnumerable FindGridsIntersecting(MapId mapId, Box2 worldAabb, bool approx = false, bool includeMap = true) - { - var grids = new List>(); - FindGridsIntersecting(mapId, worldAabb, ref grids, approx, includeMap); - - foreach (var grid in grids) - { - yield return grid.Comp; - } - } - - [Obsolete] - public IEnumerable FindGridsIntersecting(MapId mapId, Box2Rotated worldArea, bool approx = false, bool includeMap = true) - { - var grids = new List>(); - FindGridsIntersecting(mapId, worldArea, ref grids, approx, includeMap); - - foreach (var grid in grids) - { - yield return grid.Comp; - } - } - - #endregion - - - [Obsolete("Just delete the grid entity")] - void DeleteGrid(EntityUid euid); - - [Obsolete("Use HasComp")] - bool IsGrid(EntityUid uid); - - [Obsolete("Use HasComp")] - bool IsMap(EntityUid uid); - - // - // Pausing functions - // - - [Obsolete("Use MapSystem")] - void SetMapPaused(MapId mapId, bool paused); - - [Obsolete("Use MapSystem")] - void DoMapInitialize(MapId mapId); - - [Obsolete("Use MapSystem")] - bool IsMapPaused(MapId mapId); - - [Obsolete("Use MapSystem")] - bool IsMapInitialized(MapId mapId); - } -} diff --git a/Robust.Shared/Map/IMapManagerInternal.cs b/Robust.Shared/Map/IMapManagerInternal.cs deleted file mode 100644 index c2b19a20f21..00000000000 --- a/Robust.Shared/Map/IMapManagerInternal.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; - -namespace Robust.Shared.Map -{ - /// - [Obsolete] - internal interface IMapManagerInternal : IMapManager - { - /// - /// Raises the OnTileChanged event. - /// - /// A reference to the new tile. - /// The old tile that got replaced. - [Obsolete("use SharedMapSystem")] - void RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk); - } -} diff --git a/Robust.Shared/Map/MapId.cs b/Robust.Shared/Map/MapId.cs index 4045791de7c..e8848ee8f36 100644 --- a/Robust.Shared/Map/MapId.cs +++ b/Robust.Shared/Map/MapId.cs @@ -11,7 +11,6 @@ namespace Robust.Shared.Map /// All maps, aside from , are also entities. When writing generic code it's usually /// preferable to use or instead. /// - /// /// [Serializable, NetSerializable] public readonly struct MapId : IEquatable diff --git a/Robust.Shared/Map/MapManager.GridCollection.cs b/Robust.Shared/Map/MapManager.GridCollection.cs deleted file mode 100644 index 75502418251..00000000000 --- a/Robust.Shared/Map/MapManager.GridCollection.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections.Generic; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; -using Robust.Shared.Utility; - -namespace Robust.Shared.Map; -internal partial class MapManager -{ - // ReSharper disable once MethodOverloadWithOptionalParameter - [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] - public MapGridComponent CreateGrid(MapId currentMapId, ushort chunkSize = 16) - { - return CreateGridEntity(currentMapId, options: GridCreateOptions.Default with { ChunkSize = chunkSize }).Comp; - } - - [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] - public MapGridComponent CreateGrid(MapId currentMapId, in GridCreateOptions options) - { - return CreateGridEntity(currentMapId, options: options).Comp; - } - - [Obsolete("use SharedMapSystem.CreateGridEntity(...).Comp")] - public MapGridComponent CreateGrid(MapId currentMapId) - { - return CreateGridEntity(currentMapId, options: GridCreateOptions.Default).Comp; - } - - [Obsolete("use SharedMapSystem.CreateGridEntity")] - public Entity CreateGridEntity(MapId currentMapId, GridCreateOptions? options = null) - { - return MapSystem.CreateGridEntity(currentMapId, options: options); - } - - [Obsolete("use SharedMapSystem.CreateGridEntity")] - public Entity CreateGridEntity(EntityUid map, GridCreateOptions? options = null) - { - return MapSystem.CreateGridEntity(map, options: options); - } - - [Obsolete("Use HasComponent(uid)")] - public bool IsGrid(EntityUid uid) - { - return EntityManager.HasComponent(uid); - } - - [Obsolete("use SharedMapSystem.GetAllMapGrids")] - public IEnumerable GetAllMapGrids(MapId mapId) - { - return MapSystem.GetAllMapGrids(mapId); - } - - [Obsolete("use SharedMapSystem.GetAllGrids")] - public IEnumerable> GetAllGrids(MapId mapId) - { - return MapSystem.GetAllGrids(mapId); - } - - [Obsolete("just delete the grid entity")] - public virtual void DeleteGrid(EntityUid euid) - { - // Possible the grid was already deleted / is invalid - if (!EntityManager.TryGetComponent(euid, out var iGrid)) - { - DebugTools.Assert($"Calling {nameof(DeleteGrid)} with unknown uid {euid}."); - return; // Silently fail on release - } - - if (!EntityManager.TryGetComponent(euid, out MetaDataComponent? metaComp)) - { - DebugTools.Assert($"Calling {nameof(DeleteGrid)} with {euid}, but there was no allocated entity."); - return; // Silently fail on release - } - - // DeleteGrid may be triggered by the entity being deleted, - // so make sure that's not the case. - if (metaComp.EntityLifeStage < EntityLifeStage.Terminating) - EntityManager.DeleteEntity(euid); - } - - /// - [Obsolete("use SharedMapSystem.SuppressOnTileChanged")] - public bool SuppressOnTileChanged - { - get => MapSystem.SuppressOnTileChanged; - set { MapSystem.SuppressOnTileChanged = value; } - } - - /// - /// Raises the OnTileChanged event. - /// - /// A reference to the new tile. - /// The old tile that got replaced. - [Obsolete("use SharedMapSystem.RaiseOnTileChanged")] - void IMapManagerInternal.RaiseOnTileChanged(Entity entity, TileRef tileRef, Tile oldTile, Vector2i chunk) - { - MapSystem.RaiseOnTileChanged(entity, tileRef, oldTile, chunk); - } -} diff --git a/Robust.Shared/Map/MapManager.MapCollection.cs b/Robust.Shared/Map/MapManager.MapCollection.cs deleted file mode 100644 index 3810652f9a4..00000000000 --- a/Robust.Shared/Map/MapManager.MapCollection.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; - -namespace Robust.Shared.Map; - -/// -/// Arguments for when a map is created or deleted locally ore remotely. -/// -public sealed class MapEventArgs : EventArgs -{ - /// - /// Creates a new instance of this class. - /// - public MapEventArgs(MapId map) - { - Map = map; - } - - /// - /// Map that is being modified. - /// - public MapId Map { get; } -} - -internal partial class MapManager -{ - /// - public virtual void DeleteMap(MapId mapId) - { - MapSystem.DeleteMap(mapId); - } - - /// - public MapId CreateMap(MapId? mapId = null) - { - if (mapId != null) - { - MapSystem.CreateMap(mapId.Value); - return mapId.Value; - } - - MapSystem.CreateMap(out var map); - return map; - } - - /// - public bool MapExists([NotNullWhen(true)] MapId? mapId) - { - return MapSystem.MapExists(mapId); - } - - /// - public EntityUid GetMapEntityId(MapId mapId) - { - return MapSystem.GetMapOrInvalid(mapId); - } - - /// - /// Replaces GetMapEntity()'s throw-on-failure semantics. - /// - public EntityUid GetMapEntityIdOrThrow(MapId mapId) - { - return MapSystem.GetMap(mapId); - } - - public bool TryGetMap([NotNullWhen(true)] MapId? mapId, [NotNullWhen(true)] out EntityUid? uid) - { - return MapSystem.TryGetMap(mapId, out uid); - } - - /// - public IEnumerable GetAllMapIds() - { - return MapSystem.GetAllMapIds(); - } - - /// - public bool IsMap(EntityUid uid) - { - return EntityManager.HasComponent(uid); - } -} diff --git a/Robust.Shared/Map/MapManager.Pause.cs b/Robust.Shared/Map/MapManager.Pause.cs deleted file mode 100644 index 35415537c4a..00000000000 --- a/Robust.Shared/Map/MapManager.Pause.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Globalization; -using Robust.Shared.GameObjects; - -namespace Robust.Shared.Map -{ - internal partial class MapManager - { - public void SetMapPaused(MapId mapId, bool paused) - { - MapSystem.SetPaused(mapId, paused); - } - - public void SetMapPaused(EntityUid uid, bool paused) - { - MapSystem.SetPaused(uid, paused); - } - - public void DoMapInitialize(MapId mapId) - { - MapSystem.InitializeMap(mapId); - } - - public bool IsMapInitialized(MapId mapId) - { - return MapSystem.IsInitialized(mapId); - } - - /// - public bool IsMapPaused(MapId mapId) - { - return MapSystem.IsPaused(mapId); - } - - /// - public bool IsMapPaused(EntityUid uid) - { - return MapSystem.IsPaused(uid); - } - } -} diff --git a/Robust.Shared/Map/MapManager.Queries.cs b/Robust.Shared/Map/MapManager.Queries.cs deleted file mode 100644 index 8916fa76d84..00000000000 --- a/Robust.Shared/Map/MapManager.Queries.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using Robust.Shared.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Maths; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Collision.Shapes; - -namespace Robust.Shared.Map; - -internal partial class MapManager -{ - #region MapId [Obsolete] - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapId, shape, transform, ref grids, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, T shape, Transform transform, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapId, shape, transform, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldAABB, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2 worldAABB, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldAABB, ref grids, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldBounds, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref TState state, GridCallback callback, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(MapId mapId, Box2Rotated worldBounds, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapId, worldBounds, ref grids, approx: approx, includeMap: includeMap); - } - - #endregion - - #region MapEnt [Obsolete] - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Transform transform, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapEnt, shape, transform, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting( - EntityUid mapEnt, - T shape, - Transform transform, - ref TState state, - GridCallback callback, - bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapEnt, shape, transform, ref state, callback, approx: approx, includeMap: includeMap); - } - - /// - /// Returns true if any grids overlap the specified shapes. - /// - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, List shapes, Transform transform, ref List> entities, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, shapes, transform, ref entities, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapEnt, shape, transform, ref grids, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, T shape, Box2 worldAABB, Transform transform, - ref List> grids, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) where T : IPhysShape - { - MapSystem.FindGridsIntersecting(mapEnt, shape, worldAABB, transform, ref grids, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldAABB, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref TState state, GridCallback callback, bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldAABB, ref state, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2 worldAABB, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldAABB, ref grids, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, GridCallback callback, bool approx = IMapManager.Approximate, - bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldBounds, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref TState state, GridCallback callback, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldBounds, ref state, callback, approx: approx, includeMap: includeMap); - } - - [Obsolete("use SharedMapSystem")] - public void FindGridsIntersecting(EntityUid mapEnt, Box2Rotated worldBounds, ref List> grids, - bool approx = IMapManager.Approximate, bool includeMap = IMapManager.IncludeMap) - { - MapSystem.FindGridsIntersecting(mapEnt, worldBounds, ref grids, approx: approx, includeMap: includeMap); - } - - #endregion - - #region TryFindGridAt - - [Obsolete("use SharedMapSystem")] - public bool TryFindGridAt( - EntityUid mapEnt, - Vector2 worldPos, - out EntityUid uid, - [NotNullWhen(true)] out MapGridComponent? grid) - { - return MapSystem.TryFindGridAt(mapEnt, worldPos, out uid, out grid); - } - - /// - /// Attempts to find the map grid under the map location. - /// - [Obsolete("use SharedMapSystem")] - public bool TryFindGridAt(MapId mapId, Vector2 worldPos, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - return MapSystem.TryFindGridAt(mapId, worldPos, out uid, out grid); - } - - /// - /// Attempts to find the map grid under the map location. - /// - [Obsolete("use SharedMapSystem")] - public bool TryFindGridAt(MapCoordinates mapCoordinates, out EntityUid uid, [NotNullWhen(true)] out MapGridComponent? grid) - { - return MapSystem.TryFindGridAt(mapCoordinates, out uid, out grid); - } - - #endregion -} diff --git a/Robust.Shared/Map/MapManager.cs b/Robust.Shared/Map/MapManager.cs deleted file mode 100644 index 3bfd5f3e3ce..00000000000 --- a/Robust.Shared/Map/MapManager.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Log; -using Robust.Shared.Map.Components; - -namespace Robust.Shared.Map; - -/// -[Virtual] -internal partial class MapManager : IMapManagerInternal, IEntityEventSubscriber -{ - [Dependency] public IEntityManager EntityManager = default!; - [Dependency] private ILogManager _logManager = default!; - - private ISawmill _sawmill = default!; - - protected SharedMapSystem MapSystem = default!; - - /// - public void Initialize() - { - _sawmill = _logManager.GetSawmill("system.map"); - } - - /// - public void Startup() - { - MapSystem = EntityManager.System(); - - _sawmill.Debug("Starting..."); - } - - /// - public void Shutdown() - { - _sawmill.Debug("Stopping..."); - - // TODO: AllEntityQuery instead??? - var query = EntityManager.EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out _)) - { - EntityManager.DeleteEntity(uid); - } - } - - /// - public void Restart() - { - _sawmill.Debug("Restarting..."); - - // Don't just call Shutdown / Startup because we don't want to touch the subscriptions on gridtrees - // Restart can be called any time during a game, whereas shutdown / startup are typically called upon connection. - var query = EntityManager.EntityQueryEnumerator(); - - while (query.MoveNext(out var uid, out _)) - { - EntityManager.DeleteEntity(uid); - } - } -} diff --git a/Robust.Shared/Map/NetworkedMapManager.cs b/Robust.Shared/Map/NetworkedMapManager.cs deleted file mode 100644 index fdfbfadf781..00000000000 --- a/Robust.Shared/Map/NetworkedMapManager.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Robust.Shared.Timing; - -namespace Robust.Shared.Map; - -[Obsolete] -internal interface INetworkedMapManager : IMapManagerInternal -{ - [Obsolete] - void CullDeletionHistory(GameTick upToTick); -} - -[Obsolete] -internal sealed class NetworkedMapManager : MapManager, INetworkedMapManager -{ - [Obsolete] - public void CullDeletionHistory(GameTick upToTick) - { - MapSystem.CullDeletionHistory(upToTick); - } -} diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index e6fbf61f9b2..17d2108adef 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -18,7 +18,6 @@ namespace Robust.Shared.Physics.Systems public abstract partial class SharedBroadphaseSystem : EntitySystem { [Dependency] private IConfigurationManager _cfg = default!; - [Dependency] private IMapManagerInternal _mapManager = default!; [Dependency] private IParallelManager _parallel = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedGridTraversalSystem _traversal = default!; @@ -51,9 +50,9 @@ public override void Initialize() _contactJob = new() { - MapManager = _mapManager, System = this, TransformSys = EntityManager.System(), + MapSys = _map, // TODO: EntityManager one isn't ready yet? XformQuery = GetEntityQuery(), }; @@ -265,7 +264,7 @@ private void HandleGridCollisions(HashSet movedGrids) _physicsQuery, _xformQuery); - _mapManager.FindGridsIntersecting(xform.MapID, aabb, ref state, + _map.FindGridsIntersecting(xform.MapID, aabb, ref state, static (EntityUid gridBUid, MapGridComponent gridBMapComp, ref (Entity gridA, Transform gridAToWorldRigid, @@ -541,7 +540,7 @@ internal void GetBroadphases(MapId mapId, Box2 aabb, BroadphaseCallback callback if (_broadphaseQuery.TryGetComponent(map.Value, out var mapBroadphase)) callback((map.Value, mapBroadphase)); - _mapManager.FindGridsIntersecting(map.Value, + _map.FindGridsIntersecting(map.Value, aabb, ref internalState, static ( @@ -569,7 +568,7 @@ internal void GetBroadphases(MapId mapId, Box2 aabb, ref TState state, B if (_broadphaseQuery.TryGetComponent(map.Value, out var mapBroadphase)) callback((map.Value, mapBroadphase), ref state); - _mapManager.FindGridsIntersecting(map.Value, + _map.FindGridsIntersecting(map.Value, aabb, ref internalState, static ( @@ -596,7 +595,7 @@ private record struct BroadphaseContactJob() : IParallelRobustJob { public SharedBroadphaseSystem System = default!; public SharedTransformSystem TransformSys = default!; - public IMapManager MapManager = default!; + public SharedMapSystem MapSys = default!; public EntityQuery XformQuery; @@ -626,7 +625,7 @@ public void Execute(int index) var state = (System, proxy, worldAABB, Pairs); // Get every broadphase we may be intersecting. - MapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state, + MapSys.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state, static (EntityUid uid, MapGridComponent _, ref ( SharedBroadphaseSystem system, FixtureProxy proxy, diff --git a/Robust.UnitTesting/IIntegrationInstance.cs b/Robust.UnitTesting/IIntegrationInstance.cs index f7c00c1f5c2..5585120fb83 100644 --- a/Robust.UnitTesting/IIntegrationInstance.cs +++ b/Robust.UnitTesting/IIntegrationInstance.cs @@ -31,7 +31,6 @@ public interface IIntegrationInstance : IDisposable IConfigurationManager CfgMan { get; } ISharedPlayerManager PlayerMan { get; } INetManager NetMan { get; } - IMapManager MapMan { get; } IGameTiming Timing { get; } ISawmill Log { get; } diff --git a/Robust.UnitTesting/Pool/TestPair.Helpers.cs b/Robust.UnitTesting/Pool/TestPair.Helpers.cs index 1ada68dcb5a..d587ecff17f 100644 --- a/Robust.UnitTesting/Pool/TestPair.Helpers.cs +++ b/Robust.UnitTesting/Pool/TestPair.Helpers.cs @@ -281,7 +281,7 @@ public async Task CreateTestMap(bool initialized, ushort tileTypeId await Server.WaitPost(() => { TestMap.MapUid = sys.CreateMap(out TestMap.MapId, runMapInit: initialized); - TestMap.Grid = Server.MapMan.CreateGridEntity(TestMap.MapId); + TestMap.Grid = sys.CreateGridEntity(TestMap.MapId); TestMap.GridCoords = new EntityCoordinates(TestMap.Grid, 0, 0); TestMap.MapCoords = new MapCoordinates(0, 0, TestMap.MapId); sys.SetTile(TestMap.Grid.Owner, TestMap.Grid.Comp, TestMap.GridCoords, new Tile(tileTypeId)); diff --git a/Robust.UnitTesting/RobustIntegrationTest.cs b/Robust.UnitTesting/RobustIntegrationTest.cs index 78aa6842f55..966635c0fe0 100644 --- a/Robust.UnitTesting/RobustIntegrationTest.cs +++ b/Robust.UnitTesting/RobustIntegrationTest.cs @@ -406,7 +406,6 @@ public abstract class IntegrationInstance : IIntegrationInstance public ISharedPlayerManager PlayerMan { get; private set; } = default!; public INetManager NetMan { get; private set; } = default!; public IGameTiming Timing { get; private set; } = default!; - public IMapManager MapMan { get; private set; } = default!; public IConsoleHost ConsoleHost { get; private set; } = default!; public ISawmill Log { get; private set; } = default!; @@ -418,7 +417,6 @@ protected virtual void ResolveIoC(IDependencyCollection deps) PlayerMan = deps.Resolve(); Timing = deps.Resolve(); NetMan = deps.Resolve(); - MapMan = deps.Resolve(); ConsoleHost = deps.Resolve(); Log = deps.Resolve().GetSawmill("test"); } diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index 71e63806ac7..bdfc90ed64f 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -161,7 +161,6 @@ public void BaseSetup() } var entMan = deps.Resolve(); - var mapMan = deps.Resolve(); // Avoid discovering EntityCommands since they may depend on systems // that aren't available in a unit test context. @@ -193,7 +192,6 @@ public void BaseSetup() // RobustUnitTest is complete hot garbage. // This makes EventTables ignore *all* the screwed up component abuse it causes. entMan.EventBus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // The nightmare never ends - mapMan.Initialize(); systems.Initialize(); deps.Resolve().LoadAssemblies(assemblies); @@ -203,7 +201,6 @@ public void BaseSetup() modLoader.TryLoadModulesFrom(ResPath.Root, ""); entMan.Startup(); - mapMan.Startup(); } [OneTimeTearDown] From 3d1cbc6e235dbe6a81d319d107e8dbf56d3d80c5 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:00:04 -0700 Subject: [PATCH 106/178] Update Lidgren.Network (#6707) * update Lidgren.Network * release notes --- Lidgren.Network/Lidgren.Network | 2 +- RELEASE-NOTES.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index 726dd552a4b..92b4d4547a0 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit 726dd552a4b104fb2b701848705f1250a073f060 +Subproject commit 92b4d4547a04c61b81b2988c818c640fe424af8f diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 1c3acc246a0..5c66169f446 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -40,11 +40,12 @@ END TEMPLATE--> ### New features -* Added support for Tracy v0.13.1 on both the client and server. Start it by changing the prof.tracy.enabled cvar to true, and connect with a v0.13.1 Tracy client! +* Lidgren now rate-limits logging. ### Bugfixes -*None yet* +* Fixed Lidgren.ChatClient/ChatServer not building properly. +* Fixed multiple sources of memory exhaustion/DOS attack surfaces in Lidgren. ### Other From 9b00e2d936d835cf1f2f6731ccf83e88c88c301a Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 2 Jul 2026 13:06:44 +1000 Subject: [PATCH 107/178] Add lidgren rate-limited logging CVars (#6708) * Add lidgren rate-limited logging CVars * rerun tests --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> --- Robust.Shared/CVars.cs | 25 ++++++++++++++++ Robust.Shared/Network/NetManager.cs | 45 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 81a4c153b4a..e3751ad0c20 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -383,6 +383,31 @@ protected CVars() public static readonly CVarDef NetLidgrenLogError = CVarDef.Create("net.lidgren_log_error", true); + /// + /// Controls whether repeated malformed network input logs from Lidgren are rate limited. + /// + public static readonly CVarDef NetLidgrenLogRateLimit = + CVarDef.Create("net.lidgren_log_rate_limit", true); + + /// + /// Bitmask of malformed network input log categories that Lidgren should rate limit. + /// + /// + public static readonly CVarDef NetLidgrenLogRateLimitTargets = + CVarDef.Create("net.lidgren_log_rate_limit_targets", (int) NetLogRateLimitTarget.All); + + /// + /// How many matching Lidgren logs are emitted per endpoint and category before suppression starts. + /// + public static readonly CVarDef NetLidgrenLogRateLimitBurst = + CVarDef.Create("net.lidgren_log_rate_limit_burst", 5); + + /// + /// Window in seconds used by Lidgren's malformed network input log rate limiter. + /// + public static readonly CVarDef NetLidgrenLogRateLimitWindow = + CVarDef.Create("net.lidgren_log_rate_limit_window", 10.0f); + /// /// If true, run network message encryption on another thread. /// diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 371f97327f5..14a58278747 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -269,6 +269,10 @@ public void Initialize(bool isServer) _config.OnValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged); _config.OnValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimit, LidgrenLogRateLimitChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); + _config.OnValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); _config.OnValueChanged(CVars.NetVerbose, NetVerboseChanged); _config.OnValueChanged(CVars.NetLogging, NetLoggingChanged); @@ -316,6 +320,38 @@ private void LidgrenLogErrorChanged(bool newValue) } } + private void LidgrenLogRateLimitChanged(bool newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimiterEnabled = newValue; + } + } + + private void LidgrenLogRateLimitTargetsChanged(int newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitTargets = (NetLogRateLimitTarget) newValue; + } + } + + private void LidgrenLogRateLimitBurstChanged(int newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitBurst = newValue; + } + } + + private void LidgrenLogRateLimitWindowChanged(float newValue) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.LogRateLimitWindow = newValue; + } + } + private void OnAuthModeChanged(int mode) { Auth = (AuthMode)mode; @@ -489,6 +525,10 @@ public void Shutdown(string reason) _config.UnsubValueChanged(CVars.NetFakeDuplicates, FakeDuplicatesChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogWarning, LidgrenLogWarningChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogError, LidgrenLogErrorChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimit, LidgrenLogRateLimitChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); + _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); _serializer.ClientHandshakeComplete -= OnSerializerOnClientHandshakeComplete; @@ -662,6 +702,11 @@ private NetPeerConfiguration _getBaseNetPeerConfig() NetIncomingMessageType.ErrorMessage, _config.GetCVar(CVars.NetLidgrenLogError)); + netConfig.LogRateLimiterEnabled = _config.GetCVar(CVars.NetLidgrenLogRateLimit); + netConfig.LogRateLimitTargets = (NetLogRateLimitTarget) _config.GetCVar(CVars.NetLidgrenLogRateLimitTargets); + netConfig.LogRateLimitBurst = _config.GetCVar(CVars.NetLidgrenLogRateLimitBurst); + netConfig.LogRateLimitWindow = _config.GetCVar(CVars.NetLidgrenLogRateLimitWindow); + var poolSize = _config.GetCVar(CVars.NetPoolSize); if (poolSize <= 0) From 098508f3a687af47e6c45ba28467ab0902a67f17 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:14:08 -0700 Subject: [PATCH 108/178] upd release notes --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5c66169f446..50e5b2583f7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -40,7 +40,7 @@ END TEMPLATE--> ### New features -* Lidgren now rate-limits logging. +* Lidgren now rate-limits logging. You can control this via the `net.lidgren_log_rate_...` CVars. ### Bugfixes From 0a6b4a6fbe71f36d32b477b5b53161bf3dcbae61 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:16:37 -0700 Subject: [PATCH 109/178] Version: 280.0.0 --- MSBuild/Robust.Engine.Version.props | 8 ++++---- RELEASE-NOTES.md | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 198c64765d7..8d8059b9b84 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - - - 279.0.1 - + + + 280.0.0 + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 50e5b2583f7..2c2ad4d8abd 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,17 +35,15 @@ END TEMPLATE--> ### Breaking changes -* Validate UIBox2i inputs -* IMapManager has been completely nuked from the codebase. Almost all of its content-facing functionality was ported to `SharedMapSystem` in https://github.com/space-wizards/RobustToolbox/pull/6579 beforehand. +*None yet* ### New features -* Lidgren now rate-limits logging. You can control this via the `net.lidgren_log_rate_...` CVars. +*None yet* ### Bugfixes -* Fixed Lidgren.ChatClient/ChatServer not building properly. -* Fixed multiple sources of memory exhaustion/DOS attack surfaces in Lidgren. +*None yet* ### Other @@ -56,6 +54,23 @@ END TEMPLATE--> *None yet* +## 280.0.0 + +### Breaking changes + +* Validate UIBox2i inputs +* IMapManager has been completely nuked from the codebase. Almost all of its content-facing functionality was ported to `SharedMapSystem` in https://github.com/space-wizards/RobustToolbox/pull/6579 beforehand. + +### New features + +* Lidgren now rate-limits logging. You can control this via the `net.lidgren_log_rate_...` CVars. + +### Bugfixes + +* Fixed Lidgren.ChatClient/ChatServer not building properly. +* Fixed multiple sources of memory exhaustion/DOS attack surfaces in Lidgren. + + ## 279.0.1 ### Bugfixes From fbef478203bc444c0df19bd52c5771c205465f0b Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 2 Jul 2026 17:43:53 -0400 Subject: [PATCH 110/178] Fix `EntitySystemSubscriptionsGeneratorAttributes` xmldocs (#6709) Fix EntitySystemSubscriptionsGeneratorAttributes xmldocs --- ...ySystemSubscriptionsGeneratorAttributes.cs | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs index 3c920aa0b7c..3a5572ec94f 100644 --- a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs +++ b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs @@ -7,49 +7,55 @@ namespace Robust.Shared.Analyzers; // These annotations direct the operation of `Robust.Shared.EntitySystemSubscriptionsGenerator`'s // `EntitySystemSubscriptionGenerator` and `EntitySystemSubscriptionGeneratorErrorAnalyser`. +/// /// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with -/// this attribute will have a EntitySystem.SubscribeLocalEvent call generated, using the method as the handler, +/// this attribute will have a call generated, using the method as the handler, /// with the event type (and component, as relevant) inferred from the method signature. -///
+///
+/// /// For this to work, the annotated method must be compatible with one of the following delegate types: -///
    -///
  • -///
  • -///
  • -///
  • -///
-///
-/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeLocalEvent. +/// +/// +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +///
[AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public sealed class SubscribeLocalEventAttribute : Attribute; +/// /// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with -/// this attribute will have a EntitySystem.SubscribeNetworkEvent call generated, using the method as the handler, +/// this attribute will have a call generated, using the method as the handler, /// with the event type inferred from the method signature. -///
+///
+/// /// For this to work, the annotated method must be compatible with one of the following delegate types: -///
    -///
  • -///
  • -///
-///
-/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeNetworkEvent. +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +///
[AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public sealed class SubscribeNetworkEventAttribute : Attribute; +/// /// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with -/// this attribute will have a EntitySystem.SubscribeAllEvent call generated, using the method as the handler, +/// this attribute will have a call generated, using the method as the handler, /// with the event type inferred from the method signature. -///
+///
+/// /// For this to work, the annotated method must be compatible with one of the following delegate types: -///
    -///
  • -///
  • -///
-///
-/// Note that this is not any different from the normal requirements to use EntitySystem.SubscribeAllEvent. +/// +/// +/// +/// +/// Note that this is not any different from the normal requirements to use . +///
[AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public sealed class EventSubscriptionAttribute : Attribute; From 6a6cff2b876f79b4ccf74385daf8019daf30ceb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=BE=D0=BB=D1=83=D0=B1=D1=8C?= <124601871+Golubgik@users.noreply.github.com> Date: Sat, 4 Jul 2026 09:06:10 +0700 Subject: [PATCH 111/178] Serialize EyeComponent.DrawLight (#6433) --- Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs | 2 +- Robust.Shared/Graphics/Eye.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs index b71418b9476..d48e425623f 100644 --- a/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs +++ b/Robust.Shared/GameObjects/Components/Eye/EyeComponent.cs @@ -30,7 +30,7 @@ public sealed partial class EyeComponent : Component [DataField, AutoNetworkedField] public bool DrawFov = true; - [AutoNetworkedField] + [DataField, AutoNetworkedField] public bool DrawLight = true; // yes it's not networked, don't ask. diff --git a/Robust.Shared/Graphics/Eye.cs b/Robust.Shared/Graphics/Eye.cs index 461fbcaecc5..d41e07fc176 100644 --- a/Robust.Shared/Graphics/Eye.cs +++ b/Robust.Shared/Graphics/Eye.cs @@ -16,7 +16,7 @@ public class Eye : IEye private MapCoordinates _coords; /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables] public bool DrawFov { get; set; } = true; /// From 38019943bb49cfc7a66550aaafdf97f67aca657a Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:14:47 +0100 Subject: [PATCH 112/178] Add cvars for new lidgren stuff (#6658) Co-authored-by: deltanedas <@deltanedas:goida.zip> --- Robust.Shared/CVars.cs | 44 +++++++++++++++++++--- Robust.Shared/Network/NetManager.cs | 57 ++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index e3751ad0c20..6b61f349cfe 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -35,6 +35,44 @@ protected CVars() public static readonly CVarDef NetMaxConnections = CVarDef.Create("net.max_connections", 256, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + /// + /// How many seconds after the last message from the server or a client before we consider it timed out. + /// + public static readonly CVarDef NetConnectionTimeout = + CVarDef.Create("net.connection_timeout", 25.0f, CVar.ARCHIVE); + + /// + /// Hard max-cap of concurrent connections per client IP address. + /// Clients will always get a disconnection packet. + /// + /// + /// This cannot be bypassed in any way, since it is used by Lidgren internally. + /// + public static readonly CVarDef NetMaxIpConnections = + CVarDef.Create("net.max_ip_connections", 8, CVar.ARCHIVE); + + /// + /// Hard max-cap of connections a single client IP address can attempt in a window of net.rapid_connection_window. + /// Clients will only get a disconnection packet the first time, their packets are dropped afterwards. + /// + /// + /// This cannot be bypassed in any way, since it is used by Lidgren internally. + /// + public static readonly CVarDef NetMaxRapidConnections = + CVarDef.Create("net.max_rapid_connections", 3, CVar.ARCHIVE); + + /// + /// How many seconds until connection count decays for net.max_rapid_connections. + /// + public static readonly CVarDef NetRapidConnectionWindow = + CVarDef.Create("net.rapid_connection_window", 60.0, CVar.ARCHIVE); + + /// + /// How many connections are "forgotten" every net.rapid_connection_window seconds. + /// + public static readonly CVarDef NetRapidConnectionDecay = + CVarDef.Create("net.rapid_connection_decay", 1, CVar.ARCHIVE); + /// /// UDP port to bind to for main game networking. /// Each address specified in net.bindto is bound with this port. @@ -298,12 +336,6 @@ protected CVars() public static readonly CVarDef NetTimeStartOffset = CVarDef.Create("net.time_start_offset", 0, CVar.SERVERONLY); - /// - /// How many seconds after the last message from the server before we consider it timed out. - /// - public static readonly CVarDef ConnectionTimeout = - CVarDef.Create("net.connection_timeout", 25.0f, CVar.ARCHIVE | CVar.CLIENTONLY); - /// /// When doing the connection handshake, how long to wait before initial connection attempt packets. /// diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 14a58278747..2f6b3eaa0d6 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -273,6 +273,11 @@ public void Initialize(bool isServer) _config.OnValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); _config.OnValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); _config.OnValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); + _config.OnValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); + _config.OnValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); + _config.OnValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.OnValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); + _config.OnValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); _config.OnValueChanged(CVars.NetVerbose, NetVerboseChanged); _config.OnValueChanged(CVars.NetLogging, NetLoggingChanged); @@ -352,6 +357,46 @@ private void LidgrenLogRateLimitWindowChanged(float newValue) } } + private void ConnectionTimeoutChanged(float timeout) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.ConnectionTimeout = timeout; + } + } + + private void MaxIpConnectionsChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumIpConnections = limit; + } + } + + private void MaxRapidConnectionsChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumRapidConnections = limit; + } + } + + private void RapidConnectionWindowChanged(double time) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.RapidConnectionWindow = time; + } + } + + private void RapidConnectionDecayChanged(int decay) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.RapidConnectionDecay = decay; + } + } + private void OnAuthModeChanged(int mode) { Auth = (AuthMode)mode; @@ -529,6 +574,11 @@ public void Shutdown(string reason) _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitTargets, LidgrenLogRateLimitTargetsChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitBurst, LidgrenLogRateLimitBurstChanged); _config.UnsubValueChanged(CVars.NetLidgrenLogRateLimitWindow, LidgrenLogRateLimitWindowChanged); + _config.UnsubValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); + _config.UnsubValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); + _config.UnsubValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.UnsubValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); + _config.UnsubValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); _serializer.ClientHandshakeComplete -= OnSerializerOnClientHandshakeComplete; @@ -732,11 +782,14 @@ private NetPeerConfiguration _getBaseNetPeerConfig() } else { - netConfig.ConnectionTimeout = _config.GetCVar(CVars.ConnectionTimeout); netConfig.ResendHandshakeInterval = _config.GetCVar(CVars.ResendHandshakeInterval); netConfig.MaximumHandshakeAttempts = _config.GetCVar(CVars.MaximumHandshakeAttempts); } - + netConfig.ConnectionTimeout = _config.GetCVar(CVars.NetConnectionTimeout); + netConfig.MaximumIpConnections = _config.GetCVar(CVars.NetMaxIpConnections); + netConfig.MaximumRapidConnections = _config.GetCVar(CVars.NetMaxRapidConnections); + netConfig.RapidConnectionWindow = _config.GetCVar(CVars.NetRapidConnectionWindow); + netConfig.RapidConnectionDecay = _config.GetCVar(CVars.NetRapidConnectionDecay); //Simulate Latency netConfig.SimulatedLoss = _config.GetCVar(CVars.NetFakeLoss); From 38a8e32029d94dfce0cc922be49cfb154fd49e8a Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Sat, 4 Jul 2026 02:15:08 -0700 Subject: [PATCH 113/178] Less restrictive box2iui validation (#6717) --- RELEASE-NOTES.md | 2 +- Robust.Shared.Maths.Tests/UIBox2i_Test.cs | 10 --------- Robust.Shared.Maths/UIBox2i.cs | 26 ----------------------- 3 files changed, 1 insertion(+), 37 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 2c2ad4d8abd..edacb1deb81 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,7 +43,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. ### Other diff --git a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs index f83843c3b7e..99a9c31e195 100644 --- a/Robust.Shared.Maths.Tests/UIBox2i_Test.cs +++ b/Robust.Shared.Maths.Tests/UIBox2i_Test.cs @@ -89,16 +89,6 @@ public void Box2iEdgesConstructor([ValueSource(nameof(Sources))] (int, int, int, } } - [Test] - public void Box2iValidatesConstruction() - { - Assert.Multiple(() => - { - Assert.Throws(() => new UIBox2i(3, 4, -1, -2)); - Assert.Throws(() => new UIBox2i(new Vector2i(3, 4), new Vector2i(-1, -2))); - }); - } - [Test] public void Box2iValidatesProperties() { diff --git a/Robust.Shared.Maths/UIBox2i.cs b/Robust.Shared.Maths/UIBox2i.cs index 41cc573748e..2fb50ecc193 100644 --- a/Robust.Shared.Maths/UIBox2i.cs +++ b/Robust.Shared.Maths/UIBox2i.cs @@ -108,21 +108,10 @@ public Vector2i BottomRight public readonly Vector2 Center => new Vector2(_left + _right, _top + _bottom) / 2f; - private static void Validate(int left, int top, int right, int bottom) - { - if (left > right) - throw new ArgumentException("Left cannot be greater than Right.", nameof(left)); - - if (top > bottom) - throw new ArgumentException("Top cannot be greater than Bottom.", nameof(top)); - } - public UIBox2i(Vector2i topLeft, Vector2i bottomRight) { Unsafe.SkipInit(out this); - Validate(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); - _topLeft = topLeft; _bottomRight = bottomRight; } @@ -131,27 +120,12 @@ public UIBox2i(int left, int top, int right, int bottom) { Unsafe.SkipInit(out this); - Validate(left, top, right, bottom); - _left = left; _right = right; _top = top; _bottom = bottom; } - /// - /// Creates a UIBox2i with no bounds validation applied, use at your own risk. - /// - internal static UIBox2i DangerousCreate(int left, int top, int right, int bottom) - { - Unsafe.SkipInit(out UIBox2i box); - box._left = left; - box._right = right; - box._top = top; - box._bottom = bottom; - return box; - } - public static UIBox2i FromDimensions(int left, int top, int width, int height) { return new UIBox2i(left, top, left + width, top + height); From ece4f7d9bdd530019476ecaaa741f16e7e10a690 Mon Sep 17 00:00:00 2001 From: mqole Date: Sat, 4 Jul 2026 21:31:37 +1000 Subject: [PATCH 114/178] add methods to support generating color palettes --- Robust.Shared.Maths/Color.cs | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs index 7174b7e1896..735e6b68259 100644 --- a/Robust.Shared.Maths/Color.cs +++ b/Robust.Shared.Maths/Color.cs @@ -589,6 +589,69 @@ public static Vector4 ToHsv(Color rgb) return new Vector4(hue, saturation, max, rgb.A); } + /// + /// Generates a list of triadic complementary colors + /// + public static List GetTriadicComplementaries(Color color) + { + return GetComplementaryColors(color, 0.120f); + } + + /// + /// Generates a list of split complementary colors + /// + public static List GetSplitComplementaries(Color color) + { + return GetComplementaryColors(color, 0.150f); + } + + /// + /// Generates a list containing the base color and two copies of a single complementary color + /// + public static List GetOneComplementary(Color color) + { + return GetComplementaryColors(color, 0.180f); + } + + /// + /// Generates a complementary colour palette for a provided + /// colour by rotating a set amount of degrees around the + /// colour wheel, and then varying the value and saturation + /// slightly. + /// + /// + /// A list of 3 colors. + /// + private static List GetComplementaryColors(Color color, float angle) + { + var hsl = ToHsl(color); + var random = new Random(); + // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. + // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! + + var hVal = hsl.X + angle; + hVal -= MathF.Floor(hVal); + var positiveHSL = new Vector4( + hVal, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16/ 100)), + hsl.W); + + var hVal1 = hsl.X - angle; + hVal1 += hVal1 <= 0f ? hVal1 + 0.360f : hVal1; + var negativeHSL = new Vector4( + hVal1, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var c0 = FromHsl(positiveHSL); + var c1 = FromHsl(negativeHSL); + + var palette = new List { color, c0, c1 }; + return palette; + } + #region Oklab/Oklch /* From 8027cbd390c4040015466d72ab586c5a97b89b32 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:25:14 +1000 Subject: [PATCH 115/178] Fix DynamicTree Clear (#6651) --- RELEASE-NOTES.md | 1 + .../Physics/B2DynamicTree_Test.cs | 34 ++++++++++++++ .../Physics/DynamicTree_Test.cs | 37 ++++++++++++++- Robust.Shared/Physics/B2DynamicTree.cs | 47 +++++++++---------- Robust.Shared/Physics/DynamicTree.cs | 5 +- 5 files changed, 96 insertions(+), 28 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index edacb1deb81..22012ded28b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,6 +43,7 @@ END TEMPLATE--> ### Bugfixes +* Fix DynamicTree.Clear not removing node references. * Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. ### Other diff --git a/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs index c632cb87dee..9b136c970cb 100644 --- a/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/B2DynamicTree_Test.cs @@ -66,5 +66,39 @@ public void AddAndQuery() } }); } + + [Test] + public void RebuildFullPreservesQueries() + { + var dt = new B2DynamicTree(); + + for (var i = 0; i < aabbs1.Length; ++i) + { + dt.CreateProxy(aabbs1[i], uint.MaxValue, i); + } + + dt.Rebuild(true); + + var point = new Vector2(0, 0); + var box = Box2.CenteredAround(point, new Vector2(0.1f, 0.1f)); + var results = new HashSet(); + + dt.Query(proxy => + { + results.Add(dt.GetUserData(proxy)); + return true; + }, box); + + Assert.Multiple(() => + { + for (var i = 0; i < aabbs1.Length; i++) + { + if (aabbs1[i].Intersects(box)) + { + Assert.That(results, Does.Contain(i)); + } + } + }); + } } } diff --git a/Robust.Shared.Tests/Physics/DynamicTree_Test.cs b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs index e73458a8f30..369c70087be 100644 --- a/Robust.Shared.Tests/Physics/DynamicTree_Test.cs +++ b/Robust.Shared.Tests/Physics/DynamicTree_Test.cs @@ -60,11 +60,12 @@ internal sealed class DynamicTree_Test [Test] public void AddAndGrow() { - var dt = new DynamicTree((in int x) => aabbs1[x], capacity: 16, growthFunc: x => x += 2); + const int proxyCapacity = 16; + var dt = new DynamicTree((in int x) => aabbs1[x], capacity: proxyCapacity, growthFunc: x => x += 2); var initCap = dt.Capacity; - Assert.That(initCap, Is.EqualTo(16)); + Assert.That(initCap, Is.EqualTo(2 * proxyCapacity - 1)); Assert.Multiple(() => { @@ -194,6 +195,38 @@ public void AddThenRemove() }); } + [Test] + public void ClearRemovesAllEntries() + { + var aabbs = new[] + { + ((Box2) default).Enlarged(1), + new Box2(float.NaN, float.NaN, float.NaN, float.NaN), + }; + + var dt = new DynamicTree((in int x) => aabbs[x], capacity: 16, growthFunc: x => x += 2); + + Assert.That(dt.Add(0), Is.True); + Assert.That(dt.Add(1), Is.True); + Assert.That(dt.Count, Is.EqualTo(2)); + + dt.Clear(); + + Assert.Multiple(() => + { + Assert.That(dt.Count, Is.Zero); + Assert.That(dt.Contains(0), Is.False); + Assert.That(dt.Contains(1), Is.False); + Assert.That(dt, Is.Empty); + }); + + Assert.Multiple(() => + { + Assert.That(dt.Add(0), Is.True); + Assert.That(dt.Add(1), Is.True); + }); + } + [Test] public void AddAndQuery() { var dt = new DynamicTree((in int x) => aabbs1[x], capacity: 16, growthFunc: x => x += 2); diff --git a/Robust.Shared/Physics/B2DynamicTree.cs b/Robust.Shared/Physics/B2DynamicTree.cs index f00eef18a79..1deb2d69c4f 100644 --- a/Robust.Shared/Physics/B2DynamicTree.cs +++ b/Robust.Shared/Physics/B2DynamicTree.cs @@ -251,7 +251,8 @@ public B2DynamicTree(float aabbExtendSize = 1f / 32, int capacity = 256, Func(stackalloc Proxy[256]); + var stack = new GrowableStack(stackalloc Proxy[TreeStackSize]); ref var baseRef = ref _nodes[0]; stack.Push(_root); @@ -2141,15 +2149,10 @@ internal void RayCastNew(RayCastInput input, long mask, ref WorldRayCastContext } else { - var stackCount = stack.GetCount(); - Assert( stackCount < 256 - 1 ); - if (stackCount < 256 - 1 ) - { - // TODO_ERIN just put one node on the stack, continue on a child node - // TODO_ERIN test ordering children by nearest to ray origin - stack.Push(node.Child1); - stack.Push(node.Child2); - } + // TODO_ERIN just put one node on the stack, continue on a child node + // TODO_ERIN test ordering children by nearest to ray origin + stack.Push(node.Child1); + stack.Push(node.Child2); } } } @@ -2205,7 +2208,7 @@ internal void ShapeCast(ShapeCastInput input, long maskBits, TreeShapeCastCallba var subInput = input; ref var baseRef = ref _nodes[0]; - var stack = new GrowableStack(stackalloc Proxy[256]); + var stack = new GrowableStack(stackalloc Proxy[TreeStackSize]); stack.Push(_root); while (stack.GetCount() > 0) @@ -2258,16 +2261,10 @@ internal void ShapeCast(ShapeCastInput input, long maskBits, TreeShapeCastCallba } else { - var stackCount = stack.GetCount(); - Assert(stackCount < 256 - 1); - - if (stackCount < 255) - { - // TODO_ERIN just put one node on the stack, continue on a child node - // TODO_ERIN test ordering children by nearest to ray origin - stack.Push(node.Child1); - stack.Push(node.Child2); - } + // TODO_ERIN just put one node on the stack, continue on a child node + // TODO_ERIN test ordering children by nearest to ray origin + stack.Push(node.Child1); + stack.Push(node.Child2); } } } diff --git a/Robust.Shared/Physics/DynamicTree.cs b/Robust.Shared/Physics/DynamicTree.cs index e4795f029c7..6f681104be3 100644 --- a/Robust.Shared/Physics/DynamicTree.cs +++ b/Robust.Shared/Physics/DynamicTree.cs @@ -103,8 +103,11 @@ public void Clear() { foreach (var proxy in _nodeLookup.Values) { - _b2Tree.DestroyProxy(proxy); + if (proxy != DynamicTree.Proxy.Free) + _b2Tree.DestroyProxy(proxy); } + + _nodeLookup.Clear(); } public bool Contains(T item) From aae482c97120f7d5acab65c1cf126066699c9949 Mon Sep 17 00:00:00 2001 From: mqole Date: Sun, 5 Jul 2026 02:01:32 +1000 Subject: [PATCH 116/178] guess we doin arrays now --- Robust.Shared.Maths/Color.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs index 735e6b68259..e3240e4cceb 100644 --- a/Robust.Shared.Maths/Color.cs +++ b/Robust.Shared.Maths/Color.cs @@ -592,7 +592,7 @@ public static Vector4 ToHsv(Color rgb) /// /// Generates a list of triadic complementary colors /// - public static List GetTriadicComplementaries(Color color) + public static Color[] GetTriadicComplementaries(Color color) { return GetComplementaryColors(color, 0.120f); } @@ -600,7 +600,7 @@ public static List GetTriadicComplementaries(Color color) /// /// Generates a list of split complementary colors /// - public static List GetSplitComplementaries(Color color) + public static Color[] GetSplitComplementaries(Color color) { return GetComplementaryColors(color, 0.150f); } @@ -608,7 +608,7 @@ public static List GetSplitComplementaries(Color color) /// /// Generates a list containing the base color and two copies of a single complementary color /// - public static List GetOneComplementary(Color color) + public static Color[] GetOneComplementary(Color color) { return GetComplementaryColors(color, 0.180f); } @@ -622,7 +622,7 @@ public static List GetOneComplementary(Color color) /// /// A list of 3 colors. /// - private static List GetComplementaryColors(Color color, float angle) + private static Color[] GetComplementaryColors(Color color, float angle) { var hsl = ToHsl(color); var random = new Random(); @@ -648,7 +648,7 @@ private static List GetComplementaryColors(Color color, float angle) var c0 = FromHsl(positiveHSL); var c1 = FromHsl(negativeHSL); - var palette = new List { color, c0, c1 }; + var palette = new Color[] { color, c0, c1 }; return palette; } From d042eb0022e8950c9a05d8e831114bd052675674 Mon Sep 17 00:00:00 2001 From: mqole Date: Sun, 5 Jul 2026 03:00:49 +1000 Subject: [PATCH 117/178] robust random --- Robust.Shared.Maths/Color.cs | 63 ---------------- .../ColorExtensions/ColorExtensions.cs | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 63 deletions(-) create mode 100644 Robust.Shared/ColorExtensions/ColorExtensions.cs diff --git a/Robust.Shared.Maths/Color.cs b/Robust.Shared.Maths/Color.cs index e3240e4cceb..7174b7e1896 100644 --- a/Robust.Shared.Maths/Color.cs +++ b/Robust.Shared.Maths/Color.cs @@ -589,69 +589,6 @@ public static Vector4 ToHsv(Color rgb) return new Vector4(hue, saturation, max, rgb.A); } - /// - /// Generates a list of triadic complementary colors - /// - public static Color[] GetTriadicComplementaries(Color color) - { - return GetComplementaryColors(color, 0.120f); - } - - /// - /// Generates a list of split complementary colors - /// - public static Color[] GetSplitComplementaries(Color color) - { - return GetComplementaryColors(color, 0.150f); - } - - /// - /// Generates a list containing the base color and two copies of a single complementary color - /// - public static Color[] GetOneComplementary(Color color) - { - return GetComplementaryColors(color, 0.180f); - } - - /// - /// Generates a complementary colour palette for a provided - /// colour by rotating a set amount of degrees around the - /// colour wheel, and then varying the value and saturation - /// slightly. - /// - /// - /// A list of 3 colors. - /// - private static Color[] GetComplementaryColors(Color color, float angle) - { - var hsl = ToHsl(color); - var random = new Random(); - // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. - // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! - - var hVal = hsl.X + angle; - hVal -= MathF.Floor(hVal); - var positiveHSL = new Vector4( - hVal, - MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), - MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16/ 100)), - hsl.W); - - var hVal1 = hsl.X - angle; - hVal1 += hVal1 <= 0f ? hVal1 + 0.360f : hVal1; - var negativeHSL = new Vector4( - hVal1, - MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), - MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), - hsl.W); - - var c0 = FromHsl(positiveHSL); - var c1 = FromHsl(negativeHSL); - - var palette = new Color[] { color, c0, c1 }; - return palette; - } - #region Oklab/Oklch /* diff --git a/Robust.Shared/ColorExtensions/ColorExtensions.cs b/Robust.Shared/ColorExtensions/ColorExtensions.cs new file mode 100644 index 00000000000..3d8e550c505 --- /dev/null +++ b/Robust.Shared/ColorExtensions/ColorExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Numerics; +using Robust.Shared.Random; +using Robust.Shared.Maths; + +namespace Robust.Shared.ColorExtensions; + +public static class ColorExtensions +{ + + /// + /// Generates a list of triadic complementary colors + /// + public static Color[] GetTriadicComplementaries(Color color) + { + return GetComplementaryColors(color, 0.120f); + } + + /// + /// Generates a list of split complementary colors + /// + public static Color[] GetSplitComplementaries(Color color) + { + return GetComplementaryColors(color, 0.150f); + } + + /// + /// Generates a list containing the base color and two copies of a single complementary color + /// + public static Color[] GetOneComplementary(Color color) + { + return GetComplementaryColors(color, 0.180f); + } + + /// + /// Generates a complementary colour palette for a provided + /// colour by rotating a set amount of degrees around the + /// colour wheel, and then varying the value and saturation + /// slightly. + /// + /// + /// A list of 3 colors. + /// + private static Color[] GetComplementaryColors(Color color, float angle) + { + var hsl = Color.ToHsl(color); + var random = new RobustRandom(); + // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. + // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! + + var hVal = hsl.X + angle; + hVal -= MathF.Floor(hVal); + var positiveHSL = new Vector4( + hVal, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16/ 100)), + hsl.W); + + var hVal1 = hsl.X - angle; + hVal1 += hVal1 <= 0f ? hVal1 + 0.360f : hVal1; + var negativeHSL = new Vector4( + hVal1, + MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), + hsl.W); + + var c0 = Color.FromHsl(positiveHSL); + var c1 = Color.FromHsl(negativeHSL); + + var palette = new Color[] { color, c0, c1 }; + return palette; + } +} From 4a8ab7e598bd6ba48f5723e2a82d8765b261481c Mon Sep 17 00:00:00 2001 From: mqole Date: Sun, 5 Jul 2026 11:41:54 +1000 Subject: [PATCH 118/178] schmoovin --- .../{ColorExtensions => Utility}/ColorExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename Robust.Shared/{ColorExtensions => Utility}/ColorExtensions.cs (87%) diff --git a/Robust.Shared/ColorExtensions/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs similarity index 87% rename from Robust.Shared/ColorExtensions/ColorExtensions.cs rename to Robust.Shared/Utility/ColorExtensions.cs index 3d8e550c505..5b36ffe169b 100644 --- a/Robust.Shared/ColorExtensions/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -3,7 +3,7 @@ using Robust.Shared.Random; using Robust.Shared.Maths; -namespace Robust.Shared.ColorExtensions; +namespace Robust.Shared.Utility; public static class ColorExtensions { @@ -11,7 +11,7 @@ public static class ColorExtensions /// /// Generates a list of triadic complementary colors /// - public static Color[] GetTriadicComplementaries(Color color) + public static Color[] GetTriadicComplementaries(this Color color) { return GetComplementaryColors(color, 0.120f); } @@ -19,7 +19,7 @@ public static Color[] GetTriadicComplementaries(Color color) /// /// Generates a list of split complementary colors /// - public static Color[] GetSplitComplementaries(Color color) + public static Color[] GetSplitComplementaries(this Color color) { return GetComplementaryColors(color, 0.150f); } @@ -27,7 +27,7 @@ public static Color[] GetSplitComplementaries(Color color) /// /// Generates a list containing the base color and two copies of a single complementary color /// - public static Color[] GetOneComplementary(Color color) + public static Color[] GetOneComplementary(this Color color) { return GetComplementaryColors(color, 0.180f); } @@ -41,7 +41,7 @@ public static Color[] GetOneComplementary(Color color) /// /// A list of 3 colors. /// - private static Color[] GetComplementaryColors(Color color, float angle) + public static Color[] GetComplementaryColors(Color color, float angle) { var hsl = Color.ToHsl(color); var random = new RobustRandom(); From 249660be3c573df04c743768eb4162a79684e97c Mon Sep 17 00:00:00 2001 From: mqole Date: Mon, 6 Jul 2026 19:03:24 +1000 Subject: [PATCH 119/178] angels --- Robust.Shared/Utility/ColorExtensions.cs | 30 +++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index 5b36ffe169b..f4fcb34c538 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -7,13 +7,16 @@ namespace Robust.Shared.Utility; public static class ColorExtensions { + private static readonly float TriadicHueDelta = 120 / 360; // +/- 1/3. 120 degrees, 0.333 over hue + private static readonly float SplitComplementaryHueDelta = 150 / 360; // +/- 5/12. 150 degrees, 0.4166... over hue + private static readonly float ComplementaryHueDelta = 180 / 360; // +/- 1/2. 180 degrees /// /// Generates a list of triadic complementary colors /// public static Color[] GetTriadicComplementaries(this Color color) { - return GetComplementaryColors(color, 0.120f); + return GetComplementaryColors(color, TriadicHueDelta); } /// @@ -21,7 +24,7 @@ public static Color[] GetTriadicComplementaries(this Color color) /// public static Color[] GetSplitComplementaries(this Color color) { - return GetComplementaryColors(color, 0.150f); + return GetComplementaryColors(color, SplitComplementaryHueDelta); } /// @@ -29,35 +32,40 @@ public static Color[] GetSplitComplementaries(this Color color) /// public static Color[] GetOneComplementary(this Color color) { - return GetComplementaryColors(color, 0.180f); + return GetComplementaryColors(color, ComplementaryHueDelta); } /// - /// Generates a complementary colour palette for a provided - /// colour by rotating a set amount of degrees around the - /// colour wheel, and then varying the value and saturation + /// Generates a complementary color palette for a provided + /// color by rotating a set amount of degrees around the + /// color wheel, and then varying the value and saturation /// slightly. /// /// /// A list of 3 colors. /// - public static Color[] GetComplementaryColors(Color color, float angle) + public static Color[] GetComplementaryColors(Color color, float hueDelta) { var hsl = Color.ToHsl(color); var random = new RobustRandom(); + // sorry about how messy these are, but to get all random values we need to reroll for positive and negative HSL. // since we want to rotate x degrees around the colour wheel, we need to do so in both directions- doing x + x degrees will give us the wrong hue! - var hVal = hsl.X + angle; + // also varying the saturation and lightness just a little to add some contrast. + // since 'color' is our main color, we are desaturating our secondary colors. + // this means our main color will always stand out the most. + + var hVal = hsl.X + hueDelta; hVal -= MathF.Floor(hVal); var positiveHSL = new Vector4( hVal, MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), - MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16/ 100)), + MathHelper.Clamp01(hsl.Z + random.Next(-15 / 100, 16 / 100)), hsl.W); - var hVal1 = hsl.X - angle; - hVal1 += hVal1 <= 0f ? hVal1 + 0.360f : hVal1; + var hVal1 = hsl.X - hueDelta; + hVal1 += hVal1 <= 0f ? hVal1 + 1f : hVal1; var negativeHSL = new Vector4( hVal1, MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), From 622ecc624125778476829fa647dbea94f03067cd Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:41:09 +1000 Subject: [PATCH 120/178] Fix command ordering (#6729) --- RELEASE-NOTES.md | 1 + .../CustomControls/DebugConsole.xaml.Completions.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 22012ded28b..d2619189ea4 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -45,6 +45,7 @@ END TEMPLATE--> * Fix DynamicTree.Clear not removing node references. * Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. +* Fix command completions not being ordered. The list will still populate by any commands that contain the supplied arg. ### Other diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs index a6fcbe1626f..25f15c6909e 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.Completions.cs @@ -263,6 +263,7 @@ private CompletionOption[] FilterCompletions(IEnumerable compl { return completions .Where(c => c.Value.Contains(curTyping, StringComparison.CurrentCultureIgnoreCase)) + .OrderByDescending(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase)) .ToArray(); } From 89375019519cb96b3d715efe6fb37fe2c6b02472 Mon Sep 17 00:00:00 2001 From: mqole Date: Mon, 6 Jul 2026 20:09:41 +1000 Subject: [PATCH 121/178] tests --- .../Utility/ColorExtensionsTest.cs | 56 +++++++++++++++++++ Robust.Shared/Utility/ColorExtensions.cs | 8 +-- 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 Robust.Shared.Tests/Utility/ColorExtensionsTest.cs diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs new file mode 100644 index 00000000000..8345da49f3b --- /dev/null +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -0,0 +1,56 @@ +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Utility; + +namespace Robust.Shared.Tests.Utility; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +[TestOf(typeof(ColorExtensions))] +internal sealed class ColorExtensionsTest +{ + [Test] + public void TestTriadicPalette() + { + var palette = ColorExtensions.GetTriadicComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.33333f)); + Assert.That(Color.ToHsl(palette[2]).X, Is.EqualTo(0.66667f)); + } + } + + [Test] + public void TestSplitComplementaryPalette() + { + var palette = ColorExtensions.GetSplitComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.41667f)); + Assert.That(Color.ToHsl(palette[2]).X, Is.InRange(0.58333f, 0.58334f)); // TODO FIX THIS + } + } + + [Test] + public void TestComplementaryPalette() + { + var palette = ColorExtensions.GetOneComplementary(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.5f)); + Assert.That(Color.ToHsl(palette[2]).X, Is.EqualTo(0.5f)); + } + } +} diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index f4fcb34c538..5c08afdbe6b 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -7,9 +7,9 @@ namespace Robust.Shared.Utility; public static class ColorExtensions { - private static readonly float TriadicHueDelta = 120 / 360; // +/- 1/3. 120 degrees, 0.333 over hue - private static readonly float SplitComplementaryHueDelta = 150 / 360; // +/- 5/12. 150 degrees, 0.4166... over hue - private static readonly float ComplementaryHueDelta = 180 / 360; // +/- 1/2. 180 degrees + private static readonly float TriadicHueDelta = 0.33333f; // +/- 1/3. 120 degrees, 0.333 over hue + private static readonly float SplitComplementaryHueDelta = 0.41667f; // +/- 5/12. 150 degrees, 0.4166... over hue + private static readonly float ComplementaryHueDelta = 0.5f; // +/- 1/2. 180 degrees /// /// Generates a list of triadic complementary colors @@ -65,7 +65,7 @@ public static Color[] GetComplementaryColors(Color color, float hueDelta) hsl.W); var hVal1 = hsl.X - hueDelta; - hVal1 += hVal1 <= 0f ? hVal1 + 1f : hVal1; + hVal1 += hVal1 <= 0f ? 1f : 0f; var negativeHSL = new Vector4( hVal1, MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), From e9b28d80ca8933facc8575e2d43285db285048d5 Mon Sep 17 00:00:00 2001 From: Errant <35878406+Errant-4@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:51:52 +0200 Subject: [PATCH 122/178] Update lidgren ee2e945 (#6736) --- Lidgren.Network/Lidgren.Network | 2 +- RELEASE-NOTES.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index 92b4d4547a0..ee2e94572a1 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit 92b4d4547a04c61b81b2988c818c640fe424af8f +Subproject commit ee2e94572a169581546f9be17e459975a1be58b7 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index d2619189ea4..966510e6a01 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -46,6 +46,7 @@ END TEMPLATE--> * Fix DynamicTree.Clear not removing node references. * Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. * Fix command completions not being ordered. The list will still populate by any commands that contain the supplied arg. +* Lidgren rate-limit settings were tweaked to make it less likely that players will unintentionally trigger it ### Other From b35ef54877d79f7cb1cfb2cefd14291f15fb5cc8 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:37:03 -0700 Subject: [PATCH 123/178] upd release notes --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 966510e6a01..b4521ac1a6c 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -50,7 +50,7 @@ END TEMPLATE--> ### Other -*None yet* +* `EyeComponent.DrawLight` is now serialized. ### Internal From b366e29750a1b41352bc0ca0c7c47b618df36838 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:38:22 -0700 Subject: [PATCH 124/178] Version: 280.0.1 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 8d8059b9b84..881173b40b1 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 280.0.0 + 280.0.1 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index b4521ac1a6c..92531aa33eb 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,6 +43,21 @@ END TEMPLATE--> ### Bugfixes +*None yet* + +### Other + +*None yet* + +### Internal + +*None yet* + + +## 280.0.1 + +### Bugfixes + * Fix DynamicTree.Clear not removing node references. * Reverted validation for `UiBox2i` `ctor`s as it was causing regressions in debug UIs. * Fix command completions not being ordered. The list will still populate by any commands that contain the supplied arg. @@ -52,10 +67,6 @@ END TEMPLATE--> * `EyeComponent.DrawLight` is now serialized. -### Internal - -*None yet* - ## 280.0.0 From 4e69bf267a121a7414ce438289a98aa652f8b3de Mon Sep 17 00:00:00 2001 From: mqole Date: Tue, 7 Jul 2026 00:45:10 +1000 Subject: [PATCH 125/178] yeas --- .../Utility/ColorExtensionsTest.cs | 15 +++++++++------ Robust.Shared/Utility/ColorExtensions.cs | 9 +++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs index 8345da49f3b..ca6bae9d0bb 100644 --- a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -19,8 +19,9 @@ public void TestTriadicPalette() Assert.That(palette, Has.Length.EqualTo(3)); Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); - Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.33333f)); - Assert.That(Color.ToHsl(palette[2]).X, Is.EqualTo(0.66667f)); + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.TriadicHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.TriadicHueDelta)); } } @@ -34,8 +35,9 @@ public void TestSplitComplementaryPalette() Assert.That(palette, Has.Length.EqualTo(3)); Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); - Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.41667f)); - Assert.That(Color.ToHsl(palette[2]).X, Is.InRange(0.58333f, 0.58334f)); // TODO FIX THIS + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.SplitComplementaryHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.SplitComplementaryHueDelta)); } } @@ -49,8 +51,9 @@ public void TestComplementaryPalette() Assert.That(palette, Has.Length.EqualTo(3)); Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); - Assert.That(Color.ToHsl(palette[1]).X, Is.EqualTo(0.5f)); - Assert.That(Color.ToHsl(palette[2]).X, Is.EqualTo(0.5f)); + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.ComplementaryHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.ComplementaryHueDelta)); } } } diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index 5c08afdbe6b..50840f9ca55 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -7,9 +7,9 @@ namespace Robust.Shared.Utility; public static class ColorExtensions { - private static readonly float TriadicHueDelta = 0.33333f; // +/- 1/3. 120 degrees, 0.333 over hue - private static readonly float SplitComplementaryHueDelta = 0.41667f; // +/- 5/12. 150 degrees, 0.4166... over hue - private static readonly float ComplementaryHueDelta = 0.5f; // +/- 1/2. 180 degrees + public static readonly float TriadicHueDelta = 120f / 360f; // +/- 1/3. 120 degrees, 0.333 over hue + public static readonly float SplitComplementaryHueDelta = 150f / 360f; // +/- 5/12. 150 degrees, 0.4166... over hue + public static readonly float ComplementaryHueDelta = 180f / 360f; // +/- 1/2. 180 degrees /// /// Generates a list of triadic complementary colors @@ -65,7 +65,8 @@ public static Color[] GetComplementaryColors(Color color, float hueDelta) hsl.W); var hVal1 = hsl.X - hueDelta; - hVal1 += hVal1 <= 0f ? 1f : 0f; + if (hVal1 < 0f) + hVal1 += 1f; var negativeHSL = new Vector4( hVal1, MathHelper.Clamp01(hsl.Y + random.Next(-20 / 100, 0)), From a1d3d16e43f8f38ff1f116988db0995f831535cf Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:11:53 +1000 Subject: [PATCH 126/178] Update lidgren (#6737) --- Lidgren.Network/Lidgren.Network | 2 +- RELEASE-NOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index ee2e94572a1..04678d057cc 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit ee2e94572a169581546f9be17e459975a1be58b7 +Subproject commit 04678d057cc503f14f49801a725e61cfe27790a0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 92531aa33eb..d896cce1758 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Update Lidgren.Network to 04678d057cc503f14f49801a725e61cfe27790a0 with additional fixes around MTU handling, NAT handling, and malformed packets. ### New features From 332222b1345b5e2618fe1257b5e75b9cb01292ac Mon Sep 17 00:00:00 2001 From: Whatstone <166147148+whatston3@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:56:35 -0400 Subject: [PATCH 127/178] Add missing EntitySystemSubscriptionsGenerator.targets into Server.csproj (#6740) add missing SubGenerator targets to Server proj --- Robust.Server/Robust.Server.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Robust.Server/Robust.Server.csproj b/Robust.Server/Robust.Server.csproj index 4ff270ca482..21caba7222d 100644 --- a/Robust.Server/Robust.Server.csproj +++ b/Robust.Server/Robust.Server.csproj @@ -70,6 +70,7 @@ + From cd20266069a37f52c0e77f2cf891211d1ab8d105 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:59:02 +1000 Subject: [PATCH 128/178] Add new Lidgen CVars (#6742) --- RELEASE-NOTES.md | 2 +- Robust.Shared/CVars.cs | 18 ++++++++++++++++ Robust.Shared/Network/NetManager.cs | 33 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index d896cce1758..28f20934641 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Exposed new Lidgren properties. ### Bugfixes diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 6b61f349cfe..dbc41ade9f0 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -61,6 +61,12 @@ protected CVars() public static readonly CVarDef NetMaxRapidConnections = CVarDef.Create("net.max_rapid_connections", 3, CVar.ARCHIVE); + /// + /// Whether Lidgren should send disconnection reasons when rejecting connections due to internal limits. + /// + public static readonly CVarDef NetSendConnectionRejectionReasons = + CVarDef.Create("net.send_connection_rejection_reasons", true, CVar.ARCHIVE); + /// /// How many seconds until connection count decays for net.max_rapid_connections. /// @@ -146,6 +152,18 @@ protected CVars() public static readonly CVarDef NetMtuExpandFailAttempts = CVarDef.Create("net.mtu_expand_fail_attempts", 5, CVar.ARCHIVE); + /// + /// Maximum bytes used by incomplete Lidgren fragment groups for one connection. + /// + public static readonly CVarDef NetMaxFragmentReassemblyBytesPerConnection = + CVarDef.Create("net.max_fragment_reassembly_bytes_per_connection", 32 * 1024 * 1024, CVar.ARCHIVE); + + /// + /// How many seconds Lidgren keeps an incomplete fragment group alive. + /// + public static readonly CVarDef NetFragmentGroupTimeout = + CVarDef.Create("net.fragment_group_timeout", 30.0f, CVar.ARCHIVE); + /// /// Whether to enable verbose debug logging in Lidgren. /// diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 2f6b3eaa0d6..7df4fe1e6a6 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -276,8 +276,11 @@ public void Initialize(bool isServer) _config.OnValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); _config.OnValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); _config.OnValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.OnValueChanged(CVars.NetSendConnectionRejectionReasons, SendConnectionRejectionReasonsChanged); _config.OnValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); _config.OnValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); + _config.OnValueChanged(CVars.NetMaxFragmentReassemblyBytesPerConnection, MaxFragmentReassemblyBytesPerConnectionChanged); + _config.OnValueChanged(CVars.NetFragmentGroupTimeout, FragmentGroupTimeoutChanged); _config.OnValueChanged(CVars.NetVerbose, NetVerboseChanged); _config.OnValueChanged(CVars.NetLogging, NetLoggingChanged); @@ -381,6 +384,14 @@ private void MaxRapidConnectionsChanged(int limit) } } + private void SendConnectionRejectionReasonsChanged(bool sendReasons) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.SendConnectionRejectionReasons = sendReasons; + } + } + private void RapidConnectionWindowChanged(double time) { foreach (var netPeer in _netPeers) @@ -397,6 +408,22 @@ private void RapidConnectionDecayChanged(int decay) } } + private void MaxFragmentReassemblyBytesPerConnectionChanged(int limit) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.MaximumFragmentReassemblyBytesPerConnection = limit; + } + } + + private void FragmentGroupTimeoutChanged(float timeout) + { + foreach (var netPeer in _netPeers) + { + netPeer.Peer.Configuration.FragmentGroupTimeout = timeout; + } + } + private void OnAuthModeChanged(int mode) { Auth = (AuthMode)mode; @@ -577,8 +604,11 @@ public void Shutdown(string reason) _config.UnsubValueChanged(CVars.NetConnectionTimeout, ConnectionTimeoutChanged); _config.UnsubValueChanged(CVars.NetMaxIpConnections, MaxIpConnectionsChanged); _config.UnsubValueChanged(CVars.NetMaxRapidConnections, MaxRapidConnectionsChanged); + _config.UnsubValueChanged(CVars.NetSendConnectionRejectionReasons, SendConnectionRejectionReasonsChanged); _config.UnsubValueChanged(CVars.NetRapidConnectionWindow, RapidConnectionWindowChanged); _config.UnsubValueChanged(CVars.NetRapidConnectionDecay, RapidConnectionDecayChanged); + _config.UnsubValueChanged(CVars.NetMaxFragmentReassemblyBytesPerConnection, MaxFragmentReassemblyBytesPerConnectionChanged); + _config.UnsubValueChanged(CVars.NetFragmentGroupTimeout, FragmentGroupTimeoutChanged); _serializer.ClientHandshakeComplete -= OnSerializerOnClientHandshakeComplete; @@ -788,6 +818,7 @@ private NetPeerConfiguration _getBaseNetPeerConfig() netConfig.ConnectionTimeout = _config.GetCVar(CVars.NetConnectionTimeout); netConfig.MaximumIpConnections = _config.GetCVar(CVars.NetMaxIpConnections); netConfig.MaximumRapidConnections = _config.GetCVar(CVars.NetMaxRapidConnections); + netConfig.SendConnectionRejectionReasons = _config.GetCVar(CVars.NetSendConnectionRejectionReasons); netConfig.RapidConnectionWindow = _config.GetCVar(CVars.NetRapidConnectionWindow); netConfig.RapidConnectionDecay = _config.GetCVar(CVars.NetRapidConnectionDecay); @@ -807,6 +838,8 @@ private NetPeerConfiguration _getBaseNetPeerConfig() netConfig.AutoExpandMTU = _config.GetCVar(CVars.NetMtuExpand); netConfig.ExpandMTUFrequency = _config.GetCVar(CVars.NetMtuExpandFrequency); netConfig.ExpandMTUFailAttempts = _config.GetCVar(CVars.NetMtuExpandFailAttempts); + netConfig.MaximumFragmentReassemblyBytesPerConnection = _config.GetCVar(CVars.NetMaxFragmentReassemblyBytesPerConnection); + netConfig.FragmentGroupTimeout = _config.GetCVar(CVars.NetFragmentGroupTimeout); return netConfig; } From 1acad0a449e6b6601b03e79c5867ee5c752eb91f Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:50:54 -0700 Subject: [PATCH 129/178] upd release notes --- RELEASE-NOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 28f20934641..f7d37d63c4b 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,15 +35,15 @@ END TEMPLATE--> ### Breaking changes -* Update Lidgren.Network to 04678d057cc503f14f49801a725e61cfe27790a0 with additional fixes around MTU handling, NAT handling, and malformed packets. +* Updated Lidgren.Network to `04678d057cc503f14f49801a725e61cfe27790a0` with additional fixes around MTU handling, NAT handling, and malformed packets. ### New features -* Exposed new Lidgren properties. +* Exposed new Lidgren properties/CVARs for the above-mentioned fixes and previous updates around rate-limit settings. ### Bugfixes -*None yet* +* Fixed `EntitySystemSubscriptionsGenerator` not targeting server-side `SubscribeLocalEvent`/`SubscribeNetworkEvent` attributes. ### Other From 51913660b2ad5bff3b48809b1cc5200d2047af26 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:55:12 -0700 Subject: [PATCH 130/178] Version: 281.0.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 881173b40b1..96f24dffb61 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 280.0.1 + 281.0.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index f7d37d63c4b..922d8f07188 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,15 +35,15 @@ END TEMPLATE--> ### Breaking changes -* Updated Lidgren.Network to `04678d057cc503f14f49801a725e61cfe27790a0` with additional fixes around MTU handling, NAT handling, and malformed packets. +*None yet* ### New features -* Exposed new Lidgren properties/CVARs for the above-mentioned fixes and previous updates around rate-limit settings. +*None yet* ### Bugfixes -* Fixed `EntitySystemSubscriptionsGenerator` not targeting server-side `SubscribeLocalEvent`/`SubscribeNetworkEvent` attributes. +*None yet* ### Other @@ -54,6 +54,21 @@ END TEMPLATE--> *None yet* +## 281.0.0 + +### Breaking changes + +* Updated Lidgren.Network to `04678d057cc503f14f49801a725e61cfe27790a0` with additional fixes around MTU handling, NAT handling, and malformed packets. + +### New features + +* Exposed new Lidgren properties/CVARs for the above-mentioned fixes and previous updates around rate-limit settings. + +### Bugfixes + +* Fixed `EntitySystemSubscriptionsGenerator` not targeting server-side `SubscribeLocalEvent`/`SubscribeNetworkEvent` attributes. + + ## 280.0.1 ### Bugfixes From 83c5c8eaf2c094231fe87ba07113673974be98b4 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:36:28 +1000 Subject: [PATCH 131/178] Fix UserDataDir accepting relative game names (#6745) --- Robust.Client/Utility/UserDataDir.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Robust.Client/Utility/UserDataDir.cs b/Robust.Client/Utility/UserDataDir.cs index 9ebcaad46c7..893a8435014 100644 --- a/Robust.Client/Utility/UserDataDir.cs +++ b/Robust.Client/Utility/UserDataDir.cs @@ -1,6 +1,8 @@ using System; using System.IO; using JetBrains.Annotations; +using Robust.Shared.ContentPack; +using Robust.Shared.Utility; namespace Robust.Client.Utility { @@ -35,7 +37,8 @@ public static string GetRootUserDataDir(IGameControllerInternal gameController) appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); #endif - return Path.Combine(appDataDir, gameController.Options.UserDataDirectoryName); + + return PathHelpers.SafeGetResourcePath(appDataDir, new ResPath(gameController.Options.UserDataDirectoryName)); } } } From 471e92d923f7897883347cf509b3c31799430634 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:32:56 +1000 Subject: [PATCH 132/178] Update lidgren (#6750) --- Lidgren.Network/Lidgren.Network | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index 04678d057cc..68a5b883d5c 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit 04678d057cc503f14f49801a725e61cfe27790a0 +Subproject commit 68a5b883d5c3f5d4eabda8f04a2f4fd1afce12f4 From 47572c2e521e58c6acd5ea48283e2e35eca362da Mon Sep 17 00:00:00 2001 From: Whatstone Date: Tue, 7 Jul 2026 08:22:08 -0400 Subject: [PATCH 133/178] make TableContainer virtual and public --- Robust.Client/UserInterface/Controls/TableContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Robust.Client/UserInterface/Controls/TableContainer.cs b/Robust.Client/UserInterface/Controls/TableContainer.cs index 4d0625f7653..6474c82f5b4 100644 --- a/Robust.Client/UserInterface/Controls/TableContainer.cs +++ b/Robust.Client/UserInterface/Controls/TableContainer.cs @@ -4,7 +4,8 @@ namespace Robust.Client.UserInterface.Controls; -internal sealed class TableContainer : Container +[Virtual] +public class TableContainer : Container { private int _columns = 1; From ba96b63f990cab33a9b0e1af15c1bf401bbc9515 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:44:27 +1000 Subject: [PATCH 134/178] Fix ShapeCast on sensors (#6588) * Filter hard-only fixtures in sensor-only raycasts * Align * Tests * raycast --- .../Physics/RayCast_Test.cs | 119 ++++++++++++++++++ .../Physics/Systems/RayCastSystem.Geometry.cs | 5 + 2 files changed, 124 insertions(+) diff --git a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs index ee531eb2163..9aafdab131d 100644 --- a/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/RayCast_Test.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Numerics; using NUnit.Framework; @@ -17,6 +18,13 @@ namespace Robust.UnitTesting.Shared.Physics; [TestFixture] internal sealed class RayCast_Test { + public enum SensorCastKind + { + RayAll, + RayClosest, + Shape, + } + private static TestCaseData[] _rayCases = { // Ray goes through @@ -55,6 +63,34 @@ internal sealed class RayCast_Test new(new SlimPolygon(Box2.UnitCentered), new Transform(Vector2.Zero, Angle.Zero), -Vector2.UnitY, null), }; + private static TestCaseData[] _sensorCases = + { + new TestCaseData(SensorCastKind.RayAll, true, false) + .SetName("RayCast all returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, true, true) + .SetName("RayCast all returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, false, false) + .SetName("RayCast all filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayAll, false, true) + .SetName("RayCast all returns sensor fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, true, false) + .SetName("RayCast closest returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, true, true) + .SetName("RayCast closest returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, false, false) + .SetName("RayCast closest filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.RayClosest, false, true) + .SetName("RayCast closest returns sensor fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.Shape, true, false) + .SetName("ShapeCast returns hard fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.Shape, true, true) + .SetName("ShapeCast returns hard fixtures with sensor query flag"), + new TestCaseData(SensorCastKind.Shape, false, false) + .SetName("ShapeCast filters sensor fixtures without sensor query flag"), + new TestCaseData(SensorCastKind.Shape, false, true) + .SetName("ShapeCast returns sensor fixtures with sensor query flag"), + }; + [Test, TestCaseSource(nameof(_rayCases))] public void RayCast(Vector2 origin, Vector2 direction, Vector2? point) { @@ -113,6 +149,55 @@ public void ShapeCast(IPhysShape shape, Transform origin, Vector2 direction, Vec } } + [Test, TestCaseSource(nameof(_sensorCases))] + public void SensorFixtureCasts(SensorCastKind kind, bool hardFixture, bool includeSensors) + { + var sim = RobustServerSimulation.NewSimulation().RegisterEntitySystems(f => + { + f.LoadExtraSystemType(); + }).InitializeInstance(); + SetupSensorFixture(sim, out var mapId, out var target, hardFixture); + var raycast = sim.System(); + + var flags = QueryFlags.Dynamic | QueryFlags.Static; + if (includeSensors) + flags |= QueryFlags.Sensors; + + var filter = new QueryFilter + { + LayerBits = 1, + Flags = flags, + }; + + var hits = kind switch + { + SensorCastKind.RayAll => raycast.CastRay( + mapId, + Vector2.UnitX / 2f, + Vector2.UnitY * 3f, + filter), + SensorCastKind.RayClosest => raycast.CastRayClosest( + mapId, + Vector2.UnitX / 2f, + Vector2.UnitY * 3f, + filter), + SensorCastKind.Shape => raycast.CastShape( + mapId, + new PhysShapeCircle(0.1f), + new Transform(Vector2.UnitX / 2f, Angle.Zero), + Vector2.UnitY * 3f, + filter, + RayCastSystem.RayCastAllCallback), + _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null), + }; + + var expected = hardFixture || includeSensors + ? new[] { target } + : Array.Empty(); + + Assert.That(hits.Results.Select(hit => hit.Entity).ToArray(), Is.EqualTo(expected)); + } + private void Setup(ISimulation sim, out MapId mapId) { var entManager = sim.Resolve(); @@ -142,4 +227,38 @@ private void Setup(ISimulation sim, out MapId mapId) entManager.System().SetLocalRotation(grid.Owner, Angle.FromDegrees(90)); entManager.System().SetLocalPosition(grid.Owner, Vector2.UnitX / 2f); } + + private void SetupSensorFixture(ISimulation sim, out MapId mapId, out EntityUid target, bool hard) + { + var entManager = sim.Resolve(); + var mapSystem = entManager.System(); + var fixtureSystem = entManager.System(); + var physicsSystem = entManager.System(); + + mapSystem.CreateMap(out mapId); + var grid = mapSystem.CreateGridEntity(mapId); + + for (var i = 0; i < 3; i++) + { + mapSystem.SetTile(grid, new Vector2i(0, i), new Tile(1)); + } + + target = SpawnCastTarget(entManager, fixtureSystem, physicsSystem, grid.Owner, new Vector2(0.5f, 1f), hard); + } + + private EntityUid SpawnCastTarget( + IEntityManager entManager, + FixtureSystem fixtureSystem, + SharedPhysicsSystem physicsSystem, + EntityUid parent, + Vector2 position, + bool hard) + { + var uid = entManager.SpawnEntity(null, new EntityCoordinates(parent, position)); + var physics = entManager.AddComponent(uid); + fixtureSystem.CreateFixture(uid, "fix1", new Fixture(new PhysShapeCircle(0.25f), 1, 1, hard)); + physicsSystem.SetCanCollide(uid, true, body: physics); + Assert.That(physics.CanCollide); + return uid; + } } diff --git a/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs b/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs index cd523d29aef..912e8e072c1 100644 --- a/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs +++ b/Robust.Shared/Physics/Systems/RayCastSystem.Geometry.cs @@ -112,6 +112,11 @@ private static float RayCastCallback(RayCastInput input, FixtureProxy proxy, ref return input.MaxFraction; } + if ((worldContext.Filter.Flags & QueryFlags.Sensors) == 0x0 && !proxy.Fixture.Hard) + { + return input.MaxFraction; + } + if (worldContext.Filter.IsIgnored?.Invoke(proxy.Entity) == true) { return input.MaxFraction; From 34ce2ba5346440e3566f633f1111ec9f9a42cdff Mon Sep 17 00:00:00 2001 From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:18:35 -0700 Subject: [PATCH 135/178] serv5 (#6496) * Add source generator for DataDefinition validation * Add source generator for reading * Add source generator for writing * Include prototypes and other meansdatadefinition types * Target ISerializationGenerated in data definitions * Murder * Use array, struct, enum methods * Source generate get field definitions * serv5 * Fix and pray * Fix release compile * Tayrtahn review * a * Generator bugfix --------- Co-authored-by: metalgearsloth --- .../DataDefinitionAnalyzerTest.cs | 23 - Robust.Analyzers/DataDefinitionAnalyzer.cs | 194 +-- Robust.Analyzers/DataDefinitionFixer.cs | 31 +- Robust.Roslyn.Shared/AttributeHelper.cs | 5 + Robust.Roslyn.Shared/DataDefinitionHelper.cs | 349 ++++ Robust.Roslyn.Shared/Diagnostics.cs | 3 +- .../Helpers/DataFieldAttribute.cs | 18 + Robust.Roslyn.Shared/TypeSymbolHelper.cs | 14 + .../DataDefinition.cs | 11 +- Robust.Serialization.Generator/DataField.cs | 18 +- Robust.Serialization.Generator/Generator.cs | 1464 +++++++++++++---- Robust.Serialization.Generator/Types.cs | 357 ++-- .../GlobalUsings.cs | 4 +- .../PropertyAndFieldDefinitionTest.cs | 8 +- Robust.Shared/Audio/SoundSpecifier.cs | 6 +- .../EntitySerialization/EntityDeserializer.cs | 1 - .../Components/Renderable/SpriteLayerData.cs | 2 +- .../UserInterface/UserInterfaceComponent.cs | 2 +- .../Components/PhysicsComponent.Physics.cs | 2 +- Robust.Shared/Physics/Shapes/Polygon.cs | 3 +- Robust.Shared/Physics/Shapes/SlimPolygon.cs | 3 +- Robust.Shared/Prototypes/EntityPrototype.cs | 16 +- .../Serialization/ISerializationGenerated.cs | 80 +- .../Definition/DataDefinition.Delegates.cs | 31 - .../Definition/DataDefinition.Emitters.cs | 513 ------ .../Manager/Definition/DataDefinition.cs | 370 +---- .../Definition/DataDefinitionDelegates.cs | 43 + .../Definition/DataDefinitionUtility.cs | 3 + .../Manager/Definition/DataFieldDefinition.cs | 27 + .../Manager/Definition/FieldDefinition.cs | 50 - .../Manager/Definition/InheritanceBehavior.cs | 6 +- .../Manager/ISerializationManager.cs | 33 + .../SerializationManager.Composition.cs | 8 +- .../Manager/SerializationManager.Copying.cs | 55 +- .../SerializationManager.Instantiation.cs | 21 +- .../Manager/SerializationManager.Reading.cs | 977 ++++++++++- ...SerializationManager.SerializerProvider.cs | 63 +- .../SerializationManager.Validation.cs | 190 +-- .../Manager/SerializationManager.Writing.cs | 21 +- .../Manager/SerializationManager.cs | 84 +- .../Utility/InternalReflectionUtils.cs | 40 - 41 files changed, 3343 insertions(+), 1806 deletions(-) create mode 100644 Robust.Roslyn.Shared/DataDefinitionHelper.cs create mode 100644 Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs delete mode 100644 Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs delete mode 100644 Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs create mode 100644 Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs create mode 100644 Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs delete mode 100644 Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs diff --git a/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs b/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs index 7e49abc0131..cf417d53502 100644 --- a/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs +++ b/Robust.Analyzers.Tests/DataDefinitionAnalyzerTest.cs @@ -94,29 +94,6 @@ await Verifier(code, ); } - [Test] - public async Task ReadOnlyFieldTest() - { - const string code = """ - using Robust.Shared.Serialization.Manager.Attributes; - - [DataDefinition] - public sealed partial class Foo - { - [DataField] - public readonly int Bad; - - [DataField] - public int Good; - } - """; - - await Verifier(code, - // /0/Test0.cs(7,12): error RA0019: Data field Bad in data definition Foo is readonly - VerifyCS.Diagnostic(DataDefinitionAnalyzer.DataFieldWritableRule).WithSpan(7, 12, 7, 20).WithArguments("Bad", "Foo") - ); - } - [Test] public async Task PartialDataDefinitionTest() { diff --git a/Robust.Analyzers/DataDefinitionAnalyzer.cs b/Robust.Analyzers/DataDefinitionAnalyzer.cs index d5f3fd62267..40959273d6e 100644 --- a/Robust.Analyzers/DataDefinitionAnalyzer.cs +++ b/Robust.Analyzers/DataDefinitionAnalyzer.cs @@ -1,11 +1,13 @@ #nullable enable using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Robust.Roslyn.Shared; +using Robust.Roslyn.Shared.Helpers; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.ViewVariables; @@ -14,9 +16,6 @@ namespace Robust.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer { - private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; - private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; - private const string MeansDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataDefinitionAttribute"; private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; private const string ViewVariablesNamespace = "Robust.Shared.ViewVariables.ViewVariablesAttribute"; private const string NotYamlSerializableName = "Robust.Shared.Serialization.Manager.Attributes.NotYamlSerializableAttribute"; @@ -43,16 +42,6 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer "Make sure to mark any type containing a nested data definition as partial." ); - public static readonly DiagnosticDescriptor DataFieldWritableRule = new( - Diagnostics.IdDataFieldWritable, - "Data field must not be readonly", - "Data field {0} in data definition {1} is readonly", - "Usage", - DiagnosticSeverity.Error, - true, - "Make sure to remove the readonly modifier." - ); - public static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new( Diagnostics.IdDataFieldPropertyWritable, "Data field property must have a setter", @@ -93,9 +82,24 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer "Make sure to use a type that is YAML serializable." ); + public static readonly DiagnosticDescriptor DataFieldOutsideDefinition = new( + Diagnostics.IdDataFieldOutsideDefinition, + "Data field defined in a type that is not marked as a data definition or data record", + "Data field {0} is defined in type {1} which is not a data definition or data record", + "Usage", + DiagnosticSeverity.Error, + true, + "Make sure to add a data definition or data record attribute, or inherit a type or add an attribute that implicitly makes its inheritors data definitions or data records." + ); + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( - DataDefinitionPartialRule, NestedDataDefinitionPartialRule, DataFieldWritableRule, DataFieldPropertyWritableRule, - DataFieldRedundantTagRule, DataFieldNoVVReadWriteRule, DataFieldYamlSerializableRule + DataDefinitionPartialRule, + NestedDataDefinitionPartialRule, + DataFieldPropertyWritableRule, + DataFieldRedundantTagRule, + DataFieldNoVVReadWriteRule, + DataFieldYamlSerializableRule, + DataFieldOutsideDefinition ); public override void Initialize(AnalysisContext context) @@ -108,6 +112,9 @@ public override void Initialize(AnalysisContext context) if (symbolContext.Symbol is not INamedTypeSymbol typeSymbol) return; + symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration); + symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration); + if (!IsDataDefinition(typeSymbol)) return; @@ -116,9 +123,6 @@ public override void Initialize(AnalysisContext context) symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordDeclaration); symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.RecordStructDeclaration); symbolContext.RegisterSyntaxNodeAction(AnalyzeDataDefinition, SyntaxKind.InterfaceDeclaration); - - symbolContext.RegisterSyntaxNodeAction(AnalyzeDataField, SyntaxKind.FieldDeclaration); - symbolContext.RegisterSyntaxNodeAction(AnalyzeDataFieldProperty, SyntaxKind.PropertyDeclaration); }, SymbolKind.NamedType); } @@ -156,6 +160,7 @@ private static void AnalyzeDataField(SyntaxNodeAnalysisContext context) if (context.ContainingSymbol?.ContainingType is not INamedTypeSymbol type) return; + var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord); foreach (var variable in field.Declaration.Variables) { var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable); @@ -163,13 +168,13 @@ private static void AnalyzeDataField(SyntaxNodeAnalysisContext context) if (fieldSymbol == null) continue; - if (!IsDataField(fieldSymbol, out _, out var datafieldAttribute)) + if (!DataDefinitionHelper.IsDataField(fieldSymbol, isDataRecord, out _, out var datafieldAttribute)) continue; - if (IsReadOnlyDataField(type, fieldSymbol)) + if (!isDataDefinition) { - TryGetModifierLocation(field, SyntaxKind.ReadOnlyKeyword, out var location); - context.ReportDiagnostic(Diagnostic.Create(DataFieldWritableRule, location, fieldSymbol.Name, type.Name)); + var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, fieldSymbol.Name, type.Name)); } if (HasRedundantTag(fieldSymbol, datafieldAttribute)) @@ -212,19 +217,30 @@ private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context) if (propertySymbol.ContainingType is not INamedTypeSymbol type) return; - if (type.IsRecord || type.IsValueType) + if (propertySymbol == null) return; - if (propertySymbol == null) + var isDataDefinition = DataDefinitionHelper.IsDataDefinition(type, out var isDataRecord); + if (isDataDefinition && + !isDataRecord && + propertySymbol.SetMethod == null && + HasDataFieldAttribute(propertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + DataFieldPropertyWritableRule, + property.AccessorList?.GetLocation() ?? property.GetLocation(), + propertySymbol.Name, + type.Name)); return; + } - if (!IsDataField(propertySymbol, out _, out var datafieldAttribute)) + if (!DataDefinitionHelper.IsDataField(propertySymbol, isDataRecord, out _, out var datafieldAttribute)) return; - if (IsReadOnlyDataField(type, propertySymbol)) + if (!isDataDefinition) { - var location = property.AccessorList != null ? property.AccessorList.GetLocation() : property.GetLocation(); - context.ReportDiagnostic(Diagnostic.Create(DataFieldPropertyWritableRule, location, propertySymbol.Name, type.Name)); + var location = type.DeclaringSyntaxReferences[0].GetSyntax().GetLocation(); + context.ReportDiagnostic(Diagnostic.Create(DataFieldOutsideDefinition, location, propertySymbol.Name, type.Name)); } if (HasRedundantTag(propertySymbol, datafieldAttribute)) @@ -255,9 +271,16 @@ private static void AnalyzeDataFieldProperty(SyntaxNodeAnalysisContext context) } } - private static bool IsReadOnlyDataField(ITypeSymbol type, ISymbol field) + private static bool HasDataFieldAttribute(ISymbol symbol) { - return IsReadOnlyMember(type, field); + foreach (var attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is { } attributeClass && + TypeSymbolHelper.Inherits(attributeClass, DataFieldBaseNamespace)) + return true; + } + + return false; } private static bool IsPartial(TypeDeclarationSyntax type) @@ -270,53 +293,7 @@ private static bool IsDataDefinition(ITypeSymbol? type) if (type == null) return false; - return HasAttribute(type, DataDefinitionNamespace) || - MeansDataDefinition(type) || - IsImplicitDataDefinition(type); - } - - private static bool IsDataField(ISymbol member, out ITypeSymbol type, out AttributeData attribute) - { - // TODO data records and other attributes - if (member is IFieldSymbol field) - { - foreach (var attr in field.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = field.Type; - attribute = attr; - return true; - } - } - } - else if (member is IPropertySymbol property) - { - foreach (var attr in property.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = property.Type; - attribute = attr; - return true; - } - } - } - - type = null!; - attribute = null!; - return false; - } - - private static bool Inherits(ITypeSymbol type, string parent) - { - foreach (var baseType in GetBaseTypes(type)) - { - if (baseType.ToDisplayString() == parent) - return true; - } - - return false; + return DataDefinitionHelper.IsDataDefinition(type, out _); } private static bool TryGetAttributeLocation(MemberDeclarationSyntax syntax, string attributeName, out Location location) @@ -382,14 +359,14 @@ private static bool HasAttribute(ITypeSymbol type, string attributeName) return false; } - private static bool HasRedundantTag(ISymbol symbol, AttributeData datafieldAttribute) + private static bool HasRedundantTag(ISymbol symbol, DataFieldAttribute datafieldAttribute) { // No args, no problem - if (datafieldAttribute.ConstructorArguments.Length == 0) + if (datafieldAttribute.Data is not { ConstructorArguments.Length: > 0 }) return false; // If a tag is explicitly specified, it will be the first argument... - var tagArgument = datafieldAttribute.ConstructorArguments[0]; + var tagArgument = datafieldAttribute.Data.ConstructorArguments[0]; // ...but the first arg could also something else, since tag is optional // so we make sure that it's a string if (tagArgument.Value is not string explicitName) @@ -427,65 +404,8 @@ private static bool HasVVReadWrite(ISymbol symbol) return (VVAccess)accessByte == VVAccess.ReadWrite; } - private static bool MeansDataDefinition(ITypeSymbol type) - { - foreach (var attribute in type.GetAttributes()) - { - if (attribute.AttributeClass is null) - continue; - - if (HasAttribute(attribute.AttributeClass, MeansDataDefinitionNamespace)) - return true; - } - return false; - } - private static bool IsNotYamlSerializable(ISymbol field, ITypeSymbol type) { return HasAttribute(type, NotYamlSerializableName); } - - private static bool IsImplicitDataDefinition(ITypeSymbol type) - { - if (HasAttribute(type, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var baseType in GetBaseTypes(type)) - { - if (HasAttribute(baseType, ImplicitDataDefinitionNamespace)) - return true; - } - - foreach (var @interface in type.AllInterfaces) - { - if (IsImplicitDataDefinitionInterface(@interface)) - return true; - } - - return false; - } - - private static bool IsImplicitDataDefinitionInterface(ITypeSymbol @interface) - { - if (HasAttribute(@interface, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var subInterface in @interface.AllInterfaces) - { - if (HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) - return true; - } - - return false; - } - - private static IEnumerable GetBaseTypes(ITypeSymbol type) - { - var baseType = type.BaseType; - while (baseType != null) - { - yield return baseType; - baseType = baseType.BaseType; - } - } } diff --git a/Robust.Analyzers/DataDefinitionFixer.cs b/Robust.Analyzers/DataDefinitionFixer.cs index d474d28528e..218b346b340 100644 --- a/Robust.Analyzers/DataDefinitionFixer.cs +++ b/Robust.Analyzers/DataDefinitionFixer.cs @@ -18,7 +18,7 @@ public sealed class DefinitionFixer : CodeFixProvider private const string ViewVariablesAttributeName = "ViewVariables"; public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( - IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldWritable, IdDataFieldPropertyWritable, + IdDataDefinitionPartial, IdNestedDataDefinitionPartial, IdDataFieldPropertyWritable, IdDataFieldRedundantTag, IdDataFieldNoVVReadWrite ); @@ -32,8 +32,6 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) return RegisterPartialTypeFix(context, diagnostic); case IdNestedDataDefinitionPartial: return RegisterPartialTypeFix(context, diagnostic); - case IdDataFieldWritable: - return RegisterDataFieldFix(context, diagnostic); case IdDataFieldPropertyWritable: return RegisterDataFieldPropertyFix(context, diagnostic); case IdDataFieldRedundantTag: @@ -182,22 +180,6 @@ private static async Task RemoveVVAttribute(Document document, MemberD return document.WithSyntaxRoot(root); } - private static async Task RegisterDataFieldFix(CodeFixContext context, Diagnostic diagnostic) - { - var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); - var span = diagnostic.Location.SourceSpan; - var field = root?.FindToken(span.Start).Parent?.AncestorsAndSelf().OfType().FirstOrDefault(); - - if (field == null) - return; - - context.RegisterCodeFix(CodeAction.Create( - "Make data field writable", - c => MakeFieldWritable(context.Document, field, c), - "Make data field writable" - ), diagnostic); - } - private static async Task RegisterDataFieldPropertyFix(CodeFixContext context, Diagnostic diagnostic) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); @@ -214,17 +196,6 @@ private static async Task RegisterDataFieldPropertyFix(CodeFixContext context, D ), diagnostic); } - private static async Task MakeFieldWritable(Document document, FieldDeclarationSyntax declaration, CancellationToken cancellation) - { - var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation); - var token = declaration.Modifiers.First(t => t.IsKind(ReadOnlyKeyword)); - var newDeclaration = declaration.WithModifiers(declaration.Modifiers.Remove(token)); - - root = root!.ReplaceNode(declaration, newDeclaration); - - return document.WithSyntaxRoot(root); - } - private static async Task MakePropertyWritable(Document document, PropertyDeclarationSyntax declaration, CancellationToken cancellation) { var root = (CompilationUnitSyntax?) await document.GetSyntaxRootAsync(cancellation); diff --git a/Robust.Roslyn.Shared/AttributeHelper.cs b/Robust.Roslyn.Shared/AttributeHelper.cs index ac8101aac69..d7983629684 100644 --- a/Robust.Roslyn.Shared/AttributeHelper.cs +++ b/Robust.Roslyn.Shared/AttributeHelper.cs @@ -25,6 +25,11 @@ public static bool HasAttribute(ISymbol symbol, string attributeMetadataName, [N return false; } + public static bool HasAttribute(ISymbol symbol, string attributeMetadataName) + { + return HasAttribute(symbol, attributeMetadataName, out _); + } + public static bool GetNamedArgumentBool(AttributeData data, string name, bool defaultValue) { foreach (var kv in data.NamedArguments) diff --git a/Robust.Roslyn.Shared/DataDefinitionHelper.cs b/Robust.Roslyn.Shared/DataDefinitionHelper.cs new file mode 100644 index 00000000000..1401a3278bb --- /dev/null +++ b/Robust.Roslyn.Shared/DataDefinitionHelper.cs @@ -0,0 +1,349 @@ +#nullable enable +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared.Helpers; + +namespace Robust.Roslyn.Shared; + +public sealed class DataDefinitionHelper +{ + private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; + private const string DataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataRecordAttribute"; + private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; + private const string ImplicitDataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataRecordAttribute"; + private const string MeansDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataDefinitionAttribute"; + private const string MeansDataRecordNamespace = "Robust.Shared.Serialization.Manager.Attributes.MeansDataRecordAttribute"; + private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; + private const string DataFieldAttributeName = "Robust.Shared.Serialization.Manager.Attributes.DataFieldAttribute"; + private const string IdDataFieldAttributeName = "Robust.Shared.Prototypes.IdDataFieldAttribute"; + private const string ParentDataFieldAttributeName = "Robust.Shared.Prototypes.ParentDataFieldAttribute"; + private const string AbstractDataFieldAttributeName = "Robust.Shared.Prototypes.AbstractDataFieldAttribute"; + private const string IncludeDataFieldAttributeName = "Robust.Shared.Serialization.Manager.Attributes.IncludeDataFieldAttribute"; + private const string AlwaysPushInheritanceAttributeName = "Robust.Shared.Serialization.Manager.Attributes.AlwaysPushInheritanceAttribute"; + private const string NeverPushInheritanceAttributeName = "Robust.Shared.Serialization.Manager.Attributes.NeverPushInheritanceAttribute"; + + public static (bool Definition, bool Record) IsImplicitDataDefinitionInterface(ITypeSymbol @interface) + { + if (AttributeHelper.HasAttribute(@interface, ImplicitDataRecordNamespace)) + return (true, true); + + var isDefinition = false; + foreach (var subInterface in @interface.AllInterfaces) + { + if (AttributeHelper.HasAttribute(subInterface, ImplicitDataRecordNamespace)) + return (true, true); + + if (AttributeHelper.HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) + isDefinition = true; + } + + return (isDefinition || AttributeHelper.HasAttribute(@interface, ImplicitDataDefinitionNamespace), false); + } + + public static (bool Definition, bool Record) IsImplicitDataDefinition(ITypeSymbol type) + { + var isDefinition = false; + foreach (var attribute in type.GetAttributes()) + { + if (attribute.AttributeClass is not { } attributeClass) + continue; + + var str = attributeClass.ToDisplayString(); + switch (str) + { + case ImplicitDataRecordNamespace: + return (true, true); + case ImplicitDataDefinitionNamespace: + isDefinition = true; + break; + } + + foreach (var subAttribute in attributeClass.GetAttributes()) + { + if (subAttribute.AttributeClass is not { } subAttributeClass) + continue; + + var subStr = subAttributeClass.ToDisplayString(); + if (subStr is MeansDataRecordNamespace) + return (true, true); + + if (subStr is MeansDataDefinitionNamespace) + isDefinition = true; + } + } + + foreach (var baseType in TypeSymbolHelper.GetBaseTypes(type)) + { + if (AttributeHelper.HasAttribute(baseType, ImplicitDataRecordNamespace)) + return (true, true); + + if (AttributeHelper.HasAttribute(baseType, ImplicitDataDefinitionNamespace)) + isDefinition = true; + } + + foreach (var @interface in type.AllInterfaces) + { + var impl = IsImplicitDataDefinitionInterface(@interface); + if (impl.Record) + return (true, true); + + if (impl.Definition) + isDefinition = true; + } + + return (isDefinition, false); + } + + public static bool IsDataDefinition([NotNullWhen(true)] ITypeSymbol? type, out bool isDataRecord) + { + isDataRecord = false; + if (type == null) + return false; + + isDataRecord = AttributeHelper.HasAttribute(type, DataRecordNamespace); + if (isDataRecord) + return true; + + var (isImplicitDefinition, isImplicitRecord) = IsImplicitDataDefinition(type); + if (isImplicitRecord) + isDataRecord = true; + + return isImplicitDefinition || + isImplicitRecord || + AttributeHelper.HasAttribute(type, DataDefinitionNamespace); + } + + + public static DataFieldAttribute? GetDataFieldAttribute(AttributeData data, string fieldName) + { + if (data.AttributeClass == null) + return null; + + string? name = null; + var readOnly = false; + var priority = 1; + var include = false; + var isDataFieldAttribute = false; + var required = false; + var serverOnly = false; + + // (string? tag = null, bool readOnly = false, int priority = 1, bool required = false, bool serverOnly = false, Type? customTypeSerializer = null) + if (data.AttributeClass.ToDisplayString().Contains(DataFieldAttributeName)) + { + name = GetAttributeArgument(data, 0, "tag", name); + readOnly = GetAttributeArgument(data, 1, "readOnly", readOnly); + priority = GetAttributeArgument(data, 2, "priority", priority); + isDataFieldAttribute = true; + required = GetAttributeArgument(data, 3, "required", required); + serverOnly = GetAttributeArgument(data, 4, "serverOnly", serverOnly); + } + // (int priority = 1, Type? customTypeSerializer = null) + else if (data.AttributeClass.ToDisplayString().Contains(IdDataFieldAttributeName)) + { + name = "id"; + priority = GetAttributeArgument(data, 0, "priority", priority); + isDataFieldAttribute = true; + } + // (Type prototypeIdSerializer, int priority = 1) + else if (data.AttributeClass.ToDisplayString().Contains(ParentDataFieldAttributeName)) + { + name = "parent"; + priority = GetAttributeArgument(data, 1, "priority", priority); + isDataFieldAttribute = true; + } + // (int priority = 1) + else if (data.AttributeClass.ToDisplayString().Contains(AbstractDataFieldAttributeName)) + { + name = "abstract"; + priority = GetAttributeArgument(data, 0, "priority", priority); + isDataFieldAttribute = true; + } + // (bool readOnly = false, int priority = 1, bool serverOnly = false, Type? customTypeSerializer = null) + else if (data.AttributeClass.ToDisplayString().Contains(IncludeDataFieldAttributeName)) + { + var span = fieldName.AsSpan(); + name = $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; + readOnly = GetAttributeArgument(data, 0, "readOnly", readOnly); + priority = GetAttributeArgument(data, 1, "priority", priority); + include = true; + serverOnly = GetAttributeArgument(data, 2, "serverOnly", serverOnly); + } + + var camelCasedName = ToCamelCase(fieldName); + if (string.IsNullOrWhiteSpace(name)) + name = ToCamelCase(camelCasedName); + + return new DataFieldAttribute( + data, + name!, + readOnly, + priority, + include, + isDataFieldAttribute, + required, + serverOnly, + camelCasedName + ); + } + + private static T GetAttributeArgument(AttributeData data, int constructorIndex, string namedArgument, T defaultValue) + { + if (constructorIndex < data.ConstructorArguments.Length) + { + var argument = data.ConstructorArguments[constructorIndex]; + if (!argument.IsNull && argument.Value is T value) + return value; + } + + foreach (var named in data.NamedArguments) + { + if (named.Key == namedArgument && !named.Value.IsNull && named.Value.Value is T value) + return value; + } + + return defaultValue; + } + + public static bool IsDataField( + ImmutableArray attributes, + string name, + bool isImplicitlyDeclared, + bool isStatic, + bool hasSetter, + bool isRecord, + out DataFieldAttribute? attribute, + out int inheritanceBehavior) + { + attribute = null; + inheritanceBehavior = 0; + if (isStatic) + return false; + + foreach (var attr in attributes) + { + if (attr.AttributeClass is not { } attributeClass) + continue; + + if (TypeSymbolHelper.Inherits(attributeClass, DataFieldBaseNamespace)) + attribute = GetDataFieldAttribute(attr, name); + + if (attributeClass.ToDisplayString() == AlwaysPushInheritanceAttributeName) + inheritanceBehavior = 1; + else if (attributeClass.ToDisplayString() == NeverPushInheritanceAttributeName) + inheritanceBehavior = 2; + } + + if (attribute != null) + attribute = attribute with { InheritanceBehavior = inheritanceBehavior }; + + if (isImplicitlyDeclared || !hasSetter || !isRecord) + return attribute != null; + + name = ToCamelCase(name); + attribute = new DataFieldAttribute( + null, + name, + false, + 1, + false, + true, + false, + false, + name, + inheritanceBehavior + ); + + return true; + } + + public static bool IsDataField( + ISymbol member, + bool isDataRecord, + out ITypeSymbol type, + [NotNullWhen(true)] out DataFieldAttribute? attribute) + { + // TODO data records and other attributes + type = null!; + attribute = null; + var inheritanceBehavior = 0; + switch (member) + { + case IFieldSymbol field: + { + var associatedProperty = field.AssociatedSymbol as IPropertySymbol; + var isDataRecordBackingField = isDataRecord && + field.IsImplicitlyDeclared && + associatedProperty != null; + var fieldName = isDataRecordBackingField + ? associatedProperty!.Name + : field.Name; + + if (IsDataField(field.GetAttributes(), + fieldName, + isDataRecordBackingField ? false : field.IsImplicitlyDeclared, + field.IsStatic, + true, + isDataRecord, + out attribute, + out inheritanceBehavior)) + type = field.Type; + + break; + } + case IPropertySymbol property: + { + var hasSetter = property.SetMethod != null; + if (IsDataField(property.GetAttributes(), + property.Name, + property.IsImplicitlyDeclared, + property.IsStatic, + hasSetter, + isDataRecord, + out attribute, + out inheritanceBehavior)) + type = property.Type; + + break; + } + } + + if (attribute != null) + attribute = attribute with { InheritanceBehavior = inheritanceBehavior }; + + return attribute != null; + } + + private static bool IsAutoProperty(IPropertySymbol property) + { + foreach (var reference in property.DeclaringSyntaxReferences) + { + if (reference.GetSyntax() is not PropertyDeclarationSyntax syntax) + continue; + + if (syntax.ExpressionBody != null) + return false; + + if (syntax.AccessorList == null) + return false; + + return syntax.AccessorList.Accessors.All(accessor => + accessor.Body == null && + accessor.ExpressionBody == null && + accessor.SemicolonToken.RawKind != 0); + } + + return false; + } + + public static string ToCamelCase(string name) + { + if (name == "ID") + return "id"; + + var span = name.AsSpan(); + return $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; + } +} diff --git a/Robust.Roslyn.Shared/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs index c521ab4832f..03b44dab2a5 100644 --- a/Robust.Roslyn.Shared/Diagnostics.cs +++ b/Robust.Roslyn.Shared/Diagnostics.cs @@ -22,7 +22,7 @@ public static class Diagnostics public const string IdValueEventRaisedByRef = "RA0016"; public const string IdDataDefinitionPartial = "RA0017"; public const string IdNestedDataDefinitionPartial = "RA0018"; - public const string IdDataFieldWritable = "RA0019"; + public const string IdDataFieldWritable = "RA0019"; // No longer used. public const string IdDataFieldPropertyWritable = "RA0020"; public const string IdComponentPauseNotComponent = "RA0021"; public const string IdComponentPauseNoFields = "RA0022"; @@ -60,6 +60,7 @@ public static class Diagnostics public const string IdInvalidAMethodSignatureForGeneratedSubscription = "RA0054"; public const string IdInvalidContainingTypeForGeneratedSubscription = "RA0055"; public const string IdNonPartialContainingTypeForGeneratedSubscription = "RA0056"; + public const string IdDataFieldOutsideDefinition = "RA0057"; public static SuppressionDescriptor MeansImplicitAssignment => new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned."); diff --git a/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs b/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs new file mode 100644 index 00000000000..771223ccc38 --- /dev/null +++ b/Robust.Roslyn.Shared/Helpers/DataFieldAttribute.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; + +#nullable enable + +namespace Robust.Roslyn.Shared.Helpers; + +public sealed record DataFieldAttribute( + AttributeData? Data, + string Tag, + bool ReadOnly, + int Priority, + bool Include, + bool IsDataFieldAttribute, + bool Required, + bool ServerOnly, + string CamelCasedName, + int InheritanceBehavior = 0 +); diff --git a/Robust.Roslyn.Shared/TypeSymbolHelper.cs b/Robust.Roslyn.Shared/TypeSymbolHelper.cs index 878c98af2e3..d2702dc9bfb 100644 --- a/Robust.Roslyn.Shared/TypeSymbolHelper.cs +++ b/Robust.Roslyn.Shared/TypeSymbolHelper.cs @@ -96,4 +96,18 @@ public static bool Inherits(ITypeSymbol type, ITypeSymbol other) } return false; } + + /// + /// Checks if the given inherits from a type with the given metadata name. + /// + public static bool Inherits(ITypeSymbol type, string otherTypeName) + { + foreach (var baseType in GetBaseTypes(type)) + { + if (ShittyTypeMatch(baseType, otherTypeName)) + return true; + } + + return false; + } } diff --git a/Robust.Serialization.Generator/DataDefinition.cs b/Robust.Serialization.Generator/DataDefinition.cs index e33fbc6c0a3..cd851ea8702 100644 --- a/Robust.Serialization.Generator/DataDefinition.cs +++ b/Robust.Serialization.Generator/DataDefinition.cs @@ -1,6 +1,11 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; namespace Robust.Serialization.Generator; -public sealed record DataDefinition(ITypeSymbol Type, string GenericTypeName, List Fields, bool HasHooks, bool InvalidFields); +public sealed record DataDefinition( + ITypeSymbol Type, + string GenericTypeName, + List Fields, + bool HasHooks, + bool InvalidFields, + bool IsRecord); diff --git a/Robust.Serialization.Generator/DataField.cs b/Robust.Serialization.Generator/DataField.cs index 820901eaee4..ff5e44dd6a9 100644 --- a/Robust.Serialization.Generator/DataField.cs +++ b/Robust.Serialization.Generator/DataField.cs @@ -1,14 +1,26 @@ using Microsoft.CodeAnalysis; +using Robust.Roslyn.Shared.Helpers; namespace Robust.Serialization.Generator; public sealed record DataField( ISymbol Symbol, ITypeSymbol Type, - (INamedTypeSymbol Serializer, CustomSerializerType Type)? CustomSerializer); + DataFieldAttribute Attribute, + (INamedTypeSymbol Serializer, CustomSerializerType Type)? CustomSerializer +); +[Flags] public enum CustomSerializerType { - Copier, - CopyCreator + None = 0, + Copier = 1 << 0, + CopyCreator = 1 << 1, + MappingValidator = 1 << 2, + SequenceValidator = 1 << 3, + ValueValidator = 1 << 4, + MappingReader = 1 << 5, + SequenceReader = 1 << 6, + ValueReader = 1 << 7, + Writer = 1 << 8, } diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 357ee629cfe..b94f70dc6b8 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -1,8 +1,11 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Robust.Roslyn.Shared.DataDefinitionHelper; using static Robust.Serialization.Generator.CustomSerializerType; using static Robust.Serialization.Generator.Types; @@ -11,11 +14,28 @@ namespace Robust.Serialization.Generator; [Generator] public class Generator : IIncrementalGenerator { - private const string TypeCopierInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier"; - private const string TypeCopyCreatorInterfaceNamespace = "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator"; + private const string TypeCopierInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopier"; + + private const string TypeCopyCreatorInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeCopyCreator"; + + private const string TypeValidatorInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeValidator"; + + private const string TypeReaderInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeReader"; + + private const string TypeWriterInterfaceNamespace = + "Robust.Shared.Serialization.TypeSerializers.Interfaces.ITypeWriter"; + private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks"; private const string AutoStateAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute"; private const string ComponentDeltaInterfaceName = "Robust.Shared.GameObjects.IComponentDelta"; + private const string MappingDataNodeName = "Robust.Shared.Serialization.Markdown.Mapping.MappingDataNode"; + private const string SequenceDataNodeName = "Robust.Shared.Serialization.Markdown.Sequence.SequenceDataNode"; + private const string ValueDataNodeName = "Robust.Shared.Serialization.Markdown.Value.ValueDataNode"; + private const string EntityUidName = "Robust.Shared.GameObjects.EntityUid"; public void Initialize(IncrementalGeneratorInitializationContext initContext) { @@ -26,10 +46,14 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) { var type = (TypeDeclarationSyntax)context.Node; var symbol = (ITypeSymbol)context.SemanticModel.GetDeclaredSymbol(type)!; - if (!IsDataDefinition(symbol)) + + if (symbol.TypeKind == TypeKind.Interface || + !IsDataDefinition(symbol, out var isDataRecord)) + { return null; + } - return GenerateForDataDefinition(type, symbol); + return GenerateForDataDefinition(type, symbol, isDataRecord); } ) .Where(static type => type != null); @@ -55,7 +79,8 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) private static (string, string)? GenerateForDataDefinition( TypeDeclarationSyntax declaration, - ITypeSymbol type) + ITypeSymbol type, + bool isDataRecord) { var builder = new StringBuilder(); var containingTypes = new Stack(); @@ -94,19 +119,28 @@ private static (string, string)? GenerateForDataDefinition( containingTypesEnd.AppendLine("}"); } - var definition = GetDataDefinition(type); + var definition = GetDataDefinition(type, isDataRecord); if (nonPartial || definition.InvalidFields) return null; builder.AppendLine($$""" #nullable enable using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Diagnostics.CodeAnalysis; using Robust.Shared.Analyzers; using Robust.Shared.IoC; using Robust.Shared.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager; + using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; + using Robust.Shared.Serialization.Markdown; + using Robust.Shared.Serialization.Markdown.Mapping; + using Robust.Shared.Serialization.Markdown.Sequence; + using Robust.Shared.Serialization.Markdown.Validation; + using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; #pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable CS0612 // Type or member is obsolete @@ -119,188 +153,431 @@ private static (string, string)? GenerateForDataDefinition( {{GetPartialTypeDefinitionLine(type)}} : ISerializationGenerated<{{definition.GenericTypeName}}> { - {{GetConstructor(definition)}} - - {{GetCopyMethods(definition)}} + {{GetConstructors(definition)}} {{GetInstantiators(definition)}} + + {{GetCopiers(definition)}} + + {{GetReader(definition)}} + + {{GetWriter(definition)}} + + {{GetValidator(definition)}} + + {{GetFieldDefinitions(definition)}} } {{containingTypesEnd}} """); - return ($"{symbolName}.g.cs", builder.ToString()); + return ($"{symbolName}.g.cs", NormalizeSource(builder.ToString())); } - private static DataDefinition GetDataDefinition(ITypeSymbol definition) + private static string NormalizeSource(string source) { - var fields = new List(); - var invalidFields = false; + return SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(); + } - foreach (var member in definition.GetMembers()) + private static void GetDataFields( + ITypeSymbol definition, + bool isDataRecord, + List fields, + List symbols, + ref bool invalidFields) + { + foreach (var (field, fieldType, attribute) in GetAllDataFields(definition, isDataRecord)) { - if (member is not IFieldSymbol && member is not IPropertySymbol) - continue; + var existingIndex = fields.FindIndex(existing => + SymbolEqualityComparer.Default.Equals(existing.Symbol, field)); + if (existingIndex != -1) + { + if (fields[existingIndex].Attribute.Data != null || attribute.Data == null) + continue; - if (member.IsStatic) - continue; + fields.RemoveAt(existingIndex); + } - if (IsDataField(member, out var type, out var attribute)) + if (!IsDataDefinition(field.ContainingType, out _)) + invalidFields = true; + + if (attribute.Data?.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is + INamedTypeSymbol customSerializer) { - if (attribute.ConstructorArguments.FirstOrDefault(arg => arg.Kind == TypedConstantKind.Type).Value is INamedTypeSymbol customSerializer) + var serializerType = None; + if (ImplementsInterface(customSerializer, TypeCopierInterfaceNamespace)) + serializerType |= Copier; + else if (ImplementsInterface(customSerializer, TypeCopyCreatorInterfaceNamespace)) + serializerType |= CopyCreator; + + if (ImplementsInterface(customSerializer, TypeValidatorInterfaceNamespace, symbols)) { - if (ImplementsInterface(customSerializer, TypeCopierInterfaceNamespace)) + foreach (var symbol in symbols) { - fields.Add(new DataField(member, type, (customSerializer, Copier))); - continue; + if (symbol.IsGenericType && + symbol.TypeArguments is { Length: >= 2 } arguments) + { + var nodeType = arguments[1]; + if (nodeType.ToDisplayString().Contains(MappingDataNodeName)) + serializerType |= MappingValidator; + + if (nodeType.ToDisplayString().Contains(SequenceDataNodeName)) + serializerType |= SequenceValidator; + + if (nodeType.ToDisplayString().Contains(ValueDataNodeName)) + serializerType |= ValueValidator; + } } - else if (ImplementsInterface(customSerializer, TypeCopyCreatorInterfaceNamespace)) + } + + if (ImplementsInterface(customSerializer, TypeReaderInterfaceNamespace, symbols)) + { + foreach (var symbol in symbols) { - fields.Add(new DataField(member, type, (customSerializer, CopyCreator))); - continue; + if (symbol.IsGenericType && + symbol.TypeArguments is { Length: >= 2 } arguments) + { + var nodeType = arguments[1]; + if (nodeType.ToDisplayString().Contains(MappingDataNodeName)) + serializerType |= MappingReader; + + if (nodeType.ToDisplayString().Contains(SequenceDataNodeName)) + serializerType |= SequenceReader; + + if (nodeType.ToDisplayString().Contains(ValueDataNodeName)) + serializerType |= ValueReader; + } } } - fields.Add(new DataField(member, type, null)); + if (ImplementsInterface(customSerializer, TypeWriterInterfaceNamespace, symbols)) + serializerType |= Writer; - if (IsReadOnlyMember(definition, type)) + if (serializerType != None) { - invalidFields = true; + fields.Add(new DataField(field, fieldType, attribute, (customSerializer, serializerType))); + continue; } } + + fields.Add(new DataField(field, fieldType, attribute, null)); + + if (IsReadOnlyMember(definition, fieldType)) + invalidFields = true; } + } + + private static DataDefinition GetDataDefinition(ITypeSymbol definition, bool isDataRecord) + { + var fields = new List(); + var symbols = new List(); + var invalidFields = false; + + GetDataFields(definition, isDataRecord, fields, symbols, ref invalidFields); var typeName = GetGenericTypeName(definition); var hasHooks = ImplementsInterface(definition, SerializationHooksNamespace); - return new DataDefinition(definition, typeName, fields, hasHooks, invalidFields); + // Same as DataDefinition.cs + fields.Sort((a, b) => + { + var priority = b.Attribute.Priority.CompareTo(a.Attribute.Priority); + if (priority != 0) + return priority; + + return string.Compare(b.Symbol.Name, a.Symbol.Name, StringComparison.OrdinalIgnoreCase); + }); + + return new DataDefinition(definition, typeName, fields, hasHooks, invalidFields, isDataRecord); } - private static string GetConstructor(DataDefinition definition) - { - if (definition.Type.TypeKind == TypeKind.Interface) - return string.Empty; - - var builder = new StringBuilder(); - - if (NeedsEmptyConstructor(definition.Type)) - { - builder.AppendLine($$""" - // Implicit constructor - #pragma warning disable CS8618 - [RobustAutoGenerated] - public {{definition.Type.Name}}() - #pragma warning restore CS8618 - { - } - """); - } - - return builder.ToString(); - } - - private static string GetCopyMethods(DataDefinition definition) + private static string GetConstructors(DataDefinition definition) { + if (definition.Type.TypeKind == TypeKind.Interface) + return string.Empty; + var builder = new StringBuilder(); + var thisCall = new StringBuilder(); + var (needsEmpty, mustCall) = NeedsEmptyConstructor(definition.Type); + if (mustCall != null) + { + thisCall.Append(" : this("); + foreach (var parameter in mustCall.Parameters) + { + thisCall.Append($"{GetParameterDefaultExpression(parameter)},"); + } - var modifiers = IsVirtualClass(definition.Type) ? "virtual " : string.Empty; - var baseCall = string.Empty; - string baseCopy; - var baseType = definition.Type.BaseType; + if (thisCall[thisCall.Length - 1] == ',') + thisCall = thisCall.Remove(thisCall.Length - 1, 1); - if (baseType != null && IsDataDefinition(definition.Type.BaseType)) - { - var baseName = baseType.ToDisplayString(); - baseCall = $""" - var definitionCast = ({baseName}) target; - base.InternalCopy(ref definitionCast, serialization, hookCtx, context); - target = ({definition.GenericTypeName}) definitionCast; - """; - - baseCopy = $$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public override void Copy(ref {{baseName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public override void Copy(ref object target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - """; + thisCall.Append(')'); } - else + + var setsRequired = GetSetsRequiredAttributeOrEmpty(definition.Type); + if (needsEmpty) { - baseCopy = $$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void Copy(ref object target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - var cast = ({{definition.GenericTypeName}}) target; - Copy(ref cast, serialization, hookCtx, context); - target = cast!; - } - """; + // There was one case in content of a content-defined constructor calling the source-generated + // empty constructor, which would then call it again. + // Instead of attempting to find loops like this, I changed content. + // Because that's fucking stupid. + builder.AppendLine($$""" + // Implicit constructor + #pragma warning disable CS8618 + {{setsRequired}} + public {{definition.Type.Name}}(){{thisCall}} + #pragma warning restore CS8618 + { + } + """); } + var accessibility = definition.Type.IsValueType + ? "public" + : definition.Type.IsSealed + ? "private" + : "protected"; + + var copyBaseCall = IsDataDefinition(definition.Type.BaseType, out _) + ? "base(ISerializationGeneratedCopy, source, serialization, hookCtx, context)" + : "this()"; + + var readBaseCall = IsDataDefinition(definition.Type.BaseType, out _) + ? "base(ISerializationGeneratedRead, mappingDataNode, serialization, hookCtx, context)" + : "this()"; + builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void InternalCopy(ref {{definition.GenericTypeName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - {{baseCall}} - {{CopyDataFields(definition)}} - } - - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{modifiers}} void Copy(ref {{definition.GenericTypeName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) - { - InternalCopy(ref target, serialization, hookCtx, context); - } - - {{baseCopy}} - """); - - foreach (var interfaceName in InternalGetImplicitDataDefinitionInterfaces(definition.Type, true)) + [Obsolete("Used only in serialization source generation internally")] + #pragma warning disable CS8618 + {{setsRequired}} + {{accessibility}} {{definition.Type.Name}}( + ISerializationGeneratedCopy ISerializationGeneratedCopy, + {{definition.GenericTypeName}} source, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context + ) : {{copyBaseCall}} + #pragma warning restore CS8618 + { + {{GetCopyBody(definition)}} + } + + [Obsolete("Used only in serialization source generation internally")] + #pragma warning disable CS8618 + {{setsRequired}} + {{accessibility}} {{definition.Type.Name}}( + ISerializationGeneratedRead ISerializationGeneratedRead, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context + ) : {{readBaseCall}} + #pragma warning restore CS8618 + { + {{GetReadBody(definition)}} + } + """); + + return builder.ToString(); + } + + private static string GetParameterDefaultExpression(IParameterSymbol parameter) + { + var typeName = parameter.Type.ToDisplayString(); + if (!parameter.HasExplicitDefaultValue) + return $"({typeName}) default!"; + + var value = parameter.ExplicitDefaultValue; + if (value == null) + return "null!"; + + var literal = parameter.Type.SpecialType switch { - var interfaceModifiers = baseType != null && - baseType.AllInterfaces.Any(i => i.ToDisplayString() == interfaceName) - ? "override " - : modifiers; + SpecialType.System_Boolean => (bool)value ? "true" : "false", + SpecialType.System_Char => SyntaxFactory.LiteralExpression( + SyntaxKind.CharacterLiteralExpression, + SyntaxFactory.Literal((char)value)).ToFullString(), + SpecialType.System_String => SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal((string)value)).ToFullString(), + SpecialType.System_Single => Convert.ToString(value, CultureInfo.InvariantCulture) + "f", + SpecialType.System_Double => Convert.ToString(value, CultureInfo.InvariantCulture) + "d", + SpecialType.System_Decimal => Convert.ToString(value, CultureInfo.InvariantCulture) + "m", + SpecialType.System_UInt32 => Convert.ToString(value, CultureInfo.InvariantCulture) + "U", + SpecialType.System_Int64 => Convert.ToString(value, CultureInfo.InvariantCulture) + "L", + SpecialType.System_UInt64 => Convert.ToString(value, CultureInfo.InvariantCulture) + "UL", + _ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? "default!" + }; + + if (literal.StartsWith("-", StringComparison.Ordinal)) + literal = $"({literal})"; + + return $"({typeName}) {literal}"; + } + + private static string GetReadBody(DataDefinition definition, string targetPrefix = "this") + { + var builder = new StringBuilder(); + for (var i = 0; i < definition.Fields.Count; i++) + { + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + if (field.Attribute.ServerOnly) + { + builder.AppendLine(""" + if (serialization.IsServer) + { + """); + } + + var fieldName = field.Symbol.Name; + var targetName = $"{targetPrefix}.{fieldName}"; + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + if (mappingDataNode.TryGet("{{field.Attribute.Tag}}", out var node{{i}})) + { + """); + } + else + { + builder.AppendLine($$""" + { + var node{{i}} = mappingDataNode; + """); + } + + var (fieldTypeName, nonNullableFieldTypeName) = GetCleanNameForGenericType(field.Type, out _); + var tagName = field.Attribute.Tag; + var reader = field.CustomSerializer; + var readerName = reader?.Serializer.ToDisplayString(); + var nullable = field.Type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); + var nullableString = string.Empty; + if (!field.Type.IsValueType) + { + nullableString = $", {(!nullable).ToString().ToLowerInvariant()}"; + if (fieldTypeName.EndsWith("?")) + fieldTypeName = fieldTypeName.Substring(0, fieldTypeName.Length - 1); + } + + var nullExpression = + field.Type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString().Equals(EntityUidName) + ? $"{targetName} = EntityUid.Invalid;" + : nullable + ? $"{targetName} = default!;" + : "throw new NullNotAllowedException();"; builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{interfaceModifiers}} void InternalCopy(ref {{interfaceName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) + if (node{{i}}.IsNull) { - var def = ({{definition.GenericTypeName}}) target; - Copy(ref def, serialization, hookCtx, context); - target = def; + {{nullExpression}} } + else + { + """); - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CopyTo instead")] - public {{interfaceModifiers}} void Copy(ref {{interfaceName}} target, ISerializationManager serialization, SerializationHookContext hookCtx, ISerializationContext? context = null) + var method = $"Read<{fieldTypeName}>"; + if (field.Type.TypeKind == TypeKind.Enum) + { + method = $"ReadEnum<{fieldTypeName}>"; + nullableString = string.Empty; + } + else if (field.Type.IsValueType && IsDataDefinition(field.Type, out _)) + { + method = $"ReadStructDefinition<{fieldTypeName}>"; + nullableString = string.Empty; + } + else if (field.Type.TypeKind == TypeKind.Array && + field.Type is IArrayTypeSymbol { Rank: 1 } arrayTypeSymbol) // [*,*] goes the regular way + { + var elementType = arrayTypeSymbol.ElementType; + method = $"ReadArray<{elementType}>"; + + if (elementType.NullableAnnotation != NullableAnnotation.Annotated && + !elementType.ToDisplayString().EndsWith("?") && + !elementType.IsValueType) { - InternalCopy(ref target, serialization, hookCtx, context); + nullableString = $", {(!nullable).ToString().ToLowerInvariant()}"; } - """); + else + { + nullableString = string.Empty; + } + } + else if (IsDataDefinition(field.Type, out _) && field.Type.TypeKind != TypeKind.Interface) + { + method = $"ReadDefinition<{fieldTypeName}>"; + } + + if (reader is { Type: var type } && + (type & (MappingReader | SequenceReader | ValueReader)) != 0) + { + builder.AppendLine($$""" + switch (node{{i}}) + { + """); + + if ((reader.Value.Type & MappingReader) != 0) + { + builder.AppendLine($""" + case MappingDataNode mapping: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, MappingDataNode, {readerName}>(mapping, hookCtx, context, null{nullableString}); + break; + """); + } + + if ((reader.Value.Type & SequenceReader) != 0) + { + builder.AppendLine($""" + case SequenceDataNode sequence: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, SequenceDataNode, {readerName}>(sequence, hookCtx, context, null{nullableString}); + break; + """); + } + + if ((reader.Value.Type & ValueReader) != 0) + { + builder.AppendLine($""" + case ValueDataNode value: + {targetName} = serialization.Read<{nonNullableFieldTypeName}, ValueDataNode, {readerName}>(value, hookCtx, context, null{nullableString}); + break; + """); + } + + builder.AppendLine($$""" + default: + throw new InvalidOperationException($"Unable to read node for {{field.Symbol.Name}}({{field.Attribute.Data?.AttributeClass?.Name}}) as valid."); + } + """); + } + else + { + builder.AppendLine( + $"{targetName} = serialization.{method}(node{i}, hookCtx, context, null{nullableString});"); + } + + builder.AppendLine("}"); + builder.AppendLine("}"); + + if (field.Attribute is { IsDataFieldAttribute: true, Required: true }) + { + if (field.Type.IsReferenceType && fieldTypeName.EndsWith("?")) + fieldTypeName = fieldTypeName.Substring(0, fieldTypeName.Length - 1); + + builder.AppendLine($$""" + else + { + throw new RequiredFieldNotMappedException(typeof({{fieldTypeName}}), "{{tagName}}", typeof({{definition.Type.ToDisplayString()}})); + } + """); + } + + if (field.Attribute.ServerOnly) + builder.AppendLine("}"); } return builder.ToString(); @@ -311,7 +588,7 @@ private static string GetInstantiators(DataDefinition definition) var builder = new StringBuilder(); var modifiers = string.Empty; - if (definition.Type.BaseType is { } baseType && IsDataDefinition(baseType)) + if (GetFirstDataDefinitionBaseType(definition.Type) != null) modifiers = "override "; else if (IsVirtualClass(definition.Type)) modifiers = "virtual "; @@ -320,297 +597,816 @@ private static string GetInstantiators(DataDefinition definition) { // TODO make abstract once data definitions are forced to be partial builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CreateCopy instead")] - public {{modifiers}} {{definition.GenericTypeName}} Instantiate() - { - throw new NotImplementedException(); - } - """); + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + public {{modifiers}} {{definition.GenericTypeName}} Instantiate() + { + throw new NotImplementedException(); + } + """); } else { + var requiredFields = GetRequiredFieldsPropertiesAssigners(definition.Type, string.Empty); builder.AppendLine($$""" - /// - [RobustAutoGenerated] - [Obsolete("Use ISerializationManager.CreateCopy instead")] - public {{modifiers}} {{definition.GenericTypeName}} Instantiate() - { - return new {{definition.GenericTypeName}}(); - } - """); + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + public {{modifiers}} {{definition.GenericTypeName}} Instantiate() + { + return new {{definition.GenericTypeName}}(){{requiredFields}}; + } + + public static {{definition.GenericTypeName}} StaticInstantiate() + { + return new {{definition.GenericTypeName}}(); + } + + public static object StaticInstantiateObject() + { + return (object) {{definition.GenericTypeName}}.StaticInstantiate(); + } + """); } - foreach (var interfaceName in InternalGetImplicitDataDefinitionInterfaces(definition.Type, false)) + return builder.ToString(); + } + + private static string GetValidator(DataDefinition definition) + { + var builder = new StringBuilder(); + var validateBuilder = new StringBuilder(); + + for (var i = 0; i < definition.Fields.Count; i++) { + validateBuilder.Clear(); + + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + var fieldTypeName = GetNonNullableNameForGenericParameter(field.Type); + var tagName = field.Attribute.Tag; + if (field.Attribute.Include) + { + builder.AppendLine($"var node{i} = node;"); + } + else + { + builder.AppendLine($$""" + if (node.TryGetValue("{{tagName}}", out var node{{i}})) + { + """); + } + + var validator = field.CustomSerializer; + var validatorName = validator?.Serializer.ToDisplayString(); + if (validator != null && (validator.Value.Type & MappingValidator) != 0) + { + validateBuilder.AppendLine($""" + case MappingDataNode mapping: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, MappingDataNode, {validatorName}>(mapping, context); + break; + """); + } + + if (validator != null && (validator.Value.Type & SequenceValidator) != 0) + { + validateBuilder.AppendLine($""" + case SequenceDataNode sequence: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, SequenceDataNode, {validatorName}>(sequence, context); + break; + """); + } + + if (validator != null && (validator.Value.Type & ValueValidator) != 0) + { + validateBuilder.AppendLine($""" + case ValueDataNode value: + nodes["{tagName}"] = serialization.ValidateNode<{fieldTypeName}, ValueDataNode, {validatorName}>(value, context); + break; + """); + } + builder.AppendLine($$""" - [RobustAutoGenerated] - {{interfaceName}} {{interfaceName}}.Instantiate() + switch (node{{i}}) { - return Instantiate(); + {{validateBuilder}} + default: + nodes["{{tagName}}"] = serialization.ValidateNode<{{fieldTypeName}}>(node{{i}}, context); + break; } + """); + + if (!field.Attribute.Include) + builder.AppendLine("}"); + } + + if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) + builder.AppendLine($"{baseType.ToDisplayString()}.Validate(nodes, node, serialization, context);"); + + return $$""" + public static void Validate(Dictionary nodes, MappingDataNode node, ISerializationManager serialization, ISerializationContext? context = null) + { + {{builder}} + } + """; + } + + private static string GetCopiers(DataDefinition definition) + { + var builder = new StringBuilder(); + var requiredFields = GetRequiredFieldsPropertiesAssigners(definition.Type, string.Empty); + var type = definition.Type; + var baseType = type.BaseType; + var baseDefinition = false; + while (baseType != null) + { + if (!baseDefinition && IsDataDefinition(baseType, out _)) + baseDefinition = true; + + GetCopierMethod(definition, baseType, baseType.ToDisplayString(), true, builder, requiredFields); + baseType = baseType.BaseType; + } + + GetCopierMethod(definition, definition.Type, "object", baseDefinition, builder, requiredFields); + GetCopierMethod(definition, definition.Type, GetGenericTypeName(type), false, builder, requiredFields); + return builder.ToString(); + } + + private static void GetCopierMethod( + DataDefinition definition, + ITypeSymbol type, + string targetType, + bool forceOverride, + StringBuilder builder, + string requiredFields) + { + if (!IsDataDefinition(type, out _)) + return; + + var sameType = definition.Type.Equals(type, SymbolEqualityComparer.Default) && + targetType == definition.GenericTypeName && + targetType != "object"; + var isSealedOrStruct = definition.Type.IsSealed || definition.Type.IsValueType; + var isAbstract = definition.Type.IsAbstract; + var isInterface = definition.Type.TypeKind == TypeKind.Interface; + var modifier = (sameType, targetType == "object", isSealedOrStruct, isInterface) switch + { + (true, _, true, _) => string.Empty, + (true, _, false, _) => "virtual ", + (false, true, true, _) => string.Empty, + (false, true, false, _) => "virtual ", + (false, false, _, true) => string.Empty, + (false, false, _, false) => "override ", + }; + + if (!sameType && targetType == "object" && forceOverride) + modifier = "override "; + + if (forceOverride && modifier is "" or "virtual ") + { + if (modifier is "") + modifier += "override "; + else if (modifier == "virtual ") + modifier = "override "; + } - [RobustAutoGenerated] - {{interfaceName}} ISerializationGenerated<{{interfaceName}}>.Instantiate() + builder.AppendLine($""" + public {modifier}void Copy( + ref {targetType} target, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context = null) + """); + + if (!sameType) + { + builder.AppendLine($$""" + { + var def = ({{definition.GenericTypeName}})target; + Copy(ref def, serialization, hookCtx, context); + target = def; + } + """); + } + else if (definition.Type.IsValueType || definition.IsRecord) + { + builder.AppendLine($$""" { - return Instantiate(); + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedCopy.Default, + this, + serialization, + hookCtx, + context + ){{requiredFields}}; } """); } + else + { + var baseCopy = IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + base.Copy(ref definitionCast, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + var instantiate = isAbstract + ? $$""" + if (target is null) + throw new NullReferenceException("Cannot copy into a null abstract data definition target."); + """ + : $$""" + if (target is null) + { + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedCopy.Default, + this, + serialization, + hookCtx, + context + ){{requiredFields}}; + return; + } + """; - return builder.ToString(); + builder.AppendLine($$""" + { + {{instantiate}} + var source = this; + {{baseCopy}} + if (serialization.TryCustomCopy(this, ref target, hookCtx, {{definition.HasHooks.ToString().ToLower()}}, context)) + return; + + {{GetCopyBody(definition, "target")}} + } + """); + } } - [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - private static IEnumerable InternalGetImplicitDataDefinitionInterfaces( - ITypeSymbol type, - bool all) + private static string GetReader(DataDefinition definition) { - var symbols = GetImplicitDataDefinitionInterfaces(type, all); - - // TODO SOURCE GEN - // fix this jank - // The comp-state source generator will add an "IComponentDelta" interface to classes with the auto state - // attribute, and this generator creates methods that those classes then have to implement because - // IComponentDelta a DataDefinition via the ImplicitDataDefinitionForInheritorsAttribute on IComponent. - if (!TryGetAttribute(type, AutoStateAttributeName, out var data)) - return symbols; - - // If it doesn't have field deltas then ignore - if (data.ConstructorArguments[1] is not { Value: bool fields and true }) + string body; + if (definition.Type.IsAbstract) + { + var baseRead = IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + body = $$""" + if (target is null) + throw new NullReferenceException("Cannot read into a null abstract data definition target."); + + {{baseRead}} + {{GetReadBody(definition, "target")}} + """; + } + else if (definition.Type.IsValueType || definition.IsRecord) { - return symbols; + body = $$""" + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedRead.Default, + mappingDataNode, + serialization, + hookCtx, + context + ); + """; } + else + { + var baseRead = IsDataDefinition(definition.Type.BaseType, out _) + ? $$""" + var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; + {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); + target = ({{definition.GenericTypeName}})definitionCast; + """ + : string.Empty; + + body = $$""" + if (target is null) + { + target = new {{definition.GenericTypeName}}( + ISerializationGeneratedRead.Default, + mappingDataNode, + serialization, + hookCtx, + context + ); + return; + } - if (symbols.Any(x => x == ComponentDeltaInterfaceName)) - return symbols; + {{baseRead}} + {{GetReadBody(definition, "target")}} + """; + } - return symbols.Append(ComponentDeltaInterfaceName); + return $$""" + public static void Read( + ref {{definition.GenericTypeName}} target, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context) + { + {{body}} + } + """; } - // TODO serveronly? do we care? who knows!! - private static StringBuilder CopyDataFields(DataDefinition definition) + private static string GetWriter(DataDefinition definition) { var builder = new StringBuilder(); + for (var i = 0; i < definition.Fields.Count; i++) + { + var field = definition.Fields[i]; + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; - builder.AppendLine($""" -if (serialization.TryCustomCopy(this, ref target, hookCtx, {definition.HasHooks.ToString().ToLower()}, context)) - return; -"""); + if (field.Attribute.ReadOnly) + continue; - var structCopier = new StringBuilder(); - foreach (var field in definition.Fields) + var fieldType = field.Type.ToDisplayString(); + if (IsMultidimensionalArray(field.Type)) + fieldType = fieldType.Replace("*", ""); + + if (field.Type.NullableAnnotation == NullableAnnotation.Annotated && + !fieldType.EndsWith("?")) + { + fieldType += "?"; + } + + var nonNullableFieldType = GetNonNullableNameForGenericParameter(field.Type); + var nullable = fieldType.EndsWith("?"); + var nullableString = string.Empty; + if (!field.Type.IsValueType) + { + if (!nullable) + nullableString = ", true"; + + if (nonNullableFieldType.EndsWith("?")) + nonNullableFieldType = nonNullableFieldType.Substring(0, nonNullableFieldType.Length - 1); + } + + if (field.Attribute.ServerOnly) + { + builder.AppendLine(""" + if (serialization.IsServer) + { + """); + } + + if (!field.Attribute.IsDataFieldAttribute || !field.Attribute.Required) + { + builder.AppendLine($$""" + if (alwaysWrite || !EqualityComparer<{{fieldType}}>.Default.Equals(obj.{{field.Symbol.Name}}, ({{fieldType}}) defaultValues["{{field.Attribute.Tag}}"]!)) + { + """); + } + + builder.AppendLine($"""DataNode node{i};"""); + + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + if (!mapping.Has("{{field.Attribute.Tag}}")) + { + """); + } + + if (field.CustomSerializer is { } serializer && + (serializer.Type & Writer) != 0) + { + var nullableValueType = field.Type.IsValueType && nullable; + if (nullableValueType) + { + builder.AppendLine($$""" + if (obj.{{field.Symbol.Name}} == null) + { + node{{i}} = ValueDataNode.Null(); + } + else + { + """); + } + + var nullableValueTypeString = nullableValueType ? ".Value" : string.Empty; + var writerName = serializer.Serializer.ToDisplayString(); + builder.AppendLine($""" + #pragma warning disable RA0008 + node{i} = serialization.WriteValue<{nonNullableFieldType}, {writerName}>(obj.{field.Symbol.Name}{nullableValueTypeString}!, alwaysWrite, context{nullableString}); + #pragma warning restore RA0008 + """); + + if (nullableValueType) + builder.Append("}"); + } + else + { + builder.AppendLine( + $"node{i} = serialization.WriteValue<{fieldType}>(obj.{field.Symbol.Name}, alwaysWrite, context{nullableString});"); + } + + if (field.Attribute.IsDataFieldAttribute) + { + builder.AppendLine($$""" + mapping.Add("{{field.Attribute.Tag}}", node{{i}}); + } + """); + } + else + { + builder.AppendLine($$""" + if (node{{i}} is MappingDataNode mapping{{i}}) + { + mapping.Insert(mapping{{i}}, true); + } + else + { + throw new InvalidOperationException($"Writing field {{field.Symbol.Name}} for type {typeof({{definition.GenericTypeName}})} did not return a {nameof(MappingDataNode)} but was annotated to be included."); + } + """); + } + + if (!field.Attribute.IsDataFieldAttribute || !field.Attribute.Required) + builder.AppendLine("}"); + + if (field.Attribute.ServerOnly) + builder.AppendLine("}"); + + } + + if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) { - var type = field.Type; - var typeName = type.ToDisplayString(); - if (IsMultidimensionalArray(type)) + var baseTypeName = baseType.ToDisplayString(); + builder.AppendLine( + $"{baseTypeName}.Write(obj, mapping, serialization, context, alwaysWrite, defaultValues);"); + } + + return $$""" + public static void Write( + {{definition.GenericTypeName}} obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues) { - typeName = typeName.Replace("*", ""); + {{builder}} } + """; + } + + private static string GetFieldDefinitions(DataDefinition definition) + { + var builder = new StringBuilder(); + var nullConditional = definition.Type.IsValueType ? string.Empty : "?"; + var fieldTags = new List(definition.Fields.Count); + foreach (var field in definition.Fields) + { + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; - var isNullableValueType = IsNullableValueType(type); - var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); - if (isNullableValueType) + var (fieldType, _) = GetCleanNameForGenericType(field.Type, out var isNullableValueType); + var nullable = field.Type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); + + if (!isNullableValueType && fieldType.EndsWith("?")) + fieldType = fieldType.Substring(0, fieldType.Length - 1); + + builder.AppendLine($$""" + if (fieldsParsed == null || !fieldsParsed.Contains("{{field.Attribute.Tag}}")) + { + fields.Add(new DataFieldDefinition( + "{{field.Attribute.Tag}}", + {{field.Attribute.Priority}}, + {{field.Attribute.IsDataFieldAttribute.ToString().ToLowerInvariant()}}, + {{field.Attribute.Include.ToString().ToLowerInvariant()}}, + instance{{nullConditional}}.{{field.Symbol.Name}}, + (InheritanceBehavior) {{field.Attribute.InheritanceBehavior}}, + "{{field.Symbol.Name}}", + typeof({{fieldType}}), + {{nullable.ToString().ToLowerInvariant()}}, + "{{field.Attribute.CamelCasedName}}", + {{(field.CustomSerializer == null ? "null" : $"typeof({field.CustomSerializer.Value.Serializer.ToDisplayString()})")}} + )); + } + """); + + fieldTags.Add($"\"{field.Attribute.Tag}\""); + } + + if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) + builder.AppendLine( + $"{baseType.ToDisplayString()}.GetFieldDefinitions(instance, fields, [{string.Join(", ", fieldTags)}]);"); + + var instance = definition.Type.IsAbstract + ? string.Empty + : $"instance = {definition.GenericTypeName}.StaticInstantiate();"; + + return $$""" + public static void GetFieldDefinitions({{definition.GenericTypeName}}{{nullConditional}} instance, List fields, string[]? fieldsParsed = null) { - nonNullableTypeName = typeName.Substring(0, typeName.Length - 1); + {{instance}} + {{builder}} } + """; + } + + // TODO serveronly? do we care? who knows!! + private static StringBuilder GetCopyBody(DataDefinition definition, string targetPrefix = "") + { + var builder = new StringBuilder(); + foreach (var field in definition.Fields) + { + if (!definition.Type.Equals(field.Symbol.ContainingType, SymbolEqualityComparer.Default)) + continue; + + var type = field.Type; + var (typeName, nonNullableTypeName) = GetCleanNameForGenericType(type, out var isNullableValueType); var isClass = type.IsReferenceType || type.SpecialType == SpecialType.System_String; - var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated; + var isNullable = type.NullableAnnotation == NullableAnnotation.Annotated || + field.Type.ToDisplayString().EndsWith("?"); var nullableOverride = isClass && !isNullable ? ", true" : string.Empty; var name = field.Symbol.Name; - var tempVarName = $"{name}Temp"; + var targetName = string.IsNullOrEmpty(targetPrefix) ? name : $"{targetPrefix}.{name}"; var nullableValue = isNullableValueType ? ".Value" : string.Empty; var nullNotAllowed = isClass && !isNullable; - if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType }) + if (CanBeCopiedByValue(field.Symbol, field.Type)) { if (nullNotAllowed) { builder.AppendLine($$""" - if ({{name}} == null) - { - throw new NullNotAllowedException(); - } - """); + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } - builder.AppendLine($$""" - {{typeName}} {{tempVarName}} = default!; - """); - - if (isNullable || isNullableValueType) + builder.AppendLine($"{targetName} = source.{name};"); + } + else if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType } && + ((serializerType & Copier) != 0 || (serializerType & CopyCreator) != 0)) + { + if (nullNotAllowed) { builder.AppendLine($$""" - if ({{name}} == null) - { - {{tempVarName}} = null!; - } - else - { - """); - } - - var serializerName = serializer.ToDisplayString(); - switch (serializerType) - { - case Copier: - CopyToCustom( - builder, - nonNullableTypeName, - serializerName, - tempVarName, - name, - isNullable, - isClass, - isNullableValueType - ); - break; - case CopyCreator: - CreateCopyCustom( - builder, - name, - tempVarName, - nonNullableTypeName, - serializerName, - nullableValue, - nullableOverride - ); - break; + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } if (isNullable || isNullableValueType) { - builder.AppendLine("}"); + builder.AppendLine($$""" + if (source.{{name}} == null) + { + {{targetName}} = null!; + } + else + { + """); } - if (definition.Type.IsValueType) + var serializerName = serializer.ToDisplayString(); + + // TODO ROBUST should these both be created if both are present? + if ((serializerType & Copier) != 0) { - structCopier.AppendLine($"{name} = {tempVarName}!,"); + builder.AppendLine($""" + #pragma warning disable RA0008 + {nonNullableTypeName} {name}GeneratedTemp = default!; + serialization.CopyTo<{nonNullableTypeName}, {serializerName}>(source.{name}{nullableValue}, ref {name}GeneratedTemp, hookCtx, context{nullableOverride}); + #pragma warning restore RA0008 + {targetName} = {name}GeneratedTemp; + """); } - else + else if ((serializerType & CopyCreator) != 0) { - builder.AppendLine($"target.{name} = {tempVarName}!;"); + builder.AppendLine( + $"{targetName} = serialization.CreateCopy<{nonNullableTypeName}, {serializerName}>(source.{name}{nullableValue}, hookCtx, context{nullableOverride});"); } + + if (isNullable || isNullableValueType) + builder.AppendLine("}"); } else { - builder.AppendLine($$""" - {{typeName}} {{tempVarName}} = default!; - """); - if (nullNotAllowed) { builder.AppendLine($$""" - if ({{name}} == null) - { - throw new NullNotAllowedException(); - } - """); + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); } - var hasHooks = ImplementsInterface(type, SerializationHooksNamespace) || !type.IsSealed; - builder.AppendLine($$""" - if (!serialization.TryCustomCopy(this.{{name}}, ref {{tempVarName}}, hookCtx, {{hasHooks.ToString().ToLower()}}, context)) - { - """); - - if (CanBeCopiedByValue(field.Symbol, field.Type)) + if (TryGetFastCollectionCopy(field.Type, $"source.{name}", targetName, isNullable, out var collectionCopy)) { - builder.AppendLine($"{tempVarName} = {name};"); + builder.Append(collectionCopy); } - else if (IsDataDefinition(type) && !type.IsAbstract && - type is not INamedTypeSymbol { TypeKind: TypeKind.Interface }) + else { - var nullable = !type.IsValueType || IsNullableType(type); + var hasHooks = ImplementsInterface(type, SerializationHooksNamespace) || !type.IsSealed; + builder.AppendLine($$""" + {{typeName}} {{name}}GeneratedTemp = default!; + if (serialization.TryCustomCopy(source.{{name}}, ref {{name}}GeneratedTemp, hookCtx, {{hasHooks.ToString().ToLower()}}, context)) + { + {{targetName}} = {{name}}GeneratedTemp; + } + else + { + """); + + if (IsDataDefinition(type, out _) && !type.IsAbstract && + type is not INamedTypeSymbol { TypeKind: TypeKind.Interface }) + { + var nullable = !type.IsValueType || IsNullableType(type); + + if (nullable) + { + builder.AppendLine($$""" + if (source.{{name}} == null) + { + {{targetName}} = null!; + } + else + { + """); + } - if (nullable) + builder.AppendLine($""" + serialization.CopyTo(source.{name}, ref {name}GeneratedTemp, hookCtx, context{nullableOverride}); + {targetName} = {name}GeneratedTemp; + """); + + if (nullable) + builder.AppendLine("}"); + } + else { - builder.AppendLine($$""" - if ({{name}} == null) - { - {{tempVarName}} = null!; - } - else - { - """); + builder.AppendLine($"{targetName} = serialization.CreateCopy(source.{name}, hookCtx, context);"); } - builder.AppendLine($$""" - serialization.CopyTo({{name}}, ref {{tempVarName}}, hookCtx, context{{nullableOverride}}); - """); + builder.AppendLine("}"); + } + } + } + + return builder; + } - if (nullable) + private static bool TryGetFastCollectionCopy( + ITypeSymbol type, + string sourceName, + string targetName, + bool nullable, + [NotNullWhen(true)] out string? copy) + { + copy = null; + var builder = new StringBuilder(); + var nullPrefix = string.Empty; + var nullSuffix = string.Empty; + + if (!type.IsValueType) + { + if (nullable) + { + nullPrefix = $$""" + if ({{sourceName}} == null) { - builder.AppendLine("}"); + {{targetName}} = null!; } - } + else + { + """; + nullSuffix = "}\n"; + } + } + + var sourceAccess = nullable ? $"{sourceName}!" : sourceName; + + if (type is IArrayTypeSymbol { Rank: 1 } arrayType && + CanTypeBeCopiedByValue(arrayType.ElementType)) + { + var arrayTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + builder.Append(nullPrefix); + builder.AppendLine($"{targetName} = ({arrayTypeName}) {sourceAccess}.Clone();"); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } + + if (type is not INamedTypeSymbol { IsGenericType: true } namedType) + return false; + + var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + var typeArgs = namedType.TypeArguments; + var targetAccess = $"{targetName}!"; + + if (IsGenericCollectionType(namedType, "List", 1) && + CanTypeBeCopiedByValue(typeArgs[0])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count); else { - builder.AppendLine($"{tempVarName} = serialization.CreateCopy({name}, hookCtx, context);"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } - builder.AppendLine("}"); + {{targetAccess}}.AddRange({{sourceAccess}}); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } - if (definition.Type.IsValueType) + if (IsGenericCollectionType(namedType, "HashSet", 1) && + CanTypeBeCopiedByValue(typeArgs[0])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count, {{sourceAccess}}.Comparer); + else { - structCopier.AppendLine($"{name} = {tempVarName}!,"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } + + foreach (var value in {{sourceAccess}}) + {{targetAccess}}.Add(value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; + } + + if (IsGenericCollectionType(namedType, "Dictionary", 2) && + CanTypeBeCopiedByValue(typeArgs[0]) && + CanTypeBeCopiedByValue(typeArgs[1])) + { + builder.Append(nullPrefix); + builder.AppendLine($$""" + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Count, {{sourceAccess}}.Comparer); else { - builder.AppendLine($"target.{name} = {tempVarName}!;"); + {{targetAccess}}.Clear(); + {{targetAccess}}.EnsureCapacity({{sourceAccess}}.Count); } - } + + foreach (var (key, value) in {{sourceAccess}}) + {{targetAccess}}.Add(key, value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; } - if (definition.Type.IsValueType) + if (IsGenericCollectionType(namedType, "SortedDictionary", 2) && + CanTypeBeCopiedByValue(typeArgs[0]) && + CanTypeBeCopiedByValue(typeArgs[1])) { + builder.Append(nullPrefix); builder.AppendLine($$""" - target = target with - { - {{structCopier}} - }; - """); + if ({{targetName}} == null) + {{targetName}} = new {{nonNullableTypeName}}({{sourceAccess}}.Comparer); + else + {{targetAccess}}.Clear(); + + foreach (var (key, value) in {{sourceAccess}}) + {{targetAccess}}.Add(key, value); + """); + builder.Append(nullSuffix); + copy = builder.ToString(); + return true; } - return builder; + return false; } - private static void CopyToCustom( - StringBuilder builder, - string typeName, - string serializerName, - string tempVarName, - string varName, - bool isNullable, - bool isClass, - bool isNullableValueType) + private static bool IsGenericCollectionType(INamedTypeSymbol type, string name, int typeArgumentCount) { - var newTemp = isNullable && isClass ? $"{tempVarName} ??= new();" : string.Empty; - var nullableOverride = isClass ? ", true" : string.Empty; - var nullableValue = isNullableValueType ? ".Value" : string.Empty; - var nonNullableTypeName = typeName.EndsWith("?") ? typeName.Substring(0, typeName.Length - 1) : typeName; + var definition = type.ConstructedFrom; - builder.AppendLine($$""" - {{nonNullableTypeName}} {{tempVarName}}CopyTo = default!; - {{newTemp}} - serialization.CopyTo<{{typeName}}, {{serializerName}}>(this.{{varName}}{{nullableValue}}, ref {{tempVarName}}CopyTo, hookCtx, context{{nullableOverride}}); - {{tempVarName}} = {{tempVarName}}CopyTo; - """); - } - - private static void CreateCopyCustom( - StringBuilder builder, - string varName, - string tempVarName, - string nonNullableTypeName, - string serializerName, - string nullableValue, - string nullableOverride) - { - builder.AppendLine($$""" - {{tempVarName}} = serialization.CreateCopy<{{nonNullableTypeName}}, {{serializerName}}>(this.{{varName}}{{nullableValue}}, hookCtx, context{{nullableOverride}}); - """); + return definition.Name == name && + definition.TypeArguments.Length == typeArgumentCount && + definition.ContainingNamespace.ToDisplayString() == "System.Collections.Generic"; } } diff --git a/Robust.Serialization.Generator/Types.cs b/Robust.Serialization.Generator/Types.cs index c07e3530d56..7e7b782a26a 100644 --- a/Robust.Serialization.Generator/Types.cs +++ b/Robust.Serialization.Generator/Types.cs @@ -1,105 +1,31 @@ -using System.Collections.Generic; +using System.Diagnostics; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Robust.Roslyn.Shared.Helpers; +using static Robust.Roslyn.Shared.DataDefinitionHelper; namespace Robust.Serialization.Generator; internal static class Types { - private const string DataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataDefinitionAttribute"; - private const string ImplicitDataDefinitionNamespace = "Robust.Shared.Serialization.Manager.Attributes.ImplicitDataDefinitionForInheritorsAttribute"; - private const string DataFieldBaseNamespace = "Robust.Shared.Serialization.Manager.Attributes.DataFieldBaseAttribute"; private const string CopyByRefNamespace = "Robust.Shared.Serialization.Manager.Attributes.CopyByRefAttribute"; + private const string SerializationHooksNamespace = "Robust.Shared.Serialization.ISerializationHooks"; internal static bool IsPartial(TypeDeclarationSyntax type) { return type.Modifiers.IndexOf(SyntaxKind.PartialKeyword) != -1; } - internal static bool IsDataDefinition(ITypeSymbol? type) - { - if (type == null) - return false; - - return HasAttribute(type, DataDefinitionNamespace) || - IsImplicitDataDefinition(type); - } - - internal static bool IsDataField(ISymbol member, out ITypeSymbol type, out AttributeData attribute) - { - // TODO data records and other attributes - if (member is IFieldSymbol field) - { - foreach (var attr in field.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = field.Type; - attribute = attr; - return true; - } - } - } - else if (member is IPropertySymbol property) - { - foreach (var attr in property.GetAttributes()) - { - if (attr.AttributeClass != null && Inherits(attr.AttributeClass, DataFieldBaseNamespace)) - { - type = property.Type; - attribute = attr; - return true; - } - } - } - - type = null!; - attribute = null!; - return false; - } - - internal static bool IsImplicitDataDefinition(ITypeSymbol type) - { - if (HasAttribute(type, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var baseType in GetBaseTypes(type)) - { - if (HasAttribute(baseType, ImplicitDataDefinitionNamespace)) - return true; - } - - foreach (var @interface in type.AllInterfaces) - { - if (IsImplicitDataDefinitionInterface(@interface)) - return true; - } - - return false; - } - - internal static bool IsImplicitDataDefinitionInterface(ITypeSymbol @interface) - { - if (HasAttribute(@interface, ImplicitDataDefinitionNamespace)) - return true; - - foreach (var subInterface in @interface.AllInterfaces) - { - if (HasAttribute(subInterface, ImplicitDataDefinitionNamespace)) - return true; - } - - return false; - } - internal static IEnumerable GetImplicitDataDefinitionInterfaces(ITypeSymbol type, bool all) { var interfaces = all ? type.AllInterfaces : type.Interfaces; foreach (var @interface in interfaces) { - if (IsImplicitDataDefinitionInterface(@interface)) + if (IsImplicitDataDefinitionInterface(@interface) is { Definition: true }) yield return @interface.ToDisplayString(); } } @@ -130,6 +56,28 @@ internal static bool CanBeCopiedByValue(ISymbol member, ITypeSymbol type) if (type.OriginalDefinition.ToDisplayString() == "System.Nullable") return CanBeCopiedByValue(member, ((INamedTypeSymbol) type).TypeArguments[0]); + if (CanTypeBeCopiedByValue(type)) + return true; + + if (HasAttribute(member, CopyByRefNamespace)) + return true; + + return false; + } + + internal static bool CanTypeBeCopiedByValue(ITypeSymbol type) + { + return CanTypeBeCopiedByValue(type, []); + } + + private static bool CanTypeBeCopiedByValue(ITypeSymbol type, HashSet visited) + { + if (type.OriginalDefinition.ToDisplayString() == "System.Nullable") + return CanTypeBeCopiedByValue(((INamedTypeSymbol) type).TypeArguments[0], visited); + + if (HasAttribute(type, CopyByRefNamespace)) + return true; + if (type.TypeKind == TypeKind.Enum) return true; @@ -149,15 +97,39 @@ internal static bool CanBeCopiedByValue(ISymbol member, ITypeSymbol type) case SpecialType.System_Decimal: case SpecialType.System_Single: case SpecialType.System_Double: + case SpecialType.System_IntPtr: + case SpecialType.System_UIntPtr: case SpecialType.System_String: case SpecialType.System_DateTime: return true; } - if (HasAttribute(member, CopyByRefNamespace)) + if (type is not INamedTypeSymbol { TypeKind: TypeKind.Struct } namedType) + return false; + + if (namedType.IsGenericType || namedType.IsUnboundGenericType) + return false; + + if (IsDataDefinition(namedType, out _) && + ImplementsInterface(namedType, SerializationHooksNamespace)) + { + return false; + } + + var typeName = namedType.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + if (!visited.Add(typeName)) return true; - return false; + foreach (var member in namedType.GetMembers()) + { + if (member is not IFieldSymbol field || field.IsStatic) + continue; + + if (!CanTypeBeCopiedByValue(field.Type, visited)) + return false; + } + + return true; } internal static string GetGenericTypeName(ITypeSymbol symbol) @@ -223,28 +195,28 @@ internal static string GetPartialTypeDefinitionLine(ITypeSymbol symbol) return $"{access} {typeKeyword} {typeName}"; } - internal static bool Inherits(ITypeSymbol type, string parent) - { - foreach (var baseType in GetBaseTypes(type)) - { - if (baseType.ToDisplayString() == parent) - return true; - } - - return false; - } - - internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName) + internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName, List symbols) { + symbols.Clear(); foreach (var interfaceType in type.AllInterfaces) { if (interfaceType.ToDisplayString().Contains(interfaceName)) + symbols.Add(interfaceType); + + if (interfaceType.BaseType is { } baseInterface && + ImplementsInterface(baseInterface, interfaceName, symbols)) { return true; } } - return false; + return symbols.Count > 0; + } + + internal static bool ImplementsInterface(ITypeSymbol type, string interfaceName) + { + var symbols = new List(); + return ImplementsInterface(type, interfaceName, symbols); } internal static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member) @@ -253,38 +225,62 @@ internal static bool IsReadOnlyMember(ITypeSymbol type, ISymbol member) { return field.IsReadOnly; } - else if (member is IPropertySymbol property) + + if (member is IPropertySymbol property) { if (property.SetMethod == null) return true; if (property.SetMethod.IsInitOnly) return type.IsReferenceType; - - return false; } return false; } - internal static bool NeedsEmptyConstructor(ITypeSymbol type) + internal static (bool NeedsEmpty, IMethodSymbol? MustCall) NeedsEmptyConstructor(ITypeSymbol type) { if (type is not INamedTypeSymbol named) - return false; + return (false, null); - if (named.InstanceConstructors.Length == 0) - return true; + if (named.InstanceConstructors.Length == 0 || + named.InstanceConstructors.All(c => c.IsImplicitlyDeclared)) + { + return (true, null); + } + var needsEmpty = true; + IMethodSymbol? mustCall = null; foreach (var constructor in named.InstanceConstructors) { - if (constructor.Parameters.Length == 0 && - !constructor.IsImplicitlyDeclared) + if (constructor.IsImplicitlyDeclared) + continue; + + if (constructor.Parameters.Length == 0) + needsEmpty = false; + + if (mustCall != null) + continue; + + // Is there a better way to find a primary constructor? I don't know! The docs don't tell you! + // Neither does Google, because all the results are useless SEO-optimized AI garbage! + // I don't think you can even access the underlying symbol directly! Hooray! + // So we get the syntax's nodes and find out + foreach (var syntax in constructor.DeclaringSyntaxReferences) { - return false; + var nodes = syntax.GetSyntax().DescendantNodesAndSelf().ToArray(); + if (nodes.Length == 0 || nodes[0] is not TypeDeclarationSyntax) + continue; + + if (nodes.Any(n => n is ParameterListSyntax)) + { + mustCall = constructor; + break; + } } } - return true; + return (needsEmpty, mustCall); } internal static bool IsVirtualClass(ITypeSymbol type) @@ -327,4 +323,151 @@ internal static IEnumerable GetBaseTypes(ITypeSymbol type) baseType = baseType.BaseType; } } + + internal static (string Flat, string NonNullable) GetCleanNameForGenericType(ITypeSymbol type, out bool isNullableValueType) + { + var typeName = type.ToDisplayString(); + if (IsMultidimensionalArray(type)) + typeName = typeName.Replace("*", ""); + + isNullableValueType = IsNullableValueType(type); + var nonNullableTypeName = type.WithNullableAnnotation(NullableAnnotation.None).ToDisplayString(); + if (isNullableValueType) + nonNullableTypeName = typeName.Substring(0, typeName.Length - 1); + + return (typeName, nonNullableTypeName); + } + + internal static string GetNonNullableNameForGenericParameter(ITypeSymbol type) + { + var typeName = type.ToDisplayString(); + if (IsMultidimensionalArray(type)) + typeName = typeName.Replace("*", ""); + + if (typeName.EndsWith("?")) + typeName = typeName.Substring(0, typeName.Length - 1); + + return typeName; + } + + internal static IEnumerable GetRequiredFieldsProperties(ITypeSymbol type) + { + foreach (var member in type.GetMembers()) + { + if (member.IsImplicitlyDeclared) + continue; + + if (member is not IFieldSymbol { IsRequired: true } && + member is not IPropertySymbol { IsRequired: true }) + { + continue; + } + + yield return member; + } + } + + internal static string GetRequiredFieldsPropertiesAssigners(ITypeSymbol type, string accessor) + { + var requiredFields = new StringBuilder(); + foreach (var member in GetRequiredFieldsProperties(type)) + { + // Yes you can just set the field to itself to bypass the compiler's required check + // I don't know man + // Old dynamic method serialization did not change their values + // So we just do this + requiredFields.AppendLine($"{member.Name} = {accessor}{member.Name}!,"); + } + + if (requiredFields.Length > 0) + { + requiredFields.Insert(0, '{'); + requiredFields.Append('}'); + } + + return requiredFields.ToString(); + } + + internal static string GetSetsRequiredAttributeOrEmpty(ITypeSymbol type) + { + var setsRequired = string.Empty; + if (GetRequiredFieldsProperties(type).Any()) + setsRequired = "[SetsRequiredMembers]"; + + return setsRequired; + } + + internal static ITypeSymbol? GetFirstDataDefinitionBaseType(ITypeSymbol type) + { + var parent = type; + while ((parent = parent.BaseType) != null) + { + if (IsDataDefinition(parent, out _)) + return parent; + } + + return null; + } + + internal static IEnumerable<(ISymbol Field, ITypeSymbol Type, DataFieldAttribute Attribute)> GetAllDataFields(ITypeSymbol? definition, bool isDataRecord) + { + while (definition != null) + { + foreach (var member in definition.GetMembers()) + { + if (member is not IFieldSymbol && member is not IPropertySymbol) + continue; + + if (member.IsStatic) + continue; + + if (member is IPropertySymbol dataRecordProperty && isDataRecord) + { + var backingField = definition.GetMembers() + .OfType() + .FirstOrDefault(field => + field.IsImplicitlyDeclared && + (SymbolEqualityComparer.Default.Equals(field.AssociatedSymbol, dataRecordProperty) || + field.Name == $"<{dataRecordProperty.Name}>k__BackingField")); + + if (backingField != null && + IsDataField(backingField.GetAttributes(), + dataRecordProperty.Name, + false, + false, + true, + true, + out var backingAttribute, + out _)) + { + yield return (dataRecordProperty, dataRecordProperty.Type, backingAttribute!); + continue; + } + } + + if (!IsDataField(member, isDataRecord, out var type, out var attribute)) + continue; + + var outputMember = member; + if (member is IFieldSymbol { IsImplicitlyDeclared: true } implicitField) + { + var property = implicitField.AssociatedSymbol as IPropertySymbol ?? + definition.GetMembers() + .OfType() + .FirstOrDefault(propertySymbol => + implicitField.Name == $"<{propertySymbol.Name}>k__BackingField"); + + if (property != null) + { + outputMember = property; + type = property.Type; + } + } + + yield return (outputMember, type, attribute); + } + + definition = definition.BaseType; + } + } } diff --git a/Robust.Shared.IntegrationTests/GlobalUsings.cs b/Robust.Shared.IntegrationTests/GlobalUsings.cs index 316ff0ec590..da7c7fa4c32 100644 --- a/Robust.Shared.IntegrationTests/GlobalUsings.cs +++ b/Robust.Shared.IntegrationTests/GlobalUsings.cs @@ -1 +1,3 @@ -global using Is = NUnit.Framework.Is; +global using Is = NUnit.Framework.Is; + +[assembly: NUnit.Framework.Parallelizable(NUnit.Framework.ParallelScope.Fixtures)] diff --git a/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs index 641358f25bb..84ca86cbc5a 100644 --- a/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs +++ b/Robust.Shared.IntegrationTests/Serialization/PropertyAndFieldDefinitionTest.cs @@ -76,9 +76,8 @@ public void ParityTest() var dataDefinition = ((SerializationManager) Serialization).GetDefinition(propertyInfo.DeclaringType!); Assert.That(dataDefinition, Is.Not.Null); - var alwaysPushDataField = propertyInfo.GetAttribute(); var propertyDefinition = - dataDefinition!.BaseFieldDefinitions.Single(e => e.Attribute.Equals(alwaysPushDataField)); + dataDefinition!.BaseFieldDefinitions.Single(e => (e.Tag ?? e.CamelCasedName).Equals(GetOnlyPropertyWithOtherAttributeFieldTargetedName)); var inheritanceBehaviour = propertyDefinition.InheritanceBehavior; Assert.That(inheritanceBehaviour, Is.EqualTo(InheritanceBehavior.Always)); @@ -100,16 +99,15 @@ public void ParityTest() Assert.That(propertyInfo.GetBackingField()!.GetAttribute(), Is.Null); Assert.That(propertyInfo.GetAttribute(true), Is.Not.Null); - var neverPushDataField = propertyInfo.GetAttribute(); propertyDefinition = - dataDefinition!.BaseFieldDefinitions.Single(e => e.Attribute.Equals(neverPushDataField)); + dataDefinition!.BaseFieldDefinitions.Single(e => (e.Tag ?? e.CamelCasedName).Equals(GetOnlyPropertyFieldTargetedAndOtherAttributeName)); inheritanceBehaviour = propertyDefinition.InheritanceBehavior; dataDefinition = ((SerializationManager) Serialization).GetDefinition(property!.DeclaringType!); Assert.That(dataDefinition, Is.Not.Null); Assert.That(inheritanceBehaviour, Is.EqualTo(InheritanceBehavior.Never)); } - [Robust.Shared.Serialization.Manager.Attributes.DataDefinition] + [DataDefinition] internal sealed partial class PropertyAndFieldDefinitionTestDefinition { [DataField(GetOnlyPropertyName)] diff --git a/Robust.Shared/Audio/SoundSpecifier.cs b/Robust.Shared/Audio/SoundSpecifier.cs index 3378d34f8ad..3bc6fdb28b2 100644 --- a/Robust.Shared/Audio/SoundSpecifier.cs +++ b/Robust.Shared/Audio/SoundSpecifier.cs @@ -12,14 +12,14 @@ namespace Robust.Shared.Audio; -[ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] +[CopyByRef, ImplicitDataDefinitionForInheritors, Serializable, NetSerializable] public abstract partial class SoundSpecifier { [DataField("params")] public AudioParams Params { get; set; } = AudioParams.Default; } -[Serializable, NetSerializable] +[CopyByRef, Serializable, NetSerializable] public sealed partial class SoundPathSpecifier : SoundSpecifier { public const string Node = "path"; @@ -47,7 +47,7 @@ public SoundPathSpecifier(ResPath path, AudioParams? @params = null) } } -[Serializable, NetSerializable] +[CopyByRef, Serializable, NetSerializable] public sealed partial class SoundCollectionSpecifier : SoundSpecifier { public const string Node = "collection"; diff --git a/Robust.Shared/EntitySerialization/EntityDeserializer.cs b/Robust.Shared/EntitySerialization/EntityDeserializer.cs index f563c49081a..66221b13e6c 100644 --- a/Robust.Shared/EntitySerialization/EntityDeserializer.cs +++ b/Robust.Shared/EntitySerialization/EntityDeserializer.cs @@ -690,7 +690,6 @@ private void LoadEntity( // I'm scared turning over this rock will reveal a lot of bugs. So leaving that to a future PR. // I.e., creating "temp" here just unnecessarily slows everything down. var temp = (IComponent) _seriMan.Read(compReg.Type, data, this)!; - _seriMan.CopyTo(temp, ref existing, this, notNullableOverride: true); } diff --git a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs index 67d1395813d..cfa15bec80e 100644 --- a/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs +++ b/Robust.Shared/GameObjects/Components/Renderable/SpriteLayerData.cs @@ -61,7 +61,7 @@ public sealed partial class PrototypeCopyToShaderParameters /// /// The map key of the layer that will have its shader modified. /// - [DataField(required: true)] public string LayerKey; + [DataField(required: true)] public string LayerKey = null!; /// /// The name of the shader parameter that will receive the actual selected texture. diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs index 1e6f890be29..cad1dfe3f81 100644 --- a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs +++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs @@ -17,7 +17,7 @@ public sealed partial class UserInterfaceComponent : Component, IComponentDelta public GameTick LastFieldUpdate { get; set; } /// - public GameTick[] LastModifiedFields { get; set; } + public GameTick[] LastModifiedFields { get; set; } = []; /// /// The currently open interfaces. Used clientside to store the UI. diff --git a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs index 1ce7042b779..91861776973 100644 --- a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs +++ b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs @@ -39,7 +39,7 @@ namespace Robust.Shared.Physics.Components; public sealed partial class PhysicsComponent : Component, IComponentDelta { public GameTick LastFieldUpdate { get; set; } - public GameTick[] LastModifiedFields { get; set; } + public GameTick[] LastModifiedFields { get; set; } = []; /// /// Has this body been added to an island previously in this tick. diff --git a/Robust.Shared/Physics/Shapes/Polygon.cs b/Robust.Shared/Physics/Shapes/Polygon.cs index 5961d482d65..64aed0d8b60 100644 --- a/Robust.Shared/Physics/Shapes/Polygon.cs +++ b/Robust.Shared/Physics/Shapes/Polygon.cs @@ -9,7 +9,8 @@ namespace Robust.Shared.Physics.Shapes; // Internal so people don't use it when it will have breaking changes very soon. -internal record struct Polygon : IPhysShape +[DataDefinition] +internal partial record struct Polygon : IPhysShape { [DataField] public byte VertexCount { get; internal set; } diff --git a/Robust.Shared/Physics/Shapes/SlimPolygon.cs b/Robust.Shared/Physics/Shapes/SlimPolygon.cs index 53e69a16106..72272513bac 100644 --- a/Robust.Shared/Physics/Shapes/SlimPolygon.cs +++ b/Robust.Shared/Physics/Shapes/SlimPolygon.cs @@ -15,7 +15,8 @@ namespace Robust.Shared.Physics.Shapes; /// Polygon backed by FixedArray4 to be smaller. /// Useful for internal ops where the inputs are boxes to avoid the additional padding. /// -internal record struct SlimPolygon : IPhysShape +[DataDefinition] +internal partial record struct SlimPolygon : IPhysShape { public Vector2[] Vertices => _vertices.AsSpan[..VertexCount].ToArray(); diff --git a/Robust.Shared/Prototypes/EntityPrototype.cs b/Robust.Shared/Prototypes/EntityPrototype.cs index 8cf4d7098f9..45309ff97ce 100644 --- a/Robust.Shared/Prototypes/EntityPrototype.cs +++ b/Robust.Shared/Prototypes/EntityPrototype.cs @@ -307,22 +307,28 @@ public static void EnsureCompExistsAndDeserialize(EntityUid entity, IComponent data, ISerializationContext? context) { + var existed = true; if (!entityManager.TryGetComponent(entity, compReg.Idx, out var component)) { + existed = false; var newComponent = factory.GetComponent(compName); - entityManager.AddComponent(entity, newComponent); + newComponent.Owner = entity; component = newComponent; } if (context is not EntityDeserializer map) { serManager.CopyTo(data, ref component, context, notNullableOverride: true); - return; + } + else + { + map.CurrentComponent = compName; + serManager.CopyTo(data, ref component, context, notNullableOverride: true); + map.CurrentComponent = null; } - map.CurrentComponent = compName; - serManager.CopyTo(data, ref component, context, notNullableOverride: true); - map.CurrentComponent = null; + if (!existed) + entityManager.AddComponent(entity, component); } public override string ToString() diff --git a/Robust.Shared/Serialization/ISerializationGenerated.cs b/Robust.Shared/Serialization/ISerializationGenerated.cs index 467844aa4ea..0b65cfb4499 100644 --- a/Robust.Shared/Serialization/ISerializationGenerated.cs +++ b/Robust.Shared/Serialization/ISerializationGenerated.cs @@ -1,5 +1,11 @@ using System; +using System.Collections.Generic; +using System.Collections.Immutable; using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Manager.Definition; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Validation; #pragma warning disable CS0612 // Type or member is obsolete @@ -7,6 +13,24 @@ namespace Robust.Shared.Serialization; public interface ISerializationGenerated : ISerializationGenerated { + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + T Instantiate(); + + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + static virtual T StaticInstantiate() + { + throw new NotImplementedException(); + } + + /// + [Obsolete("Use ISerializationManager.CreateCopy instead")] + static virtual object StaticInstantiateObject() + { + throw new NotImplementedException(); + } + /// [Obsolete("Use ISerializationManager.CopyTo instead")] void Copy( @@ -15,17 +39,47 @@ void Copy( SerializationHookContext hookCtx, ISerializationContext? context = null); - /// - [Obsolete("Use ISerializationManager.CopyTo instead")] - void InternalCopy( + /// + [Obsolete("Use ISerializationManager.Read instead")] + static virtual void Read( ref T target, + MappingDataNode mappingDataNode, ISerializationManager serialization, SerializationHookContext hookCtx, - ISerializationContext? context = null); + ISerializationContext? context) + { + throw new NotImplementedException(); + } - /// - [Obsolete("Use ISerializationManager.CreateCopy instead")] - T Instantiate(); + /// + [Obsolete("Use ISerializationManager.Write instead")] + static virtual void Write( + T obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues) + { + throw new NotImplementedException(); + } + + /// + [Obsolete("Use ISerializationManager.ValidateNode instead")] + static virtual void Validate( + Dictionary nodes, + MappingDataNode node, + ISerializationManager serialization, + ISerializationContext? context = null) + { + throw new NotImplementedException(); + } + + [Obsolete("Used only in serialization source generation internally")] + static virtual void GetFieldDefinitions(T? instance, List fields, string[]? fieldsParsed = null) + { + throw new NotImplementedException(); + } } public interface ISerializationGenerated @@ -38,3 +92,15 @@ void Copy( SerializationHookContext hookCtx, ISerializationContext? context = null); } + +[Obsolete("Used only in serialization source generation internally")] +public enum ISerializationGeneratedRead +{ + Default = 0 +} + +[Obsolete("Used only in serialization source generation internally")] +public enum ISerializationGeneratedCopy +{ + Default = 0 +} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs deleted file mode 100644 index 08ae3b7fec3..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Delegates.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Robust.Shared.Serialization.Markdown; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Validation; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal partial class DataDefinition - { - //todo paul make these use the mngr delegates - public delegate void PopulateDelegateSignature( - ref T target, - MappingDataNode mappingDataNode, - SerializationHookContext hookCtx, - ISerializationContext? context); - - public delegate MappingDataNode SerializeDelegateSignature( - T obj, - ISerializationContext? context, - bool alwaysWrite); - - public delegate void CopyDelegateSignature( - T source, - ref T target, - SerializationHookContext hookCtx, - ISerializationContext? context); - - private delegate ValidationNode ValidateFieldDelegate( - DataNode node, - ISerializationContext? context); - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs deleted file mode 100644 index 890e162f0dc..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.Emitters.cs +++ /dev/null @@ -1,513 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Robust.Shared.Network; -using Robust.Shared.Physics.Dynamics; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Serialization.Manager.Exceptions; -using Robust.Shared.Serialization.Markdown; -using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; -using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Utility; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal partial class DataDefinition - { - private PopulateDelegateSignature EmitPopulateDelegate(SerializationManager manager) - { - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var managerConst = Expression.Constant(manager); - - var targetParam = Expression.Parameter(typeof(T).MakeByRefType()); - var mappingDataParam = Expression.Parameter(typeof(MappingDataNode)); - var hookCtxParam = Expression.Parameter(typeof(SerializationHookContext)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - - var expressions = new List(); - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - var nodeVariable = Expression.Variable(typeof(DataNode)); - var valueVariable = Expression.Variable(fieldDefinition.FieldType); - Expression call; - if (fieldDefinition.Attribute.CustomTypeSerializer != null && (FieldInterfaceInfos[i].Reader.Value || - FieldInterfaceInfos[i].Reader.Sequence || - FieldInterfaceInfos[i].Reader.Mapping)) - { - var switchCases = new List(); - var nullable = fieldDefinition.FieldType.IsNullable(); - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - if (FieldInterfaceInfos[i].Reader.Value) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression( - Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(ValueDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(ValueDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(ValueDataNode)))); - } - - if (FieldInterfaceInfos[i].Reader.Sequence) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression(Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(SequenceDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(SequenceDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(SequenceDataNode)))); - } - - if (FieldInterfaceInfos[i].Reader.Mapping) - { - switchCases.Add(Expression.SwitchCase(Expression.Block(typeof(void), - Expression.Assign(valueVariable, SerializationManager.WrapNullableIfNeededExpression(Expression.Call( - managerConst, - "Read", - new []{fieldType, typeof(MappingDataNode), fieldDefinition.Attribute.CustomTypeSerializer}, - Expression.Convert(nodeVariable, typeof(MappingDataNode)), - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldType)), - Expression.Constant(!isNullable)), nullable))), - Expression.Constant(typeof(MappingDataNode)))); - } - - call = Expression.Switch(ExpressionUtils.GetTypeExpression(nodeVariable), - ExpressionUtils.ThrowExpression($"Unable to read node for {fieldDefinition} as valid."), - switchCases.ToArray()); - - call = Expression.IfThenElse( - Expression.Call(typeof(SerializationManager), "IsNull", Type.EmptyTypes, nodeVariable), - isNullable - ? Expression.Block(typeof(void), - Expression.Assign(valueVariable, - SerializationManager.GetNullExpression(managerConst, fieldType))) - : ExpressionUtils.ThrowExpression(), - call); - } - else - { - call = Expression.Assign(valueVariable, Expression.Call( - managerConst, - "Read", - new[] { fieldDefinition.FieldType }, - nodeVariable, - hookCtxParam, - contextParam, - Expression.Constant(null, typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(fieldDefinition.FieldType)), - Expression.Constant(!isNullable))); - } - - call = Expression.Block( - new[] { valueVariable }, - call, - AssignIfNotDefaultExpression(i, targetParam, valueVariable)); - - - if (fieldDefinition.Attribute is DataFieldAttribute dfa) - { - var tagConst = Expression.Constant(dfa.Tag); - - expressions.Add(Expression.Block( - new []{nodeVariable}, - Expression.IfThenElse( - Expression.Call( - mappingDataParam, - typeof(MappingDataNode).GetMethod("TryGet", - new [] { typeof(string), typeof(DataNode).MakeByRefType() })!, - tagConst, - nodeVariable), - call, - dfa.Required - ? ExpressionUtils.ThrowExpression(fieldDefinition.FieldType, tagConst, typeof(T)) - : AssignIfNotDefaultExpression(i, targetParam, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType)) - ))); - } - else - { - expressions.Add(Expression.Block( - new []{nodeVariable}, - Expression.Assign(nodeVariable, mappingDataParam), - call)); - } - } - - return Expression.Lambda( - Expression.Block(expressions), - targetParam, - mappingDataParam, - hookCtxParam, - contextParam).Compile(); - } - - private SerializeDelegateSignature EmitSerializeDelegate(SerializationManager manager) - { - var managerConst = Expression.Constant(manager); - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var objParam = Expression.Parameter(typeof(T)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - var alwaysWriteParam = Expression.Parameter(typeof(bool)); - - var expressions = new List(); - var mappingDataVar = Expression.Variable(typeof(MappingDataNode)); - - expressions.Add( - Expression.Assign( - mappingDataVar, - ExpressionUtils.NewExpression() - )); - - for (var i = BaseFieldDefinitions.Length - 1; i >= 0; i--) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ReadOnly) - { - continue; - } - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - Expression call; - var valueVar = Expression.Variable(fieldDefinition.FieldType); - if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].Writer) - { - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - Expression valueAccess = fieldDefinition.FieldType.IsValueType && isNullable - ? Expression.Variable(fieldType) - : Expression.Convert(valueVar, fieldType); - - call = Expression.Call( - managerConst, - "WriteValue", - new[]{fieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - valueAccess, - alwaysWriteParam, - contextParam, - Expression.Constant(!isNullable)); - - if (fieldDefinition.FieldType.IsValueType && isNullable) - { - var nodeVar = Expression.Variable(typeof(DataNode)); - call = Expression.Block( - new []{nodeVar}, - Expression.IfThenElse( - SerializationManager.StructNullHasValue(valueVar), - Expression.Block( - new[] { (ParameterExpression)valueAccess }, - Expression.Assign(valueAccess, Expression.Convert(valueVar, fieldType)), - Expression.Assign(nodeVar, SerializationManager.WrapNullableIfNeededExpression(call, true))), - isNullable - ? Expression.Assign(nodeVar, Expression.Constant(ValueDataNode.Null())) - : ExpressionUtils.ThrowExpression()), - nodeVar); - } - } - else - { - call = Expression.Call( - managerConst, - "WriteValue", - new[] { fieldDefinition.FieldType }, - valueVar, - alwaysWriteParam, - contextParam, - Expression.Constant(!isNullable)); - } - - Expression writeExpression; - var nodeVariable = Expression.Variable(typeof(DataNode)); - if (fieldDefinition.Attribute is DataFieldAttribute dfa) - { - writeExpression = Expression.IfThen(Expression.Not(Expression.Call( - mappingDataVar, - typeof(MappingDataNode).GetMethod("Has", new[] { typeof(string) })!, - Expression.Constant(dfa - .Tag))), //check if this node was already written by a type higher up the includetree - Expression.Call( - mappingDataVar, - typeof(MappingDataNode).GetMethod("Add", new[] { typeof(string), typeof(DataNode) })!, - Expression.Constant(dfa.Tag), - nodeVariable)); - } - else - { - writeExpression = Expression.IfThenElse(Expression.TypeIs(nodeVariable, typeof(MappingDataNode)), - Expression.Call( - mappingDataVar, - "Insert", - Type.EmptyTypes, - Expression.Convert(nodeVariable, typeof(MappingDataNode)), - Expression.Constant(true)), - ExpressionUtils.ThrowExpression( - $"Writing field {fieldDefinition} for type {typeof(T)} did not return a {nameof(MappingDataNode)} but was annotated to be included.")); - } - - writeExpression = Expression.Block( - new[] { nodeVariable }, - Expression.Assign(nodeVariable, call), - writeExpression); - - if (fieldDefinition.Attribute is not DataFieldAttribute { Required: true }) - { - expressions.Add(Expression.Block( - new []{valueVar}, - Expression.Assign(valueVar, AccessExpression(i, objParam)), - Expression.IfThen( - Expression.OrElse(alwaysWriteParam, - Expression.Not(IsDefault(i, valueVar, fieldDefinition))), - writeExpression))); - } - else - { - expressions.Add( - Expression.Block( - new []{valueVar}, - Expression.Assign(valueVar, AccessExpression(i, objParam)), - writeExpression)); - } - } - - expressions.Add(mappingDataVar); - - return Expression.Lambda( - Expression.Block( - new []{mappingDataVar}, - expressions), - objParam, - contextParam, - alwaysWriteParam).Compile(); - } - - private CopyDelegateSignature EmitCopyDelegate(SerializationManager manager) - { - var managerConst = Expression.Constant(manager); - var isServer = manager.DependencyCollection.Resolve().IsServer; - - var sourceParam = Expression.Parameter(typeof(T)); - var targetParam = Expression.Parameter(typeof(T).MakeByRefType()); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - var hookCtxParam = Expression.Parameter(typeof(SerializationHookContext)); - - var expressions = new List(); - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - - if (fieldDefinition.Attribute.ServerOnly && !isServer) - { - continue; - } - - var isNullable = NullableHelper.IsMarkedAsNullable(fieldDefinition.FieldInfo); - - Expression call; - if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].Copier) - { - var targetValue = Expression.Variable(fieldDefinition.FieldType.EnsureNotNullableType()); - var finalTargetValue = Expression.Variable(fieldDefinition.FieldType); - var fieldType = fieldDefinition.FieldType.EnsureNotNullableType(); - - var sourceAccess = fieldType.IsValueType && isNullable - ? Expression.Variable(fieldType) - : AccessExpression(i, sourceParam); - - call = Expression.Block( - Expression.Call( - managerConst, - "CopyTo", - new[]{fieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - sourceAccess, - targetValue, - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)), - Expression.Assign(finalTargetValue, Expression.Convert(targetValue, fieldDefinition.FieldType))); - - //null check for non-value types is handled in copyto. we are just making sure the types match up - if (isNullable && fieldType.IsValueType) - { - var sourceValue = Expression.Variable(fieldDefinition.FieldType); - call = Expression.Block( - new[] { sourceValue, (ParameterExpression)sourceAccess }, - Expression.Assign(sourceValue, AccessExpression(i, sourceParam)), - Expression.IfThenElse(SerializationManager.StructNullHasValue(sourceValue), - Expression.Block( - Expression.Assign(sourceAccess, Expression.Convert(sourceValue, fieldType)), - call), - Expression.Assign(finalTargetValue, - SerializationManager.GetNullExpression(managerConst, fieldType)))); - } - - call = Expression.Block( - new[] { finalTargetValue, targetValue }, - Expression.Assign(targetValue, manager.InstantiationExpression(managerConst, fieldDefinition.FieldType.EnsureNotNullableType())), - call, - finalTargetValue); - } - else if (fieldDefinition.Attribute.CustomTypeSerializer != null && FieldInterfaceInfos[i].CopyCreator) - { - call = Expression.Call( - managerConst, - "CreateCopy", - new []{fieldDefinition.FieldType, fieldDefinition.Attribute.CustomTypeSerializer}, - AccessExpression(i, sourceParam), - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)); - } - else - { - call = Expression.Call( - managerConst, - "CreateCopy", - new[] { fieldDefinition.FieldType }, - AccessExpression(i, sourceParam), - hookCtxParam, - contextParam, - Expression.Constant(!isNullable)); - } - - expressions.Add(AssignIfNotDefaultExpression(i, targetParam, call)); - } - - return Expression.Lambda( - Expression.Block(expressions), - sourceParam, - targetParam, - hookCtxParam, - contextParam).Compile(); - } - - private ValidateFieldDelegate EmitFieldValidationDelegate(SerializationManager manager, int i) - { - var managerConst = Expression.Constant(manager); - - var nodeParam = Expression.Parameter(typeof(DataNode)); - var contextParam = Expression.Parameter(typeof(ISerializationContext)); - - var field = BaseFieldDefinitions[i]; - var interfaceInfo = FieldInterfaceInfos[i]; - - var fieldType = field.FieldType.EnsureNotNullableType(); - - var switchCases = new List(); - if (interfaceInfo.Validator.Value) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(ValueDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(ValueDataNode)), - contextParam), - Expression.Constant(typeof(ValueDataNode)))); - } - - if (interfaceInfo.Validator.Sequence) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(SequenceDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(SequenceDataNode)), - contextParam), - Expression.Constant(typeof(SequenceDataNode)))); - } - - if (interfaceInfo.Validator.Mapping) - { - switchCases.Add(Expression.SwitchCase( - Expression.Call( - managerConst, - "ValidateNode", - new []{fieldType, typeof(MappingDataNode), field.Attribute.CustomTypeSerializer!}, - Expression.Convert(nodeParam, typeof(MappingDataNode)), - contextParam), - Expression.Constant(typeof(MappingDataNode)))); - } - - var @switch = Expression.Switch(ExpressionUtils.GetTypeExpression(nodeParam), - Expression.Call( - managerConst, - "ValidateNode", - new[] { fieldType }, - nodeParam, - contextParam), - switchCases.ToArray()); - - return Expression.Lambda( - @switch, - nodeParam, - contextParam).Compile(); - } - - private Expression AssignIfNotDefaultExpression(int i, Expression obj, Expression value) - { - var assigner = FieldAssigners[i]; - Expression assignerExpr; - - if (assigner is FieldInfo fieldInfo) - assignerExpr = Expression.Assign(Expression.Field(obj, fieldInfo), value); - else if (assigner is MethodInfo methodInfo) - assignerExpr = Expression.Call(obj, methodInfo, value); - else - assignerExpr = Expression.Invoke(Expression.Constant(assigner), obj, value); - - return Expression.IfThen( - Expression.Not(ExpressionUtils.EqualExpression( - Expression.Constant(DefaultValues[i], BaseFieldDefinitions[i].FieldType), value)), - assignerExpr); - } - - private Expression AccessExpression(int i, Expression obj) - { - var accessor = FieldAccessors[i]; - if (accessor is FieldInfo fieldInfo) - return Expression.Field(obj, fieldInfo); - - if (accessor is MethodInfo methodInfo) - return Expression.Call(obj, methodInfo); - - return Expression.Invoke(Expression.Constant(accessor), obj); - } - - private Expression IsDefault(int i, Expression left, FieldDefinition fieldDefinition) - { - return ExpressionUtils.EqualExpression(left, Expression.Constant(DefaultValues[i], fieldDefinition.FieldType)); - } - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs index d7ab422540f..dfac4a42339 100644 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinition.cs @@ -1,217 +1,119 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Linq; using JetBrains.Annotations; -using Robust.Shared.Log; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown.Mapping; -using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Serialization.Markdown.Value; -using Robust.Shared.Serialization.TypeSerializers.Interfaces; using Robust.Shared.Utility; -using YamlDotNet.Serialization.NamingConventions; using static Robust.Shared.Serialization.Manager.SerializationManager; namespace Robust.Shared.Serialization.Manager.Definition { public abstract class DataDefinition { - internal ImmutableArray BaseFieldDefinitions { get; init; } +#pragma warning disable CS0618 + internal ImmutableArray BaseFieldDefinitions { get; init; } internal bool IsRecord { get; init; } + internal abstract PopulateDelegateSignature PopulateObj { get; init; } + internal abstract ISerializationManager.InstantiationDelegate InstantiateObj { get; init; } +#pragma warning restore CS0618 - public abstract bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates); + public abstract bool TryGetDuplicates(out string[] duplicates); } - internal sealed partial class DataDefinition : DataDefinition where T : notnull + internal sealed class DataDefinition : DataDefinition where T : ISerializationGenerated { - private readonly struct FieldInterfaceInfo - { - public readonly (bool Value, bool Sequence, bool Mapping) Reader; - public readonly bool Writer; - public readonly bool Copier; - public readonly bool CopyCreator; - public readonly (bool Value, bool Sequence, bool Mapping) Validator; - - - public FieldInterfaceInfo((bool Value, bool Sequence, bool Mapping) reader, bool writer, bool copier, bool copyCreator, (bool Value, bool Sequence, bool Mapping) validator) - { - Reader = reader; - Writer = writer; - Copier = copier; - CopyCreator = copyCreator; - Validator = validator; - } - } +#pragma warning disable CS0618 + internal readonly PopulateDelegateSignature Populate; + internal readonly SerializeDelegateSignature Serialize; + internal readonly CopyDelegateSignature CopyTo; - internal readonly PopulateDelegateSignature Populate; - internal readonly SerializeDelegateSignature Serialize; - internal readonly CopyDelegateSignature CopyTo; + internal override PopulateDelegateSignature PopulateObj { get; init; } +#pragma warning restore CS0618 + internal override ISerializationManager.InstantiationDelegate InstantiateObj { get; init; } [UsedImplicitly] internal DataDefinition(SerializationManager manager, bool isRecord) { IsRecord = isRecord; - var fieldDefs = GetFieldDefinitions(manager, isRecord); - foreach (var field in fieldDefs) +#pragma warning disable CS0618 + var fieldDefs = new List(); + T.GetFieldDefinitions(default, fieldDefs); +#pragma warning restore CS0618 + for (var i = 0; i < fieldDefs.Count; i++) { - if (field.Attribute is not DataFieldAttribute attribute || - attribute.Tag != null) - { - continue; - } - - attribute.Tag = DataDefinitionUtility.AutoGenerateTag(field.FieldInfo.Name); + var field = fieldDefs[i]; + if (field is { IsDataField: true, Tag: null }) + field.Tag = DataDefinitionUtility.AutoGenerateTag(field.FieldInfoName); } + fieldDefs.Sort((a, b) => + { + var priority = b.Priority.CompareTo(a.Priority); + if (priority != 0) + return priority; + + return b.FieldInfoName.CompareTo(a.FieldInfoName, StringComparison.OrdinalIgnoreCase); + }); + var dataFields = fieldDefs - .Select(f => f.Attribute) - .OfType().ToArray(); + .Where(f => f.IsDataField) + .Select(f => f.Tag ?? f.CamelCasedName) + .ToArray(); Duplicates = dataFields .Where(f => - dataFields.Count(df => df.Tag == f.Tag) > 1) - .Select(f => f.Tag!) + dataFields.Count(df => df == f) > 1) .Distinct() .ToArray(); - var fields = fieldDefs; - - fields.Sort((a, b) => b.Attribute.Priority.CompareTo(a.Attribute.Priority)); - - BaseFieldDefinitions = fields.ToImmutableArray(); - - DefaultValues = fieldDefs.Select(f => f.DefaultValue).ToArray(); - var fieldAssigners = new object[BaseFieldDefinitions.Length]; - var fieldAccessors = new object[BaseFieldDefinitions.Length]; - var fieldValidators = new ValidateFieldDelegate[BaseFieldDefinitions.Length]; - - var interfaceInfos = new FieldInterfaceInfo[BaseFieldDefinitions.Length]; - - for (var i = 0; i < BaseFieldDefinitions.Length; i++) - { - var fieldDefinition = BaseFieldDefinitions[i]; - fieldAssigners[i] = InternalReflectionUtils.EmitFieldAssigner(typeof(T), fieldDefinition.BackingField); - fieldAccessors[i] = InternalReflectionUtils.EmitFieldAccessor(typeof(T), fieldDefinition); - - if (fieldDefinition.Attribute.CustomTypeSerializer != null) - { - //reader (value, sequence, mapping), writer, copier - var reader = (false, false, false); - var writer = false; - var copier = false; - var copyCreator = false; - var validator = (false, false, false); - foreach (var @interface in fieldDefinition.Attribute.CustomTypeSerializer.GetInterfaces()) - { - DebugTools.Assert(@interface.IsGenericType, $"Tried to use a custom type serializer for {GetType()} that isn't generic?"); - var genericTypedef = @interface.GetGenericTypeDefinition(); - if (genericTypedef == typeof(ITypeWriter<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - writer = true; - } - } - else if (genericTypedef == typeof(ITypeCopier<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - copier = true; - } - } - else if (genericTypedef == typeof(ITypeCopyCreator<>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - copyCreator = true; - } - } - else if (genericTypedef == typeof(ITypeReader<,>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - if (@interface.GenericTypeArguments[1] == typeof(ValueDataNode)) - { - reader.Item1 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(SequenceDataNode)) - { - reader.Item2 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(MappingDataNode)) - { - reader.Item3 = true; - } - } - } - else if (genericTypedef == typeof(ITypeValidator<,>)) - { - if (@interface.GenericTypeArguments[0].IsAssignableTo(fieldDefinition.FieldType)) - { - if (@interface.GenericTypeArguments[1] == typeof(ValueDataNode)) - { - validator.Item1 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(SequenceDataNode)) - { - validator.Item2 = true; - } - else if (@interface.GenericTypeArguments[1] == typeof(MappingDataNode)) - { - validator.Item3 = true; - } - } - } - } + BaseFieldDefinitions = fieldDefs.ToImmutableArray(); - if (!reader.Item1 && !reader.Item2 && !reader.Item3 && !writer && !copier && !validator.Item1 && !validator.Item2 && !validator.Item3) - { - throw new InvalidOperationException( - $"Could not find any fitting implementation of ITypeReader, ITypeWriter or ITypeCopier for field {fieldDefinition}({fieldDefinition.FieldType}) on type {typeof(T)} on CustomTypeSerializer {fieldDefinition.Attribute.CustomTypeSerializer}"); - } + DefaultValues = fieldDefs.Select(f => f.DefaultValue).ToImmutableArray(); + DefaultValuesDict = fieldDefs.ToImmutableDictionary( + f => f.IsDataField + ? f.Tag ?? f.CamelCasedName + : f.CamelCasedName, + f => f.DefaultValue + ); - interfaceInfos[i] = new FieldInterfaceInfo(reader, writer, copier, copyCreator, validator); - } - } - - FieldInterfaceInfos = interfaceInfos.ToImmutableArray(); - FieldAssigners = fieldAssigners.ToImmutableArray(); - FieldAccessors = fieldAccessors.ToImmutableArray(); - - for (int i = 0; i < BaseFieldDefinitions.Length; i++) + //has to be done after fieldinterfaceinfos are done +#pragma warning disable CS0618 + // TODO source gen this one too! + PopulateObj = (ref target, node, serialization, ctx, context) => { - //has to be done after fieldinterfaceinfos are done - fieldValidators[i] = EmitFieldValidationDelegate(manager, i); - } - - FieldValidators = fieldValidators.ToImmutableArray(); - - Populate = EmitPopulateDelegate(manager); - Serialize = EmitSerializeDelegate(manager); - CopyTo = EmitCopyDelegate(manager); + var obj = (T) target; + T.Read(ref obj, node, serialization, ctx, context); + target = obj; + }; + + Populate = T.Read; + Serialize = T.Write; + CopyTo = (source, ref target, ctx, context) => source.Copy(ref target, manager, ctx, context); // TODO source gen this one too! + FieldValidators = T.Validate; + InstantiateObj = T.StaticInstantiateObject; +#pragma warning restore CS0618 } private string[] Duplicates { get; } - private object?[] DefaultValues { get; } - - private ImmutableArray FieldInterfaceInfos { get; } - - private ImmutableArray FieldAssigners { get; } - private ImmutableArray FieldAccessors { get; } + internal ImmutableArray DefaultValues { get; } + internal ImmutableDictionary DefaultValuesDict { get; } - private ImmutableArray FieldValidators { get; } +#pragma warning disable CS0618 + private ValidateAllFieldsDelegate FieldValidators { get; } +#pragma warning restore CS0618 private bool TryGetIndex(string tag, out int index) { for (index = 0; index < BaseFieldDefinitions.Length; index++) { - if (BaseFieldDefinitions[index].Attribute is DataFieldAttribute dataFieldAttribute && - dataFieldAttribute.Tag == tag) + var field = BaseFieldDefinitions[index]; + if (field.IsDataField && field.Tag == tag) return true; } @@ -223,8 +125,9 @@ private bool TryGetIncludeMappingPair(List includeValidati foreach (var includeValidation in includeValidations) { if (includeValidation.Mapping.TryFirstOrNull(x => - x.Key is ValidatedValueNode valVal && valVal.DataNode is ValueDataNode valNode && - valNode.Value == key, out var validatedPair)) + x.Key is ValidatedValueNode { DataNode: ValueDataNode valNode } && + valNode.Value == key, + out var validatedPair)) { pair = validatedPair.Value; return true; @@ -241,18 +144,19 @@ public ValidationNode Validate( ISerializationContext? context) { var validatedMapping = new Dictionary(); - var includeValidations = new List(); - for (var i = 0; i < BaseFieldDefinitions.Length; i++) + var validations = new Dictionary(); + FieldValidators(validations, mapping, serialization, context); + + foreach (var fieldDefinition in BaseFieldDefinitions) { - var fieldDefinition = BaseFieldDefinitions[i]; - if (fieldDefinition.Attribute is not IncludeDataFieldAttribute) continue; + if (!fieldDefinition.IsIncludeDataField) continue; - var validationNode = FieldValidators[i](mapping, context); + var validationNode = validations[fieldDefinition.CamelCasedName]; if (validationNode is ErrorNode errorNode) { - validatedMapping.Add(new InconclusiveNode(new ValueDataNode($"<{nameof(IncludeDataFieldAttribute)}={fieldDefinition.FieldInfo.Name}>")), errorNode); + validatedMapping.Add(new InconclusiveNode(new ValueDataNode($"<{nameof(IncludeDataFieldAttribute)}={fieldDefinition.FieldInfoName}>")), errorNode); continue; } @@ -285,7 +189,7 @@ public ValidationNode Validate( ValidationNode valNode; if (IsNull(val)) { - if (!NullableHelper.IsMarkedAsNullable(BaseFieldDefinitions[idx].FieldInfo)) + if (!BaseFieldDefinitions[idx].FieldInfoNullable) { var error = new ErrorNode( val, @@ -299,7 +203,7 @@ public ValidationNode Validate( } else { - valNode = FieldValidators[idx](val, context); + valNode = validations[key]; } //include node errors override successful nodes on the main datadef @@ -318,128 +222,10 @@ public ValidationNode Validate( return new ValidatedMappingNode(validatedMapping); } - public override bool TryGetDuplicates([NotNullWhen(true)] out string[] duplicates) + public override bool TryGetDuplicates(out string[] duplicates) { duplicates = Duplicates; return duplicates.Length > 0; } - - private bool GatherFieldData(AbstractFieldInfo fieldInfo, out DataFieldBaseAttribute? dataFieldBaseAttribute, - [NotNullWhen(true)]out AbstractFieldInfo? backingField, [NotNullWhen(true)] ref InheritanceBehavior? inheritanceBehavior) - { - dataFieldBaseAttribute = null; - backingField = fieldInfo; - inheritanceBehavior ??= InheritanceBehavior.Default; - - if (fieldInfo.HasAttribute(true)) - inheritanceBehavior = InheritanceBehavior.Always; - else if (fieldInfo.HasAttribute(true)) - inheritanceBehavior = InheritanceBehavior.Never; - - if (fieldInfo is SpecificPropertyInfo propertyInfo) - { - // We only want the most overriden instance of a property for the type we are working with - if (!propertyInfo.IsMostOverridden(typeof(T))) - return false; - - if (propertyInfo.PropertyInfo.GetMethod == null) - { - Logger.ErrorS(LogCategory, $"Property {propertyInfo} is annotated with DataFieldAttribute but has no getter"); - return false; - } - } - - // Most data fields have an explicit data field attribute - if (fieldInfo.TryGetAttribute(out var dataFieldAttribute, true)) - return GatherDataFieldData(fieldInfo, out dataFieldBaseAttribute, ref backingField, dataFieldAttribute); - - if (fieldInfo.TryGetAttribute(out var includeDataFieldAttribute, true)) - { - dataFieldBaseAttribute = includeDataFieldAttribute; - return true; - } - - // This field/property has no explicit data field related annotations. However, things like - // DataRecordAttribute will cause all fields to be interpreted as data fields, so we still handle them - - if (fieldInfo is not SpecificPropertyInfo) - return true; - - var potentialBackingField = fieldInfo.GetBackingField(); - if (potentialBackingField == null) - return false; - - return GatherFieldData(potentialBackingField, - out dataFieldBaseAttribute, - out backingField, - ref inheritanceBehavior); - } - - private static bool GatherDataFieldData( - AbstractFieldInfo fieldInfo, - out DataFieldBaseAttribute dataFieldBaseAttribute, - ref AbstractFieldInfo backingField, - DataFieldAttribute dataFieldAttribute) - { - dataFieldBaseAttribute = dataFieldAttribute; - - if (fieldInfo is not SpecificPropertyInfo property - || dataFieldAttribute.ReadOnly - || property.PropertyInfo.SetMethod != null) - { - return true; - } - - if (!property.TryGetBackingField(out var backingFieldInfo)) - { - Logger.ErrorS(LogCategory, $"Property {property} in type {property.DeclaringType} is annotated with DataFieldAttribute as non-readonly but has no auto-setter"); - return false; - } - - backingField = backingFieldInfo; - return true; - } - - private List GetFieldDefinitions(SerializationManager manager, bool isRecord) - { - var dummyObject = manager.GetOrCreateInstantiator(isRecord)(); - var fieldDefinitions = new List(); - - foreach (var abstractFieldInfo in typeof(T).GetAllPropertiesAndFields()) - { - if (abstractFieldInfo.IsBackingField()) - continue; - - if (isRecord && abstractFieldInfo.IsAutogeneratedRecordMember()) - continue; - - InheritanceBehavior? inheritanceBehavior = InheritanceBehavior.Default; - if (!GatherFieldData(abstractFieldInfo, out var dataFieldBaseAttribute, out var backingField, - ref inheritanceBehavior)) - continue; - - if (dataFieldBaseAttribute == null) - { - if (!isRecord) - continue; - - dataFieldBaseAttribute = new DataFieldAttribute(CamelCaseNamingConvention.Instance.Apply(abstractFieldInfo.Name)); - } - - var fieldDefinition = new FieldDefinition( - dataFieldBaseAttribute, - abstractFieldInfo.GetValue(dummyObject), - abstractFieldInfo, - backingField, - inheritanceBehavior.Value); - - fieldDefinitions.Add(fieldDefinition); - } - - // There should be no duplicates - // I.e., we haven't accidentally included a property's backing field twice? - DebugTools.Assert(fieldDefinitions.Select(x=> x.FieldInfo).Distinct().Count() == fieldDefinitions.Count); - return fieldDefinitions; - } } } diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs new file mode 100644 index 00000000000..278fda6c4e5 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionDelegates.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Validation; + +namespace Robust.Shared.Serialization.Manager.Definition; + +//todo paul make these use the mngr delegates +[Obsolete("Used only in source generation")] +internal delegate void CopyDelegateSignature( + T source, + ref T target, + SerializationHookContext hookCtx, + ISerializationContext? context); + +[Obsolete("Used only in source generation")] +internal delegate void PopulateDelegateSignature( + ref T target, + MappingDataNode mappingDataNode, + ISerializationManager serialization, + SerializationHookContext hookCtx, + ISerializationContext? context +); + +[Obsolete("Used only in source generation")] +internal delegate void SerializeDelegateSignature( + T obj, + MappingDataNode mapping, + ISerializationManager serialization, + ISerializationContext? context, + bool alwaysWrite, + ImmutableDictionary defaultValues +); + +[Obsolete("Used only in source generation")] +internal delegate void ValidateAllFieldsDelegate( + Dictionary nodes, + MappingDataNode node, + ISerializationManager serialization, + ISerializationContext? context = null +); diff --git a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs index 72aec126e46..f16632ad7db 100644 --- a/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs +++ b/Robust.Shared/Serialization/Manager/Definition/DataDefinitionUtility.cs @@ -6,6 +6,9 @@ public static class DataDefinitionUtility { public static string AutoGenerateTag(string name) { + if (name == "ID") + return "id"; + var span = name.AsSpan(); return $"{char.ToLowerInvariant(span[0])}{span.Slice(1).ToString()}"; } diff --git a/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs new file mode 100644 index 00000000000..5d581fbc6e9 --- /dev/null +++ b/Robust.Shared/Serialization/Manager/Definition/DataFieldDefinition.cs @@ -0,0 +1,27 @@ +using System; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Utility; +using YamlDotNet.Serialization.NamingConventions; + +namespace Robust.Shared.Serialization.Manager.Definition; + +[Obsolete("Only used in serialization source generators")] +public record struct DataFieldDefinition( + string? Tag, + int Priority, + bool IsDataField, + bool IsIncludeDataField, + object? DefaultValue, + InheritanceBehavior InheritanceBehavior, + string FieldInfoName, + Type FieldType, + bool FieldInfoNullable, + string CamelCasedName, + Type? CustomTypeSerializer +) +{ + public override string ToString() + { + return $"{FieldInfoName}({Tag ?? CamelCasedName})"; + } +} diff --git a/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs b/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs deleted file mode 100644 index 1d62d7aa350..00000000000 --- a/Robust.Shared/Serialization/Manager/Definition/FieldDefinition.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Robust.Shared.Serialization.Manager.Attributes; -using Robust.Shared.Utility; - -namespace Robust.Shared.Serialization.Manager.Definition -{ - internal sealed class FieldDefinition - { - public FieldDefinition( - DataFieldBaseAttribute attr, - object? defaultValue, - AbstractFieldInfo fieldInfo, - AbstractFieldInfo backingField, - InheritanceBehavior inheritanceBehavior) - { - BackingField = backingField; - Attribute = attr; - DefaultValue = defaultValue; - FieldInfo = fieldInfo; - InheritanceBehavior = inheritanceBehavior; - } - - public DataFieldBaseAttribute Attribute { get; } - - public object? DefaultValue { get; } - - public InheritanceBehavior InheritanceBehavior { get; } - - public AbstractFieldInfo BackingField { get; } - - public AbstractFieldInfo FieldInfo { get; } - - public Type FieldType => FieldInfo.FieldType; - - public object? GetValue(object? obj) - { - return BackingField.GetValue(obj); - } - - public void SetValue(object? obj, object? value) - { - BackingField.SetValue(obj, value); - } - - public override string ToString() - { - return $"{FieldInfo.Name}({Attribute})"; - } - } -} diff --git a/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs b/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs index b992b19852f..42b6cd97530 100644 --- a/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs +++ b/Robust.Shared/Serialization/Manager/Definition/InheritanceBehavior.cs @@ -2,8 +2,8 @@ { public enum InheritanceBehavior : byte { - Default, - Always, - Never + Default = 0, + Always = 1, + Never = 2 } } diff --git a/Robust.Shared/Serialization/Manager/ISerializationManager.cs b/Robust.Shared/Serialization/Manager/ISerializationManager.cs index 4abe1d444bb..09cd166b5e1 100644 --- a/Robust.Shared/Serialization/Manager/ISerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/ISerializationManager.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Robust.Shared.Reflection; @@ -14,6 +16,8 @@ public interface ISerializationManager { public delegate T InstantiationDelegate(); + bool IsServer { get; } + /// /// Initializes the serialization manager. /// @@ -109,6 +113,7 @@ ValidationNode ValidateNode(TNode node, /// The type of object to create and populate. /// The deserialized object, or null. T Read(DataNode node, ISerializationContext? context = null, bool skipHook = false, InstantiationDelegate? instanceProvider = null, [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + T Read( DataNode node, SerializationHookContext hookCtx, @@ -116,6 +121,34 @@ T Read( InstantiationDelegate? instanceProvider = null, [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + T[] ReadArray( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false); + + T ReadDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : ISerializationGenerated; + + T ReadEnum( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : struct, Enum; + + T ReadStructDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + InstantiationDelegate? instanceProvider = null, + [NotNullableFlag(nameof(T))] bool notNullableOverride = false) where T : struct, ISerializationGenerated; + /// /// Deserializes a node into a populated object of the given generic type using the provided instance. diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs index 13d797637fa..8675fff33c0 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Composition.cs @@ -181,17 +181,17 @@ private MappingDataNode PushInheritanceDefinition(MappingDataNode child, Mapping { var newMapping = child.Copy(); var processedTags = new HashSet(); - var fieldQueue = new Queue(definition.BaseFieldDefinitions); + var fieldQueue = new Queue(definition.BaseFieldDefinitions); while (fieldQueue.TryDequeue(out var field)) { if (field.InheritanceBehavior == InheritanceBehavior.Never) continue; - if (field.Attribute is DataFieldAttribute dfa) + if (field.IsDataField) { // tag is set on data definition creation - if(!processedTags.Add(dfa.Tag!)) continue; //tag was already processed, probably because we are using the same tag in an include + if(!processedTags.Add(field.Tag!)) continue; //tag was already processed, probably because we are using the same tag in an include - var key = dfa.Tag!; + var key = field.Tag!; if (parent.TryGetValue(key, out var parentValue)) { if (newMapping.TryGetValue(key, out var childValue)) diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs index ede48426650..8994e3f696b 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs @@ -109,12 +109,18 @@ static object ValueFactory(Type baseType, Type actualType, SerializationManager Expression.Constant(false)), Expression.Constant(true)); } - else + else if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) { call = Expression.Call(instanceParam, nameof(CopyToInternal), new[] { actualType }, sourceVar, targetVar, Expression.Constant(manager.GetDefinition(actualType), typeof(DataDefinition<>).MakeGenericType(actualType)), instanceParam, hookCtxParam, contextParam); } + else + { + call = Expression.Block( + Expression.Assign(targetVar, sourceVar), + Expression.Constant(true)); + } if (!sameType) { @@ -235,7 +241,7 @@ private CreateCopyGenericDelegate GetOrCreateCreateCopyGenericDelegate() contextParam, Expression.Constant(false)), type); } - else + else if (typeof(T).IsAssignableTo(typeof(ISerializationGenerated))) { call = Expression.Call( instanceParam, @@ -246,6 +252,16 @@ private CreateCopyGenericDelegate GetOrCreateCreateCopyGenericDelegate() contextParam, Expression.Constant(manager.GetDefinition(type), typeof(DataDefinition<>).MakeGenericType(type))); } + else + { + call = Expression.Call( + instanceParam, + nameof(CreateCopyInternalNotGenerated), + new[] {type}, + sourceParamAccess, + hookCtxParam, + contextParam); + } } return Expression.Lambda>( @@ -271,13 +287,15 @@ private bool CopyToInternal( ISerializationManager serializationManager, SerializationHookContext hookCtx, ISerializationContext? context) - where TCommon : notnull + where TCommon : ISerializationGenerated { if (context != null && context.SerializerProvider.TryGetTypeSerializer, TCommon>(out var copier)) { var commonTarget = target; copier.CopyTo(this, source, ref commonTarget, DependencyCollection, hookCtx, context); + target = commonTarget; + return true; } if (ShouldReturnSource(typeof(TCommon))) //todo paul can be precomputed @@ -338,7 +356,7 @@ private T[] CreateArrayCopy(T[] source, SerializationHookContext hookCtx, ISe return copy; } - private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISerializationContext context, DataDefinition? definition) where T : notnull + private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISerializationContext context, DataDefinition? definition) where T : ISerializationGenerated { if (source is DataNode node) return (T)(object)node.Copy(); @@ -370,6 +388,26 @@ private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISer } } + private T CreateCopyInternalNotGenerated(T source, SerializationHookContext hookCtx, ISerializationContext context) + { + if (source is DataNode node) + return (T)(object)node.Copy(); + + ref readonly var information = ref SerializedType.Information; + if (information.ReturnSource || typeof(T).IsValueType) + { + return source; + } + + var target = GetOrCreateInstantiator(false)(); + + if (!GetOrCreateCopyToGenericDelegate(source)(source, ref target, hookCtx, context)) + { + throw new CopyToFailedException(); + } + return target!; + } + private void NotNullOverrideCheck(bool notNullableOverride, Type? type = null) { if (notNullableOverride || (type != null && !type.IsNullable())) throw new NullNotAllowedException(); @@ -397,13 +435,6 @@ public void CopyTo( return; } - if (source is ISerializationGenerated generated) - { - generated.Copy(ref target!, this, hookCtx, context); - RunAfterHook(target, hookCtx); - return; - } - if (target == null) { target = CreateCopy(source, hookCtx, context); @@ -438,7 +469,7 @@ public void CopyTo( } ref readonly var information = ref SerializedType.Information; - if (information.SerializationGenerated) + if (information.SerializationGenerated && !typeof(T).IsAbstract && !typeof(T).IsInterface) { var generated = Unsafe.As>(source); target ??= generated.Instantiate(); diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs index 17e701d3017..eeefefe85a0 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Instantiation.cs @@ -156,6 +156,16 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< return (ISerializationManager.InstantiationDelegate)_instantiators.GetOrAdd(type, static (type, isRecord) => { + var generated = typeof(ISerializationGenerated<>).MakeGenericType(type); + if (type.IsAssignableTo(generated)) + { + var instantiate = type + .GetMethod(nameof(ISerializationGenerated<>.StaticInstantiate), BindingFlags.Public | BindingFlags.Static)! + .CreateDelegate(typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(type)); + + return instantiate; + } + var method = new DynamicMethod( "Instantiator", type, @@ -180,15 +190,4 @@ internal ISerializationManager.InstantiationDelegate GetOrCreateInstantiator< return method.CreateDelegate(typeof(ISerializationManager.InstantiationDelegate<>).MakeGenericType(type)); }, isDataRecord); } - - //we can safely set isDataRecord to false here due to a delegate already existing if it if it were - private T InstantiateValue() => GetOrCreateInstantiator(false)(); - - internal MethodCallExpression InstantiationExpression(ConstantExpression managerConst, Type type) - { - return Expression.Call( - managerConst, - nameof(InstantiateValue), - new[] { type }); - } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs index 11b9b7abfc9..7fc5e606fbd 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Concurrent; using System.Diagnostics; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown; @@ -30,9 +34,18 @@ private delegate T ReadGenericDelegate( ISerializationContext? context = null, ISerializationManager.InstantiationDelegate? instanceProvider = null); + private delegate object? ReadDelegate( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + bool notNullableOverride = false + ); + + private MethodInfo _read = default!; private readonly ConcurrentDictionary<(Type type, bool notNullableOverride), ReadBoxingDelegate> _readBoxingDelegates = new(); private readonly ConcurrentDictionary<(Type baseType, Type actualType, Type node, bool notNullableOverride), object> _readGenericBaseDelegates = new(); private readonly ConcurrentDictionary<(Type value, Type node, bool notNullableOverride), object> _readGenericDelegates = new(); + private readonly ConcurrentDictionary> _reads = new(); public T Read(DataNode node, ISerializationContext? context = null, bool skipHook = false, ISerializationManager.InstantiationDelegate? instanceProvider = null, bool notNullableOverride = false) { @@ -44,6 +57,492 @@ public T Read(DataNode node, ISerializationContext? context = null, bool skip notNullableOverride); } + public T[] ReadArray( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + var isValueType = baseType.IsValueType; + + T[]? val = null; + if (instanceProvider != null) + val ??= []; + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, MappingDataNode>( + out var serializer)) + { + val = serializer.Read(this, mapping, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, SequenceDataNode>( + out var serializer)) + { + val = serializer.Read(this, sequence, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializerArray, T, ValueDataNode>( + out var serializer)) + { + val = serializer.Read(this, value, DependencyCollection, hookCtx, context, instanceProvider); + if (notNullableOverride) + Debug.Assert(val != null, "Reader call returned null value! Forbidden!"); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = null; + else + throw new NullNotAllowedException(); + } + + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val!; + + void RegularRead() + { + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, MappingDataNode>(out var reader)) + { + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, SequenceDataNode>(out var reader)) + { + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializerArray, T, ValueDataNode>(out var reader)) + { + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + val = node switch + { + SequenceDataNode sequence => ReadArraySequence(sequence, hookCtx, context), + ValueDataNode value => ReadArrayValue(value, hookCtx, context), + _ => throw new ArgumentException($"Cannot read array from data node type {node.GetType()}") + }; + } + } + + public T ReadStructDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + where T : struct, ISerializationGenerated + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = default!; + else if (baseType == typeof(EntityUid)) + val = default!; + else + throw new NullNotAllowedException(); + } + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsArray) + { + val = node switch + { + SequenceDataNode sequence => (T)(object)ReadArraySequence(sequence, hookCtx, context), + ValueDataNode value => (T)(object)ReadArrayValue(value, hookCtx, context), + _ => throw new ArgumentException($"Cannot read array from data node type {node.GetType()}") + }; + } + else if (baseType.IsEnum) + { + val = node switch + { + SequenceDataNode sequence => ReadEnumSequence(sequence), + ValueDataNode value => ReadEnumValue(value), + _ => throw new InvalidNodeTypeException( + $"Cannot serialize node as {baseType}, unsupported node type {node.GetType()}") + }; + } + else if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + var selfSerialize = (ISelfSerialize) val; + selfSerialize.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + T.Read(ref val, mapping, this, hookCtx, context); + break; + } + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {typeof(T)} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } + } + + public T ReadEnum( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) + where T : struct, Enum + { + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + break; + } + } + } + else + { + RegularRead(); + } + + if (node.IsNull) + { + if (nullable && !notNullableOverride) + val = default!; + else + throw new NullNotAllowedException(); + } + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (hasSerializer) + return; + + val = node switch + { + SequenceDataNode sequence => ReadEnumSequence(sequence), + ValueDataNode value => ReadEnumValue(value), + _ => throw new InvalidNodeTypeException( + $"Cannot serialize node as {baseType}, unsupported node type {node.GetType()}") + }; + } + } + + private object? ReadObject( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + bool notNullableOverride = false) + { + return Read(node, hookCtx, context, null, notNullableOverride); + } + public T Read( DataNode node, SerializationHookContext hookCtx, @@ -64,10 +563,12 @@ public T Read( { if (instanceProvider != null) { - var val = instanceProvider(); + var instantiatedVal = instanceProvider(); //make this debug-only? -(false, type)(); @@ -79,13 +580,431 @@ public T Read( this))(node, hookCtx, context, instanceProvider); } - return ((ReadGenericDelegate)_readGenericDelegates.GetOrAdd((typeof(T), node.GetType()!, notNullableOverride), - static (tuple, manager) => ReadDelegateValueFactory(tuple.value, tuple.value, tuple.node, tuple.notNullableOverride, manager), this))(node, hookCtx, context, instanceProvider); + var baseType = typeof(T); + if (baseType.IsEnum || baseType.IsArray || + (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(Nullable<>))) + { + return ((ReadGenericDelegate)_readGenericDelegates.GetOrAdd((typeof(T), node.GetType()!, notNullableOverride), + static (tuple, manager) => ReadDelegateValueFactory(tuple.value, tuple.value, tuple.node, tuple.notNullableOverride, manager), this))(node, hookCtx, context, instanceProvider); + } + var nullable = baseType.IsNullable(); + + T val = default!; + if (node.IsNull) + { + if (nullable && !notNullableOverride) + return default!; + + if (baseType == typeof(EntityUid)) + return (T) (object) EntityUid.Invalid; + + throw new NullNotAllowedException(); + } + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + // TODO actual type for type tag + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + } + } + else + { + RegularRead(); + } + + var isValueType = baseType.IsValueType; + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= GetOrCreateInstantiator(false); + val = instanceProvider(); + var selfSerialize = (ISelfSerialize?) val; + selfSerialize!.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + var definition = GetDefinition(typeof(T)); + if (definition == null) + throw new ArgumentException($"No data definition found for type {baseType} with node type {node.GetType()} when reading"); + + var valObj = instanceProvider == null ? definition.InstantiateObj() : instanceProvider.Invoke()!; + definition.PopulateObj(ref valObj, mapping, this, hookCtx, context); + val = (T) valObj; + + break; + } + case ValueDataNode value: + { + instanceProvider ??= GetOrCreateInstantiator(false); + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {baseType} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } + } + + public T ReadDefinition( + DataNode node, + SerializationHookContext hookCtx, + ISerializationContext? context = null, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) where T : ISerializationGenerated + { + if (node.Tag?.StartsWith("!type:") ?? false) + { + var type = ResolveConcreteType(typeof(T), node.Tag.Substring(6)); + if (type.IsInterface || type.IsAbstract) + { + throw new ArgumentException($"Interface or abstract type used for !type node. Type: {type}"); + } + + //!type tag overrides null value on default. i did this because i couldnt come up with a usecase where you'd specify the type but have a null value. yell at me if you found one -paul + if (node.IsEmpty || node.IsNull) + { + if (instanceProvider != null) + { + var instantiatedVal = instanceProvider(); + //make this debug-only? -(false, type)(); + } + + return ((ReadGenericDelegate)_readGenericBaseDelegates.GetOrAdd( + (typeof(T), type, node.GetType()!, notNullableOverride), + static (tuple, manager) => ReadDelegateValueFactory(tuple.baseType, tuple.actualType, tuple.node, tuple.notNullableOverride, manager), + this))(node, hookCtx, context, instanceProvider); + } + + var baseType = typeof(T); + var nullable = baseType.IsNullable(); + + T val = default!; + if (node.IsNull) + { + if (nullable && !notNullableOverride) + return default!; + + if (baseType == typeof(EntityUid)) + return (T) (object) EntityUid.Invalid; + + throw new NullNotAllowedException(); + } + + if (instanceProvider != null) + val = instanceProvider.Invoke(); + + if (context != null) + { + switch (node) + { + case MappingDataNode mapping: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, MappingDataNode>( + out var serializer)) + { + val = Read( + serializer, + mapping, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case SequenceDataNode sequence: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, SequenceDataNode>( + out var serializer)) + { + val = Read( + serializer, + sequence, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + case ValueDataNode value: + { + if (context.SerializerProvider + .TryGetTypeNodeSerializer, T, ValueDataNode>( + out var serializer)) + { + val = Read( + serializer, + value, + hookCtx, + context, + instanceProvider, + notNullableOverride + ); + } + else + { + RegularRead(); + } + + break; + } + } + } + else + { + RegularRead(); + } + + var isValueType = baseType.IsValueType; + if (!nullable && !isValueType && val == null) + throw new ReadCallReturnedNullException(); + + return val; + + void RegularRead() + { + var hasSerializer = false; + switch (node) + { + case MappingDataNode mapping: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, MappingDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, mapping, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case SequenceDataNode sequence: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, SequenceDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, sequence, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + case ValueDataNode value: + { + if (_regularSerializerProvider.TryGetTypeNodeSerializer, T, ValueDataNode>(out var reader)) + { + hasSerializer = true; + val = Read(reader, value, hookCtx, context, instanceProvider, notNullableOverride); + } + break; + } + } + + if (!hasSerializer) + { + if (baseType.IsAssignableTo(typeof(ISelfSerialize))) + { + switch (node) + { + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + var selfSerialize = (ISelfSerialize) val; + selfSerialize.Deserialize(value.Value); + val = (T) selfSerialize; + break; + } + default: + throw new InvalidNodeTypeException($"Cannot read {nameof(ISelfSerialize)} from node type {node.GetType()}. Expected {nameof(ValueDataNode)}"); + } + } + else + { + switch (node) + { + case MappingDataNode mapping: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + T.Read(ref val, mapping, this, hookCtx, context); + break; + } + case ValueDataNode value: + { + instanceProvider ??= T.StaticInstantiate; + val = instanceProvider(); + if (value.Value != string.Empty) + throw new ArgumentException($"No mapping node provided for type {baseType} at line: {node.Start.Line}"); + + break; + } + default: + throw new ArgumentException($"No mapping or value node provided for type {baseType}."); + } + + RunAfterHook(val, hookCtx); + } + } + } } - public T Read(ITypeReader reader, TNode node, ISerializationContext? context = null, - bool skipHook = false, ISerializationManager.InstantiationDelegate? instanceProvider = null, bool notNullableOverride = false) + public T Read( + ITypeReader reader, + TNode node, + ISerializationContext? context = null, + bool skipHook = false, + ISerializationManager.InstantiationDelegate? instanceProvider = null, + bool notNullableOverride = false) where TNode : DataNode { return Read( @@ -328,18 +1247,29 @@ Expression BaseInstantiatorToActual() } else if (nodeType == typeof(MappingDataNode)) { - var definition = manager.GetDefinition(actualType); - var definitionConst = Expression.Constant(definition, typeof(DataDefinition<>).MakeGenericType(actualType)); + if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) + { + var definition = manager.GetDefinition(actualType); + var definitionConst = Expression.Constant(definition, typeof(DataDefinition<>).MakeGenericType(actualType)); - call = Expression.Call( - managerConst, - nameof(ReadGenericMapping), - new[] { actualType }, - Expression.Convert(nodeParam, typeof(MappingDataNode)), - definitionConst, - hookCtxParam, - contextParam, - instantiatorVariable); + call = Expression.Call( + managerConst, + nameof(ReadGenericMapping), + new[] { actualType }, + Expression.Convert(nodeParam, typeof(MappingDataNode)), + definitionConst, + hookCtxParam, + contextParam, + instantiatorVariable); + } + else + { + call = Expression.Call( + managerConst, + nameof(ReadNoSerializer), + new[] { actualType }, + nodeParam); + } } else { @@ -508,20 +1438,23 @@ private TValue ReadGenericMapping( SerializationHookContext hookCtx, ISerializationContext? context, ISerializationManager.InstantiationDelegate instanceProvider) - where TValue : notnull + where TValue : ISerializationGenerated { if (definition == null) - { throw new ArgumentException($"No data definition found for type {typeof(TValue)} with node type {node.GetType()} when reading"); - } var instance = instanceProvider(); - definition.Populate(ref instance, node, hookCtx, context); + definition.Populate(ref instance, node, this, hookCtx, context); RunAfterHook(instance, hookCtx); return instance; } + + private TValue ReadNoSerializer(DataNode node) + { + throw new ArgumentException($"No type serializer or data definition found for type {typeof(TValue)} with node type {node.GetType()} when reading"); + } } } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs index f4430d182d4..c07b43d37e3 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs @@ -71,7 +71,10 @@ public bool TryGetCopierOrCreator(out ITypeCopier? copier, out ITy [Obsolete] public bool TryCustomCopy(T source, ref T target, SerializationHookContext hookCtx, bool hasHooks, ISerializationContext? context = null) { - if (TryGetCopierOrCreator(out var copier, out var copyCreator)) + if (target != null && source is ISerializationGenerated) + return false; + + if (TryGetCopierOrCreator(out var copier, out var copyCreator, context)) { if (copier != null) { @@ -142,6 +145,18 @@ public bool TryGetTypeNodeSerializer([NotNullWhen(true return true; } + internal bool TryGetTypeNodeSerializerArray([NotNullWhen(true)] out TInterface? serializer) + where TInterface : BaseSerializerInterfaces.ITypeNodeInterface + where TNode : DataNode + { + serializer = default; + if (!TryGetTypeNodeSerializer(typeof(TInterface).GetGenericTypeDefinition(), typeof(TType[]), typeof(TNode), out var rawSerializer)) + return false; + + serializer = (TInterface)rawSerializer; + return true; + } + public bool TryGetTypeNodeSerializer(Type interfaceType, Type objectType, Type nodeType, [NotNullWhen(true)] out object? serializer) { lock (_lock) @@ -234,23 +249,29 @@ internal bool TryGetCopierOrCreator(out ITypeCopier? copier, out I copyCreator = null; var information = SerializedType.Information; - if (information.Id >= _typeSerializersArray.Length) - return false; + if (information.Id < _typeSerializersArray.Length && + _typeSerializersArray[information.Id] is { } serializerArray) + { + var copiers = serializerArray[CopierIndex]; + var copyCreators = serializerArray[CopyCreatorIndex]; + copier = Unsafe.As?>(copiers.Regular); + copyCreator = Unsafe.As?>(copyCreators.Regular); - var serializerArray = _typeSerializersArray[information.Id]; - if (serializerArray == null) - return false; + if (copier != null || copyCreator != null) + return true; - var copiers = serializerArray[CopierIndex]; - var copyCreators = serializerArray[CopyCreatorIndex]; - copier = Unsafe.As?>(copiers.Regular); - copyCreator = Unsafe.As?>(copyCreators.Regular); + copier = Unsafe.As?>(copiers.Generic); + copyCreator = Unsafe.As?>(copyCreators.Generic); - if (copier != null || copyCreator != null) - return true; + if (copier != null || copyCreator != null) + return true; + } + + if (TryGetTypeSerializer(typeof(ITypeCopier<>), typeof(TType), out var rawCopier)) + copier = (ITypeCopier) rawCopier; - copier = Unsafe.As?>(copiers.Generic); - copyCreator = Unsafe.As?>(copyCreators.Generic); + if (TryGetTypeSerializer(typeof(ITypeCopyCreator<>), typeof(TType), out var rawCopyCreator)) + copyCreator = (ITypeCopyCreator) rawCopyCreator; return copier != null || copyCreator != null; } @@ -415,13 +436,17 @@ private void RegisterSerializerInterface(Type type) private void RegisterIndexedSerializer(Type elementType, int interfaceIndex, object serializer, bool regular) { var id = SerializedType.GetId(elementType); - if (id >= _typeSerializers.Count) + if (id >= _typeSerializersArray.Length) { Array.Resize(ref _typeSerializersArray, (id + 1) * 2); } - var array = new (object? Regular, object? Generic)[SerializerInterfaces.Length]; - _typeSerializersArray[id] = array; + var array = _typeSerializersArray[id]; + if (array == null) + { + array = new (object? Regular, object? Generic)[SerializerInterfaces.Length]; + _typeSerializersArray[id] = array; + } if (regular) { @@ -453,7 +478,7 @@ internal static int GetId(Type type) } } - private static class SerializedType + internal static class SerializedType { // ReSharper disable once StaticMemberInGenericType internal static readonly TypeInformation Information; @@ -467,7 +492,7 @@ static SerializedType() } } - private readonly struct TypeInformation + internal readonly struct TypeInformation { internal readonly int Id; internal readonly bool ReturnSource; diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs index b55f06c10c7..584769794fa 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Validation.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using System.Reflection; using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Markdown; using Robust.Shared.Serialization.Markdown.Mapping; @@ -21,119 +23,91 @@ public sealed partial class SerializationManager private readonly ConcurrentDictionary<(Type type, Type node), ValidationDelegate> _validationDelegates = new(); - private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type node) + private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type nodeType) { - return _validationDelegates.GetOrAdd((type, node), static (key, manager) => + return _validationDelegates.GetOrAdd((type, nodeType), static (key, manager) => { - var managerConst = Expression.Constant(manager); - var nodeParam = Expression.Parameter(typeof(DataNode), "node"); - var contextParam = Expression.Parameter(typeof(ISerializationContext), "context"); - - Expression call; - if (manager._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), key.type, key.node, out var serializer)) - { - var serializerConst = Expression.Constant(serializer); - - call = Expression.Call( - managerConst, - nameof(ValidateNode), - new []{key.type, key.node}, - serializerConst, - Expression.Convert(nodeParam, key.node), - contextParam); - } - else if (key.type.IsArray) - { - if (!key.node.IsAssignableTo(typeof(SequenceDataNode))) - { - call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for array.", true); - } - else - { - var elementType = key.type.GetElementType(); - if (elementType == null) - throw new ArgumentException($"Failed to get ElementType of ArrayType {key.type}"); - - call = Expression.Call( - managerConst, - nameof(ValidateArray), - new[] { elementType }, - Expression.Convert(nodeParam, typeof(SequenceDataNode)), - contextParam); - } - } - else if (key.type.IsEnum) - { - // Does not include cases where the target type is System.Enum. - // Those get handled by the generic enum serializer which uses reflection to resolve strings into enums. - DebugTools.Assert(key.type != typeof(Enum)); - - call = Expression.Call( - managerConst, - nameof(ValidateEnum), - new[] { key.type }, - nodeParam); - } - else if (key.type.IsAssignableTo(typeof(ISelfSerialize))) - { - if (key.node.IsAssignableTo(typeof(ValueDataNode))) - { - call = manager.ValidateNodeExpression(nodeParam); - } - else - { - call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for ISelfSerialize"); - } - } - else if (manager.TryGetDefinition(key.type, out var dataDefinition)) + var validate = Validate(manager, key.type, key.node); + return (node, context) => { - var dataDefConst = Expression.Constant(dataDefinition, typeof(DataDefinition<>).MakeGenericType(key.type)); - - call = Expression.Call( - managerConst, - nameof(ValidateDataDefinition), - new []{key.type}, - nodeParam, - dataDefConst, - contextParam); - } - else - { - call = Expression.Call( - managerConst, - nameof(ValidateGenericValue), - new[] { key.type, key.node }, - nodeParam, - contextParam); - } + if (!IsNull(node)) + return validate(node, context); + + if (key.type.IsNullable()) + return new ValidatedValueNode(node); - //insert a nullcheck at the beginning, but ONLY if we are actually found a way of validating this node - call = Expression.Condition( - Expression.Call( - typeof(SerializationManager), - nameof(IsNull), - Type.EmptyTypes, - nodeParam), - Expression.Convert(key.type.IsNullable() - ? manager.ValidateNodeExpression(nodeParam) - : manager.ErrorNodeExpression(nodeParam, "Non-nullable field contained a null value", true), typeof(ValidationNode)), - Expression.Convert(call, typeof(ValidationNode))); - - return Expression.Lambda( - call, - nodeParam, - contextParam).Compile(); + return new ErrorNode(node, "Non-nullable field contained a null value"); + }; }, this); } - private Expression ErrorNodeExpression(ParameterExpression nodeParam, string message, bool alwaysRelevant = true) + private static Func Validate(SerializationManager serialization, Type type, Type nodeType) { - return ExpressionUtils.NewExpression(nodeParam, message, alwaysRelevant); - } + if (serialization._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), + type, + nodeType, + out var serializer)) + { + var method = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.Public) + .First(m => m.Name == nameof(ValidateNode) && m.GetGenericArguments().Length == 2) + .MakeGenericMethod(type, nodeType); + return (node, context) => (ValidationNode) method.Invoke(serialization, [serializer, node, context])!; + } - private Expression ValidateNodeExpression(ParameterExpression nodeParam) - { - return ExpressionUtils.NewExpression(nodeParam); + if (type.IsArray) + { + if (!nodeType.IsAssignableTo(typeof(SequenceDataNode))) + return (node, _) => new ErrorNode(node, "Invalid nodetype for array."); + + var elementType = type.GetElementType(); + if (elementType == null) + throw new ArgumentException($"Failed to get ElementType of ArrayType {type}"); + + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateArray), + BindingFlags.Instance | BindingFlags.NonPublic, + [typeof(SequenceDataNode), typeof(ISerializationContext)])! + .MakeGenericMethod(elementType); + return (node, context) => (ValidationNode) method.Invoke(serialization, [node, context])!; + } + + if (type.IsEnum) + { + DebugTools.Assert(type != typeof(Enum)); + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateEnum), BindingFlags.Instance | BindingFlags.NonPublic, [typeof(DataNode)])! + .MakeGenericMethod(type); + return (node, _) => (ValidationNode)method.Invoke(serialization, [node])!; + } + + if (type.IsAssignableTo(typeof(ISelfSerialize))) + { + if (nodeType.IsAssignableTo(typeof(ValueDataNode))) + return (node, _) => new ValidatedValueNode(node); + + return (node, _) => new ErrorNode(node, "Invalid nodetype for ISelfSerialize"); + } + + if (serialization.TryGetDefinition(type, out var dataDefinition)) + { + var method = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .First(m => m.Name == nameof(ValidateDataDefinition)) + .MakeGenericMethod(type); + return (node, context) => (ValidationNode)method.Invoke(serialization, [node, dataDefinition, context])!; + } + else + { + var method = typeof(SerializationManager) + .GetMethod(nameof(ValidateGenericValue), + BindingFlags.Instance | BindingFlags.NonPublic, + [ + typeof(DataNode), typeof(ISerializationContext) + ])! + .MakeGenericMethod(type, nodeType); + return (node, context) => (ValidationNode)method.Invoke(serialization, [node, context])!; + } } private ValidationNode ValidateArray(SequenceDataNode sequenceDataNode, ISerializationContext? context) @@ -170,7 +144,7 @@ private ValidationNode ValidateEnum(DataNode node) return new ValidatedValueNode(node); } - private ValidationNode ValidateDataDefinition(DataNode node, DataDefinition dataDefinition, ISerializationContext? context) where T : notnull + private ValidationNode ValidateDataDefinition(DataNode node, DataDefinition dataDefinition, ISerializationContext? context) where T : notnull, ISerializationGenerated { return node switch { @@ -231,11 +205,7 @@ public ValidationNode ValidateNode(Type type, DataNode node, ISerializationConte if (node.Tag?.StartsWith("!type:") == true) { var typeString = node.Tag.Substring(6); - try - { - underlyingType = ResolveConcreteType(underlyingType, typeString); - } - catch (InvalidOperationException) + if (!TryResolveConcreteType(underlyingType, typeString, out underlyingType)) { return new ErrorNode(node, $"Failed to resolve !type tag: {typeString}", false); } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs index 3bc5109dc28..c4f95c00fbf 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.Writing.cs @@ -6,6 +6,7 @@ using Robust.Shared.Serialization.Manager.Definition; using Robust.Shared.Serialization.Manager.Exceptions; using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Serialization.TypeSerializers.Interfaces; @@ -146,7 +147,7 @@ static object ValueFactory(Type baseType, Type actualType, bool notNullableOverr Type.EmptyTypes, Expression.Convert(objParam, typeof(ISelfSerialize))); } - else + else if (actualType.IsAssignableTo(typeof(ISerializationGenerated<>).MakeGenericType(actualType))) { call = Expression.Call( instanceParam, @@ -167,6 +168,14 @@ static object ValueFactory(Type baseType, Type actualType, bool notNullableOverr nodeVar); } } + else + { + call = Expression.Call( + instanceParam, + nameof(WriteNoSerializer), + Type.EmptyTypes, + Expression.Constant(actualType, typeof(Type))); + } // check for customtypeserializer before anything var serializerType = typeof(ITypeWriter<>).MakeGenericType(actualType); @@ -221,6 +230,11 @@ private DataNode WriteSelfSerializable(ISelfSerialize obj) return new ValueDataNode(obj.Serialize()); } + private DataNode WriteNoSerializer(Type type) + { + throw new ArgumentException($"No type serializer or data definition found for type {type} when writing"); + } + private DataNode WriteArray(TElement[] obj, bool alwaysWrite, ISerializationContext? context) { var sequenceNode = new SequenceDataNode(); @@ -239,13 +253,14 @@ private DataNode WriteValueInternal( DataDefinition? definition, bool alwaysWrite, ISerializationContext? context) - where T : notnull + where T : ISerializationGenerated { //this check is in here on purpose. we cannot check this during expression tree generation due to the value maybe being handled by a custom typeserializer if(definition == null) throw new InvalidOperationException($"No data definition found for type {typeof(T)} when writing"); - var mapping = definition.Serialize(value, context, alwaysWrite); + var mapping = new MappingDataNode(); + definition.Serialize(value, mapping, this, context, alwaysWrite, definition.DefaultValuesDict); return mapping; } diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs index be2253cb9cc..5d83e594393 100644 --- a/Robust.Shared/Serialization/Manager/SerializationManager.cs +++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs @@ -9,17 +9,20 @@ using System.Threading.Tasks; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Definition; +using Robust.Shared.Serialization.Markdown; using Robust.Shared.Utility; namespace Robust.Shared.Serialization.Manager { public sealed partial class SerializationManager : ISerializationManager { - [Dependency] private IReflectionManager _reflectionManager = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IReflectionManager _reflectionManager = default!; public IReflectionManager ReflectionManager => _reflectionManager; @@ -33,10 +36,10 @@ public sealed partial class SerializationManager : ISerializationManager // Always has a dummy value of 0 for any types that should be copied by ref private readonly ConcurrentDictionary _copyByRefRegistrations = new(); - [IoC.Dependency] - private IDependencyCollection _dependencyCollection = null!; + [field: Dependency] + public IDependencyCollection DependencyCollection { get; } = default!; - public IDependencyCollection DependencyCollection => _dependencyCollection; + public bool IsServer { get; private set; } public void Initialize() { @@ -46,8 +49,19 @@ public void Initialize() if (_initialized) throw new InvalidOperationException($"{nameof(SerializationManager)} has already been initialized."); + IsServer = _net.IsServer; _initializing = true; + _read = typeof(SerializationManager) + .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) + .First(m => m.Name == nameof(ReadObject) && + m.GetGenericArguments().Length == 1 && + GetParametersBase(m) + .SequenceEqual([ + typeof(DataNode), typeof(SerializationHookContext), typeof(ISerializationContext), + typeof(bool) + ])); + var flagsTypes = new ConcurrentBag(); var constantsTypes = new ConcurrentBag(); var typeSerializers = new ConcurrentBag(); @@ -142,10 +156,7 @@ IEnumerable GetImplicitTypes(Type type) foreach (var (type, definition) in _dataDefinitions) { var invalidTypes = new List(); - foreach (var includedField in definition.BaseFieldDefinitions.Where(x => x.Attribute is IncludeDataFieldAttribute - { - CustomTypeSerializer: null - })) + foreach (var includedField in definition.BaseFieldDefinitions.Where(x => x is { IsIncludeDataField: true, CustomTypeSerializer: null })) { if (!dataDefs.Contains(includedField.FieldType)) { @@ -196,7 +207,7 @@ IEnumerable GetImplicitTypes(Type type) if (field.FieldType.ContainsGenericParameters) continue; // This just isn't supported yet, can't validate it so just skip it. - if (field.Attribute.CustomTypeSerializer != null) + if (field.CustomTypeSerializer != null) continue; // Assume that anything with a custom type serializer can be handled. if (!ValidateIsSerializable(field.FieldType, forbidden)) @@ -280,9 +291,11 @@ private void CollectAttributedTypes( private DataDefinition CreateDataDefinition(Type t, bool isRecord) { return (DataDefinition)typeof(DataDefinition<>).MakeGenericType(t) - .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, new[] - { typeof(SerializationManager), typeof(bool) })! - .Invoke(new object[]{this, isRecord}); + .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, + [ + typeof(SerializationManager), typeof(bool) + ])! + .Invoke([this, isRecord]); } public void Shutdown() @@ -301,19 +314,17 @@ public void Shutdown() _initialized = false; } - internal DataDefinition? GetDefinition() where T : notnull + internal DataDefinition? GetDefinition() where T : ISerializationGenerated { return GetDefinition(typeof(T)) as DataDefinition; } internal DataDefinition? GetDefinition(Type type) { - return _dataDefinitions.TryGetValue(type, out var dataDefinition) - ? dataDefinition - : null; + return _dataDefinitions.GetValueOrDefault(type); } - internal bool TryGetDefinition([NotNullWhen(true)] out DataDefinition? dataDefinition) where T : notnull + internal bool TryGetDefinition([NotNullWhen(true)] out DataDefinition? dataDefinition) where T : ISerializationGenerated { dataDefinition = GetDefinition(); return dataDefinition != null; @@ -332,28 +343,30 @@ public bool TryGetVariableType(Type type, string variableName, [NotNullWhen(true variableType = null; return false; } - var foundFieldDef = definition.BaseFieldDefinitions.FirstOrDefault(fieldDef => fieldDef?.Attribute is DataFieldAttribute attr && attr.Tag==variableName, null); - if(foundFieldDef != null) + + var foundFieldDef = definition.BaseFieldDefinitions.FirstOrDefault(fieldDef => fieldDef.IsDataField && fieldDef.Tag == variableName, default); + if (foundFieldDef != default) { - variableType = foundFieldDef.BackingField.FieldType; + variableType = foundFieldDef.FieldType; return true; } - else - { - variableType = null; - return false; - } + + variableType = null; + return false; + } + + private bool TryResolveConcreteType(Type baseType, string typeName, [NotNullWhen(true)] out Type? concreteType) + { + concreteType = ReflectionManager.YamlTypeTagLookup(baseType, typeName); + return concreteType != null; } private Type ResolveConcreteType(Type baseType, string typeName) { - var type = ReflectionManager.YamlTypeTagLookup(baseType, typeName); - if (type == null) - { - throw new InvalidOperationException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); - } + if (TryResolveConcreteType(baseType, typeName, out var concreteType)) + return concreteType; - return type; + throw new InvalidOperationException($"Type '{baseType}' is abstract, but could not find concrete type '{typeName}'."); } #pragma warning disable CS0618 @@ -375,6 +388,15 @@ private static void RunAfterHookGenerated(TValue instance, Serialization else instance.AfterDeserialization(); } + + private static IEnumerable GetParametersBase(MethodInfo method) + { + return method.GetParameters() + .Select(p => + p.ParameterType.IsGenericType + ? p.ParameterType.GetGenericTypeDefinition() + : p.ParameterType); + } #pragma warning restore CS0618 } } diff --git a/Robust.Shared/Utility/InternalReflectionUtils.cs b/Robust.Shared/Utility/InternalReflectionUtils.cs index 10f3e609681..920c5a20769 100644 --- a/Robust.Shared/Utility/InternalReflectionUtils.cs +++ b/Robust.Shared/Utility/InternalReflectionUtils.cs @@ -37,46 +37,6 @@ private static void EmitSetField(ILGenerator rGenerator, AbstractFieldInfo info) } } - internal static object EmitFieldAccessor(Type obj, FieldDefinition fieldDefinition) - { - if (fieldDefinition.BackingField is SpecificFieldInfo fieldInfo) - return fieldInfo.FieldInfo; - - if (fieldDefinition.BackingField is SpecificPropertyInfo propertyInfo) - return propertyInfo.PropertyInfo.GetGetMethod(true) ?? throw new InvalidOperationException("Property has no getter"); - - var method = new DynamicMethod( - "AccessField", - fieldDefinition.BackingField.FieldType, - new[] {obj.MakeByRefType()}, - true); - - method.DefineParameter(1, ParameterAttributes.Out, "target"); - - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); - - if(!obj.IsValueType) - generator.Emit(OpCodes.Ldind_Ref); - - switch (fieldDefinition.BackingField) - { - case SpecificFieldInfo field: - generator.Emit(OpCodes.Ldfld, field.FieldInfo); - break; - case SpecificPropertyInfo property: - var getter = property.PropertyInfo.GetGetMethod(true) ?? throw new NullReferenceException(); - var opCode = fieldDefinition.BackingField.FieldType.IsValueType ? OpCodes.Call : OpCodes.Callvirt; - generator.Emit(opCode, getter); - break; - } - - generator.Emit(OpCodes.Ret); - - return method.CreateDelegate(typeof(AccessField<,>).MakeGenericType(obj, fieldDefinition.BackingField.FieldType)); - } - internal static object EmitFieldAssigner(Type objType, AbstractFieldInfo backingField, bool boxing = false) { if (!boxing) From 960edb32c4dd417496e4667177625d8c3cb14f7e Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Wed, 8 Jul 2026 22:23:10 +1000 Subject: [PATCH 136/178] Version: 282.0.0 --- MSBuild/Robust.Engine.Version.props | 8 ++++---- RELEASE-NOTES.md | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 96f24dffb61..699ce228c59 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - - - 281.0.0 - + + + 282.0.0 + diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 922d8f07188..6aaa3abdf94 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -54,6 +54,17 @@ END TEMPLATE--> *None yet* +## 282.0.0 + +### Breaking changes + +* Serv5 has been merged, re-doing the internals of DataDefinition serialization. + * It can now write into readonly fields. + * DataFields defined on objects that don't have DataDefinitions will cause errors in the analyzer and require the attribute. + * Value types will now be copied directly where possible rather than round-tripping through TryCustomCopy if no custom serializer is specified. + * It no longer uses as many expression trees so more tests should be able to run concurrently + + ## 281.0.0 ### Breaking changes From c2d0490a9ef7492ca2bd17feb407d4334f1b6b0e Mon Sep 17 00:00:00 2001 From: Rouden <149893554+Roudenn@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:26:21 +0300 Subject: [PATCH 137/178] Add `EntitySystem.TryComp(EntityUid, Type, out IComponent?)` proxy method (#6746) --- .../GameObjects/EntitySystem.Proxy.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs index 9c4c055044d..8a8c7d380f6 100644 --- a/Robust.Shared/GameObjects/EntitySystem.Proxy.cs +++ b/Robust.Shared/GameObjects/EntitySystem.Proxy.cs @@ -495,6 +495,14 @@ protected bool TryComp(EntityUid uid, [NotNullWhen(true)] out T? comp) where return EntityManager.TryGetComponent(uid, out comp); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ProxyFor(typeof(EntityManager), nameof(EntityManager.TryGetComponent))] + protected bool TryComp(EntityUid uid, Type type, [NotNullWhen(true)] out IComponent? comp) + { + return EntityManager.TryGetComponent(uid, type, out comp); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected bool TryComp(EntityUid uid, [NotNullWhen(true)] out TransformComponent? comp) @@ -523,6 +531,20 @@ protected bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true) return EntityManager.TryGetComponent(uid.Value, out comp); } + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [ProxyFor(typeof(EntityManager), nameof(EntityManager.TryGetComponent))] + protected bool TryComp([NotNullWhen(true)] EntityUid? uid, Type type, [NotNullWhen(true)] out IComponent? comp) + { + if (!uid.HasValue) + { + comp = null; + return false; + } + + return EntityManager.TryGetComponent(uid.Value, type, out comp); + } + /// protected bool TryComp([NotNullWhen(true)] EntityUid? uid, [NotNullWhen(true)] out TransformComponent? comp) { From dcadd34389dd025d5e0b6b93f2dbdcf09f30d0d1 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 9 Jul 2026 00:08:10 +1000 Subject: [PATCH 138/178] Serv5 fixes (#6761) --- Robust.Serialization.Generator/Generator.cs | 30 +++++++++---------- Robust.Serialization.Generator/Types.cs | 6 ++++ Robust.Shared/Prototypes/ProtoId.cs | 2 ++ .../Custom/TimeOffsetSerializer.cs | 12 +++++++- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index b94f70dc6b8..ec9c3f9e7d0 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -1140,21 +1140,7 @@ private static StringBuilder GetCopyBody(DataDefinition definition, string targe var nullableValue = isNullableValueType ? ".Value" : string.Empty; var nullNotAllowed = isClass && !isNullable; - if (CanBeCopiedByValue(field.Symbol, field.Type)) - { - if (nullNotAllowed) - { - builder.AppendLine($$""" - if (source.{{name}} == null) - { - throw new NullNotAllowedException(); - } - """); - } - - builder.AppendLine($"{targetName} = source.{name};"); - } - else if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType } && + if (field.CustomSerializer is { Serializer: var serializer, Type: var serializerType } && ((serializerType & Copier) != 0 || (serializerType & CopyCreator) != 0)) { if (nullNotAllowed) @@ -1201,6 +1187,20 @@ private static StringBuilder GetCopyBody(DataDefinition definition, string targe if (isNullable || isNullableValueType) builder.AppendLine("}"); } + else if (CanBeCopiedByValue(field.Symbol, field.Type)) + { + if (nullNotAllowed) + { + builder.AppendLine($$""" + if (source.{{name}} == null) + { + throw new NullNotAllowedException(); + } + """); + } + + builder.AppendLine($"{targetName} = source.{name};"); + } else { if (nullNotAllowed) diff --git a/Robust.Serialization.Generator/Types.cs b/Robust.Serialization.Generator/Types.cs index 7e7b782a26a..e029835a333 100644 --- a/Robust.Serialization.Generator/Types.cs +++ b/Robust.Serialization.Generator/Types.cs @@ -78,6 +78,12 @@ private static bool CanTypeBeCopiedByValue(ITypeSymbol type, HashSet vis if (HasAttribute(type, CopyByRefNamespace)) return true; + if (type is INamedTypeSymbol named && + HasAttribute(named.OriginalDefinition, CopyByRefNamespace)) + { + return true; + } + if (type.TypeKind == TypeKind.Enum) return true; diff --git a/Robust.Shared/Prototypes/ProtoId.cs b/Robust.Shared/Prototypes/ProtoId.cs index 8d69ad02b17..1b7fb85a17f 100644 --- a/Robust.Shared/Prototypes/ProtoId.cs +++ b/Robust.Shared/Prototypes/ProtoId.cs @@ -1,4 +1,5 @@ using System; +using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; using Robust.Shared.Toolshed.TypeParsers; @@ -14,6 +15,7 @@ namespace Robust.Shared.Prototypes; /// /// for an alias. [Serializable] +[CopyByRef] [PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))] public readonly record struct ProtoId(string Id) : IEquatable, diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs index 0c8c4ac2ce8..63394f82568 100644 --- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs +++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/TimeOffsetSerializer.cs @@ -21,7 +21,7 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; /// to prevent time-offsets from being unintentionally saved to maps while mapping. If an entity must have an initial /// non-zero time, then that time should just be configured during map-init. /// -public sealed class TimeOffsetSerializer : ITypeSerializer +public sealed class TimeOffsetSerializer : ITypeSerializer, ITypeCopyCreator { public TimeSpan Read(ISerializationManager serializationManager, ValueDataNode node, IDependencyCollection dependencies, @@ -81,4 +81,14 @@ public DataNode Write( return new ValueDataNode(value.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } + + public TimeSpan CreateCopy( + ISerializationManager serializationManager, + TimeSpan source, + IDependencyCollection dependencies, + SerializationHookContext hookCtx, + ISerializationContext? context = null) + { + return source; + } } From eb25a0380879ef8ee64e24982803e17f4b69cdd3 Mon Sep 17 00:00:00 2001 From: mqole Date: Thu, 9 Jul 2026 20:47:54 +1000 Subject: [PATCH 139/178] fuckit one more --- .../Utility/ColorExtensionsTest.cs | 16 ++++++++++++++++ Robust.Shared/Utility/ColorExtensions.cs | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs index ca6bae9d0bb..0896f2d3078 100644 --- a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -9,6 +9,22 @@ namespace Robust.Shared.Tests.Utility; [TestOf(typeof(ColorExtensions))] internal sealed class ColorExtensionsTest { + [Test] + public void TestAnalogousPalette() + { + var palette = ColorExtensions.GetAnalogousComplementaries(Color.Red); + + using (Assert.EnterMultipleScope()) + { + Assert.That(palette, Has.Length.EqualTo(3)); + Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); + + Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.AnalogousHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.AnalogousHueDelta)); + } + } + [Test] public void TestTriadicPalette() { diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index 50840f9ca55..8e19eed5ca4 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -7,10 +7,19 @@ namespace Robust.Shared.Utility; public static class ColorExtensions { + public static readonly float AnalogousHueDelta = 45f / 360f; // +/- 1/8. 45 degrees, 0.125 over hue public static readonly float TriadicHueDelta = 120f / 360f; // +/- 1/3. 120 degrees, 0.333 over hue public static readonly float SplitComplementaryHueDelta = 150f / 360f; // +/- 5/12. 150 degrees, 0.4166... over hue public static readonly float ComplementaryHueDelta = 180f / 360f; // +/- 1/2. 180 degrees + /// + /// Generates a list of analogous complementary colors + /// + public static Color[] GetAnalogousComplementaries(this Color color) + { + return GetComplementaryColors(color, TriadicHueDelta); + } + /// /// Generates a list of triadic complementary colors /// From 4d1e393735f9029ef8600b2d18291786c6fb2d99 Mon Sep 17 00:00:00 2001 From: mqole Date: Thu, 9 Jul 2026 22:39:30 +1000 Subject: [PATCH 140/178] OOPS --- Robust.Shared/Utility/ColorExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index 8e19eed5ca4..38d254ed05c 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -17,7 +17,7 @@ public static class ColorExtensions /// public static Color[] GetAnalogousComplementaries(this Color color) { - return GetComplementaryColors(color, TriadicHueDelta); + return GetComplementaryColors(color, AnalogousHueDelta); } /// From 7ffe5fc58c73ffc9f95b9022e84775c20f677a37 Mon Sep 17 00:00:00 2001 From: mqole Date: Fri, 10 Jul 2026 01:25:22 +1000 Subject: [PATCH 141/178] yeah ok --- Robust.Shared/Utility/ColorExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Utility/ColorExtensions.cs b/Robust.Shared/Utility/ColorExtensions.cs index 38d254ed05c..007914540c9 100644 --- a/Robust.Shared/Utility/ColorExtensions.cs +++ b/Robust.Shared/Utility/ColorExtensions.cs @@ -7,7 +7,7 @@ namespace Robust.Shared.Utility; public static class ColorExtensions { - public static readonly float AnalogousHueDelta = 45f / 360f; // +/- 1/8. 45 degrees, 0.125 over hue + public static readonly float AnalogousHueDelta = 30f / 360f; // +/- 1/12. 30 degrees, 0.08333... over hue public static readonly float TriadicHueDelta = 120f / 360f; // +/- 1/3. 120 degrees, 0.333 over hue public static readonly float SplitComplementaryHueDelta = 150f / 360f; // +/- 5/12. 150 degrees, 0.4166... over hue public static readonly float ComplementaryHueDelta = 180f / 360f; // +/- 1/2. 180 degrees From 167a93a94de0fb6da38b00c76f61700c3365c34e Mon Sep 17 00:00:00 2001 From: mqole Date: Fri, 10 Jul 2026 12:00:36 +1000 Subject: [PATCH 142/178] test edits --- .../Utility/ColorExtensionsTest.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs index 0896f2d3078..2ae74f3157b 100644 --- a/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs +++ b/Robust.Shared.Tests/Utility/ColorExtensionsTest.cs @@ -4,7 +4,6 @@ namespace Robust.Shared.Tests.Utility; -[TestFixture] [Parallelizable(ParallelScope.All)] [TestOf(typeof(ColorExtensions))] internal sealed class ColorExtensionsTest @@ -20,8 +19,8 @@ public void TestAnalogousPalette() Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.AnalogousHueDelta)); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.AnalogousHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 30f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 30f / 360f)); } } @@ -36,8 +35,8 @@ public void TestTriadicPalette() Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.TriadicHueDelta)); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.TriadicHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 120f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 120f / 360f)); } } @@ -52,8 +51,8 @@ public void TestSplitComplementaryPalette() Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.SplitComplementaryHueDelta)); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.SplitComplementaryHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 150f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 150f / 360f)); } } @@ -68,8 +67,8 @@ public void TestComplementaryPalette() Assert.That(MathHelper.CloseToPercent(palette[0], Color.Red)); Assert.That(Color.ToHsl(palette[0]).X, Is.Zero); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, ColorExtensions.ComplementaryHueDelta)); - Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - ColorExtensions.ComplementaryHueDelta)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[1]).X, 180f / 360f)); + Assert.That(MathHelper.CloseToPercent(Color.ToHsl(palette[2]).X, 1f - 180f / 360f)); } } } From b0b1fa354a64ba8725608e72e2bef36621b70f2a Mon Sep 17 00:00:00 2001 From: Pok <113675512+Pok27@users.noreply.github.com> Date: Sat, 11 Jul 2026 00:13:05 +0300 Subject: [PATCH 143/178] FovRenderTarget (#6781) --- Robust.Client/Graphics/Clyde/Clyde.Viewport.cs | 1 + Robust.Client/Graphics/Clyde/ClydeHeadless.cs | 3 +++ Robust.Client/Graphics/IClydeViewport.cs | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs index efbc3451502..a474f28bfbf 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Viewport.cs @@ -252,6 +252,7 @@ public void FireClear() IRenderTexture IClydeViewport.RenderTarget => RenderTarget; IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget; + IRenderTexture IClydeViewport.FovRenderTarget => _clyde._fovRenderTarget; public IEye? Eye { get; set; } } diff --git a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs index 093527e6b99..c8c708c5d6c 100644 --- a/Robust.Client/Graphics/Clyde/ClydeHeadless.cs +++ b/Robust.Client/Graphics/Clyde/ClydeHeadless.cs @@ -522,6 +522,9 @@ public void Dispose() public IRenderTexture LightRenderTarget { get; } = new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One)); + public IRenderTexture FovRenderTarget { get; } = + new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One)); + public IEye? Eye { get; set; } public Vector2i Size { get; } public event Action? ClearCachedResources; diff --git a/Robust.Client/Graphics/IClydeViewport.cs b/Robust.Client/Graphics/IClydeViewport.cs index e4001978298..0e5525965b8 100644 --- a/Robust.Client/Graphics/IClydeViewport.cs +++ b/Robust.Client/Graphics/IClydeViewport.cs @@ -25,6 +25,13 @@ public interface IClydeViewport : IDisposable IRenderTexture RenderTarget { get; } IRenderTexture LightRenderTarget { get; } + /// + /// The render target holding the FOV shadow-depth map for this viewport's eye. + /// Can be sampled by world-space shaders (together with the eye position) to + /// determine per-pixel whether a world point is occluded by FOV. + /// + IRenderTexture FovRenderTarget { get; } + IEye? Eye { get; set; } Vector2i Size { get; } From 5a83ab127e636c23f254c551794ec8749e61a6c0 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:20:48 +1000 Subject: [PATCH 144/178] Try to make connection failure msg useful (#6743) Right now we just get an aggregateexception of IPV4 + IPV6 and roll the dice on which exception to use. The issue is if we get a generic "no response from remote host" because no IPV6 for example it can bulldoze an actually useful exception log. I would wager 90% of the time this is why connection messages aren't being useful because IPV6 is failing fast and being picked for the first exception but I could also be wrong. --- .../Network/NetManager.ClientConnect.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Network/NetManager.ClientConnect.cs b/Robust.Shared/Network/NetManager.ClientConnect.cs index 4f671d6d681..509322be625 100644 --- a/Robust.Shared/Network/NetManager.ClientConnect.cs +++ b/Robust.Shared/Network/NetManager.ClientConnect.cs @@ -323,7 +323,7 @@ async Task AttemptConnection(IPAddress address, CancellationT // Connection failed, clean up and yeet an exception peer.Shutdown(reason); _toCleanNetPeers.Add(peer); - throw new Exception($"Connection failed: {reason}"); + throw new ConnectionFailedException(reason); } return new ConnectionAttempt(peerData, connection, this); @@ -377,13 +377,32 @@ async Task AwaitNonInitStatusChange(NetConnection connection, Cancellati catch (AggregateException ae) { // ParallelTask throws AggregateException with all connection failures - // We just take the first one - var message = ae.InnerExceptions.First().Message; + // We'll try to take the most useful one. + var message = SelectConnectionFailureMessage(ae); OnConnectFailed(message); return null; } } + private static string SelectConnectionFailureMessage(AggregateException aggregateException) + { + var connectionFailures = aggregateException.InnerExceptions + .OfType() + .ToArray(); + + if (connectionFailures.Length == 0) + return aggregateException.InnerExceptions.First().Message; + + return connectionFailures.FirstOrDefault(e => !e.IsNoResponseFromRemoteHost)?.Message + ?? connectionFailures[0].Message; + } + + private sealed class ConnectionFailedException(string reason) : Exception($"Connection failed: {reason}") + { + public bool IsNoResponseFromRemoteHost { get; } = + reason.Contains("no response from remote host", StringComparison.OrdinalIgnoreCase); + } + private Task AwaitStatusChange(NetConnection connection, CancellationToken cancellationToken = default) { if (_awaitingStatusChange.ContainsKey(connection)) From 2e07cfcbdd93630663c3dca811e8454ced89765a Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Sat, 11 Jul 2026 10:21:30 -0400 Subject: [PATCH 145/178] Add TestPair ID to GetPair logging (#6780) If a test pair dies during cleanup, this saves a bit of hassle trying to figure out which pair had the problem without having to dig through the gravestones. --- Robust.UnitTesting/Pool/PoolManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.UnitTesting/Pool/PoolManager.cs b/Robust.UnitTesting/Pool/PoolManager.cs index 9b31a41c66a..4b756bdefa0 100644 --- a/Robust.UnitTesting/Pool/PoolManager.cs +++ b/Robust.UnitTesting/Pool/PoolManager.cs @@ -180,7 +180,7 @@ await testOut.WriteLineAsync( if (pair != null) { pair.ActivateContext(testOut); - await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found"); + await testOut.WriteLineAsync($"{nameof(GetPair)}: Suitable pair found (Pair {pair.Id})"); if (pair.Settings.CanFastRecycle(settings)) { From 72d56f37d51328377e90032be20d4d60b02b0349 Mon Sep 17 00:00:00 2001 From: TemporalOroboros Date: Sat, 11 Jul 2026 07:21:52 -0700 Subject: [PATCH 146/178] Remove unused GridEventHandler delegate (#6777) Remove unused event --- Robust.Shared/Map/GridEventHandler.cs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 Robust.Shared/Map/GridEventHandler.cs diff --git a/Robust.Shared/Map/GridEventHandler.cs b/Robust.Shared/Map/GridEventHandler.cs deleted file mode 100644 index d0f6f374755..00000000000 --- a/Robust.Shared/Map/GridEventHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameObjects; - -namespace Robust.Shared.Map -{ - /// - /// Invoked when a grid is altered. - /// - /// Passed to the delegate given it may no longer be retrievable. - /// The index of the grid being changed. - public delegate void GridEventHandler(MapId mapId, EntityUid gridId); -} From 89a054696b1e7384325384badcd92868cec45cd4 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:32:53 -0700 Subject: [PATCH 147/178] Move EventBus benchmarks to the Engine. (#6778) * Foolish engine benchmark, I laced yo shit * Ok it works now. --- .../EntityManager/RaiseEventBenchmark.cs | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs diff --git a/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs b/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs new file mode 100644 index 00000000000..847e28f1dd1 --- /dev/null +++ b/Robust.Benchmarks/EntityManager/RaiseEventBenchmark.cs @@ -0,0 +1,318 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Robust.Shared; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.UnitTesting.Server; + +namespace Robust.Benchmarks.EntityManager; + +[Virtual] +public partial class RaiseEventBenchmark +{ + private ISimulation _simulation = default!; + private IEntityManager _entityManager = default!; + private BenchSystem _sys = default!; + + [GlobalSetup] + public async Task Setup() + { + ProgramShared.PathOffset = "../../../../"; + _simulation = RobustServerSimulation + .NewSimulation() + .RegisterComponents(f => + { + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + f.RegisterClass(); + }) + .RegisterEntitySystems(f => f.LoadExtraSystemType()) + .InitializeInstance(); + + _entityManager = _simulation.Resolve(); + var bus = (EntityEventBus)_entityManager.EventBus; + _entityManager.EntitySysManager.Resolve(ref _sys); + + var uid = _entityManager.Spawn(); + _sys.Ent = new(uid, _entityManager.GetComponent(uid)); + _sys.Ent2 = new(_sys.Ent.Owner, _sys.Ent.Comp); + _sys.NetId = _entityManager.ComponentFactory.GetRegistration().NetID!.Value; + _sys.EvSubs = bus.GetNetCompEventHandlers(); + + var id2 = _entityManager.Spawn(); + _entityManager.AddComponent(id2); + _entityManager.AddComponent(id2); + _sys.Ent2Comps = id2; + + var id4 = _entityManager.Spawn(); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _entityManager.AddComponent(id4); + _sys.Ent4Comps = id4; + + var id8 = _entityManager.Spawn(); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _entityManager.AddComponent(id8); + _sys.Ent8Comps = id8; + + var id16 = _entityManager.Spawn(); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _entityManager.AddComponent(id16); + _sys.Ent16Comps = id16; + } + + [Benchmark(Baseline = true)] + public int RaiseEvent1() + { + return _sys.RaiseEvent1(); + } + + [Benchmark] + public int RaiseEvent2() + { + return _sys.RaiseEvent2(); + } + + [Benchmark] + public int RaiseEvent4() + { + return _sys.RaiseEvent4(); + } + + [Benchmark] + public int RaiseEvent8() + { + return _sys.RaiseEvent8(); + } + + [Benchmark] + public int RaiseEvent16() + { + return _sys.RaiseEvent16(); + } + + [Benchmark] + public int RaiseCompEvent() + { + return _sys.RaiseCompEvent(); + } + + [Benchmark] + public int RaiseICompEvent() + { + return _sys.RaiseICompEvent(); + } + + [Benchmark] + public int RaiseNetEvent() + { + return _sys.RaiseNetIdEvent(); + } + + [Benchmark] + public int RaiseCSharpEvent() + { + return _sys.CSharpEvent(); + } + + public sealed partial class BenchSystem : EntitySystem + { + public Entity Ent; + public Entity Ent2; + public EntityUid Ent2Comps; + public EntityUid Ent4Comps; + public EntityUid Ent8Comps; + public EntityUid Ent16Comps; + + public delegate void EntityEventHandler(EntityUid uid, TransformComponent comp, ref BenchEv ev); + + public event EntityEventHandler? OnCSharpEvent; + public ushort NetId; + internal EntityEventBus.DirectedEventHandler?[] EvSubs = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + SubscribeLocalEvent(OnEvent); + OnCSharpEvent += OnEvent; + } + + public int RaiseEvent1() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent.Owner, ref ev); + return ev.N; + } + + public int RaiseEvent2() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent2Comps, ref ev); + return ev.N; + } + + public int RaiseEvent4() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent4Comps, ref ev); + return ev.N; + } + + public int RaiseEvent8() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent8Comps, ref ev); + return ev.N; + } + + public int RaiseEvent16() + { + var ev = new BenchEv(); + RaiseLocalEvent(Ent16Comps, ref ev); + return ev.N; + } + + public int RaiseCompEvent() + { + var ev = new BenchEv(); + RaiseComponentEvent(Ent.Owner, Ent.Comp, ref ev); + return ev.N; + } + + public int RaiseICompEvent() + { + // Raise with an IComponent instead of concrete type + var ev = new BenchEv(); + RaiseComponentEvent(Ent2.Owner, Ent2.Comp, ref ev); + return ev.N; + } + + public int RaiseNetIdEvent() + { + // Raise a "IComponent" event using a net-id index delegate array (for PVS & client game-state events) + var ev = new BenchEv(); + ref var unitEv = ref Unsafe.As(ref ev); + EvSubs[NetId]?.Invoke(Ent2.Owner, Ent2.Comp, ref unitEv); + return ev.N; + } + + public int CSharpEvent() + { + var ev = new BenchEv(); + OnCSharpEvent?.Invoke(Ent.Owner, Ent.Comp, ref ev); + return ev.N; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void OnEvent(EntityUid uid, T component, ref BenchEv args) + { + args.N += uid.Id; + } + + [ByRefEvent] + [ComponentEvent(Exclusive = false)] + public struct BenchEv + { + public int N; + } + + [RegisterComponent] + public sealed partial class Bench1Component : Component; + + [RegisterComponent] + public sealed partial class Bench2Component : Component; + + [RegisterComponent] + public sealed partial class Bench3Component : Component; + + [RegisterComponent] + public sealed partial class Bench4Component : Component; + + [RegisterComponent] + public sealed partial class Bench5Component : Component; + + [RegisterComponent] + public sealed partial class Bench6Component : Component; + + [RegisterComponent] + public sealed partial class Bench7Component : Component; + + [RegisterComponent] + public sealed partial class Bench8Component : Component; + + [RegisterComponent] + public sealed partial class Bench9Component : Component; + + [RegisterComponent] + public sealed partial class Bench10Component : Component; + + [RegisterComponent] + public sealed partial class Bench11Component : Component; + + [RegisterComponent] + public sealed partial class Bench12Component : Component; + + [RegisterComponent] + public sealed partial class Bench13Component : Component; + + [RegisterComponent] + public sealed partial class Bench14Component : Component; + + [RegisterComponent] + public sealed partial class Bench15Component : Component; + + [RegisterComponent] + public sealed partial class Bench16Component : Component; + } +} From 657e9bfb2d4fd800987831690d72448b5375dca8 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Sun, 12 Jul 2026 16:44:31 +1000 Subject: [PATCH 148/178] Reduce ImageSharp arraypool ballooning (#6768) Dispose images as soon as practical so we don't balloon its arraypool as much. --- .../ResourceCache.Preload.cs | 47 +++++++++++------ .../ResourceTypes/RSIResource.cs | 52 ++++++++++++------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs index 5e35b74de95..7d1e5a693c2 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.Preload.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.Preload.cs @@ -325,10 +325,18 @@ private void PreloadRsis(ISawmill sawmill) // Finalize the atlases. for (var i = 0; i < imageAtlases.Count; i++) { - var atlasTexture = Clyde.LoadTextureFromImage(imageAtlases[i], $"Meta atlas {i}"); - finalAtlases.Add(atlasTexture); + var imageAtlas = imageAtlases[i]; + try + { + var atlasTexture = Clyde.LoadTextureFromImage(imageAtlas, $"Meta atlas {i}"); + finalAtlases.Add(atlasTexture); - sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlases[i].Height):P2}, fill percentage: {(float)imageAtlases[i].Height / maxSize:P2}"); + sawmill.Debug($"(Meta atlas {i}) - cropped utilization: {(float)finalPixels[i] / (maxSize * imageAtlas.Height):P2}, fill percentage: {(float)imageAtlas.Height / maxSize:P2}"); + } + finally + { + imageAtlas.Dispose(); + } } // Finally, reference the actual atlas from the RSIs. @@ -359,23 +367,30 @@ private void PreloadRsis(ISawmill sawmill) var errors = 0; foreach (var data in rsiList) { - if (data.Bad) - { - errors += 1; - continue; - } - try { - var rsiRes = new RSIResource(); - rsiRes.LoadFinish(this, data); - resList[data.Path] = rsiRes; + if (data.Bad) + { + errors += 1; + continue; + } + + try + { + var rsiRes = new RSIResource(); + rsiRes.LoadFinish(this, data); + resList[data.Path] = rsiRes; + } + catch (Exception e) + { + sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}"); + data.Bad = true; + errors += 1; + } } - catch (Exception e) + finally { - sawmill.Error($"Exception while loading RSI {data.Path}:\n{e}"); - data.Bad = true; - errors += 1; + data.AtlasSheet?.Dispose(); } } diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index 0880f8e9518..494834e5490 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -83,27 +83,44 @@ private static void LoadPreTextureFolder(IResourceManager manager, LoadStepData metadata = RsiLoading.LoadRsiMetadata(manifestFile); } - data.FrameCounts = RsiLoading.CalculateFrameCounts(metadata); - data.Images = RsiLoading.LoadImages( - metadata, - SixLabors.ImageSharp.Configuration.Default, - name => - { - var texPath = data.Path / (name + ".png"); - return manager.ContentFileRead(texPath); - }); + Image[]? images = null; + Image sheet; - var sheet = RsiLoading.GenerateAtlas( - metadata, - data.FrameCounts, - data.Images, - SixLabors.ImageSharp.Configuration.Default, - out var dimensionX); + try + { + data.FrameCounts = RsiLoading.CalculateFrameCounts(metadata); + images = RsiLoading.LoadImages( + metadata, + SixLabors.ImageSharp.Configuration.Default, + name => + { + var texPath = data.Path / (name + ".png"); + return manager.ContentFileRead(texPath); + }); + + sheet = RsiLoading.GenerateAtlas( + metadata, + data.FrameCounts, + images, + SixLabors.ImageSharp.Configuration.Default, + out var dimensionX); + + data.AtlasSheet = sheet; + data.DimX = dimensionX; + } + finally + { + if (images != null) + { + foreach (var image in images) + { + image.Dispose(); + } + } + } LoadPreTextureCommon(metadata, data); - data.AtlasSheet = sheet; - data.DimX = dimensionX; data.LoadParameters = metadata.LoadParameters; data.MetaAtlas = metadata.MetaAtlas; } @@ -392,7 +409,6 @@ internal sealed class LoadStepData public int DimX; public StateReg[] AtlasList = default!; public int[] FrameCounts = default!; - public Image[] Images = default!; public Vector2i FrameSize; public Dictionary CallbackOffsets = default!; public Texture AtlasTexture = default!; From b3daa0b7947f74bb6cb24f1c0e1f3174a79bbfb6 Mon Sep 17 00:00:00 2001 From: eoineoineoin Date: Tue, 14 Jul 2026 03:59:12 +0100 Subject: [PATCH 149/178] Fix BaseWindow jittering on resize (#6788) --- .../UserInterface/Controls/LayoutContainer.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Robust.Client/UserInterface/Controls/LayoutContainer.cs b/Robust.Client/UserInterface/Controls/LayoutContainer.cs index f161c3b6cce..01f35d48c1f 100644 --- a/Robust.Client/UserInterface/Controls/LayoutContainer.cs +++ b/Robust.Client/UserInterface/Controls/LayoutContainer.cs @@ -462,20 +462,19 @@ public static void SetMarginsPreset(Control control, LayoutPreset preset, protected override Vector2 MeasureOverride(Vector2 availableSize) { var min = Vector2.Zero; - var uiScale = UIScale; foreach (var child in Children) { var growH = child.GetValue(GrowHorizontalProperty); var growV = child.GetValue(GrowVerticalProperty); - var anchorMargins = CalcAnchorMargins(availableSize, uiScale, child); + var anchorMargins = CalcAnchorMargins(availableSize, child); var size = availableSize; if (growH == GrowDirection.Constrain) - size.X = anchorMargins.Width / uiScale; + size.X = anchorMargins.Width; if (growV == GrowDirection.Constrain) - size.Y = anchorMargins.Height / uiScale; + size.Y = anchorMargins.Height; child.Measure(size); min = Vector2.Max(min, child.DesiredSize); @@ -488,7 +487,7 @@ protected override Vector2 ArrangeOverride(Vector2 finalSize) { foreach (var child in Children) { - child.Arrange(CalcChildRect(finalSize, UIScale, child, out _)); + child.Arrange(CalcChildRect(finalSize, child, out _)); } return finalSize; @@ -509,7 +508,7 @@ protected internal override void Draw(DrawingHandleScreen handle) continue; } - var rect = CalcChildRect(Size, UIScale, child, out var anchorSize); + var rect = CalcChildRect(Size, child, out var anchorSize); var left = rect.Left * UIScale; var right = rect.Right * UIScale; @@ -542,24 +541,22 @@ void DrawHLine(float y, Color color) } } - private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control child) + private static UIBox2 CalcAnchorMargins(Vector2 ourSize, Control child) { - var (pSizeX, pSizeY) = ourSize * uiScale; - var anchorLeft = child.GetValue(AnchorLeftProperty); var anchorTop = child.GetValue(AnchorTopProperty); var anchorRight = child.GetValue(AnchorRightProperty); var anchorBottom = child.GetValue(AnchorBottomProperty); - var marginLeft = child.GetValue(MarginLeftProperty) * uiScale; - var marginTop = child.GetValue(MarginTopProperty) * uiScale; - var marginRight = child.GetValue(MarginRightProperty) * uiScale; - var marginBottom = child.GetValue(MarginBottomProperty) * uiScale; + var marginLeft = child.GetValue(MarginLeftProperty); + var marginTop = child.GetValue(MarginTopProperty); + var marginRight = child.GetValue(MarginRightProperty); + var marginBottom = child.GetValue(MarginBottomProperty); - var left = anchorLeft * pSizeX + marginLeft; - var top = anchorTop * pSizeY + marginTop; - var right = anchorRight * pSizeX + marginRight; - var bottom = anchorBottom * pSizeY + marginBottom; + var left = anchorLeft * ourSize.X + marginLeft; + var top = anchorTop * ourSize.Y + marginTop; + var right = anchorRight * ourSize.X + marginRight; + var bottom = anchorBottom * ourSize.Y + marginBottom; // Yes, this can return boxes with left > right (and top > bottom). // This is "intentional", see comment in CalcChildRect. @@ -567,23 +564,23 @@ private static UIBox2 CalcAnchorMargins(Vector2 ourSize, float uiScale, Control return new UIBox2(left, top, right, bottom); } - private static UIBox2 CalcChildRect(Vector2 ourSize, float uiScale, Control child, out UIBox2 anchorSize) + private static UIBox2 CalcChildRect(Vector2 ourSize, Control child, out UIBox2 anchorSize) { // Calculate where the control "wants" to be by its anchors/margins. var growHorizontal = child.GetValue(GrowHorizontalProperty); var growVertical = child.GetValue(GrowVerticalProperty); - anchorSize = CalcAnchorMargins(ourSize, uiScale, child); + anchorSize = CalcAnchorMargins(ourSize, child); // This intentionally results in negatives if the right bound is < the left bound. // Which then causes HandleLayoutOverflow to CORRECTLY work from the right bound instead. var (wSizeX, wSizeY) = (anchorSize.Right - anchorSize.Left, anchorSize.Bottom - anchorSize.Top); - var (minSizeX, minSizeY) = child.DesiredPixelSize; + var (minSizeX, minSizeY) = child.DesiredSize; HandleLayoutOverflow(growHorizontal, minSizeX, anchorSize.Left, wSizeX, out var posX, out var sizeX); HandleLayoutOverflow(growVertical, minSizeY, anchorSize.Top, wSizeY, out var posY, out var sizeY); - return UIBox2.FromDimensions(posX / uiScale, posY / uiScale, sizeX / uiScale, sizeY / uiScale); + return UIBox2.FromDimensions(posX, posY, sizeX, sizeY); } private static void HandleLayoutOverflow(GrowDirection direction, float minSize, float wPos, float wSize, From d314fe48a8e4d257a24a2ad912d3169513cc61e7 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:58:15 +1000 Subject: [PATCH 150/178] Avoid NetMessage copies for MsgState (#6744) * Avoid NetMessage copies for MsgState We have some rough idea of the final size so we'll set the NetMessage size a bit above that so we can actually use Lidgren's storage pool. Currently it gets set to 4 but NetMessage uses Array.Resize if the size is inadequate so it never uses the pooled buffers for this. * RN --- RELEASE-NOTES.md | 2 +- Robust.Shared/Network/Messages/MsgState.cs | 33 ++++++++++++++++++++++ Robust.Shared/Network/NetEncryption.cs | 4 ++- Robust.Shared/Network/NetManager.Send.cs | 2 +- Robust.Shared/Network/NetManager.cs | 9 ++++-- Robust.Shared/Network/NetMessage.cs | 6 ++++ 6 files changed, 51 insertions(+), 5 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 6aaa3abdf94..053e4692b95 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,7 +39,7 @@ END TEMPLATE--> ### New features -*None yet* +* Added a NetMessage.EstimateBufferSize() to provide an estimate of the initialCapacity required for NetMessages. This will make NetMessage take an existing adequately sized pooled buffer for your message. This is opt-in and will default to the old 4-byte size if not specified. ### Bugfixes diff --git a/Robust.Shared/Network/Messages/MsgState.cs b/Robust.Shared/Network/Messages/MsgState.cs index 95eed008e8d..0b20558c7dd 100644 --- a/Robust.Shared/Network/Messages/MsgState.cs +++ b/Robust.Shared/Network/Messages/MsgState.cs @@ -21,6 +21,7 @@ public sealed class MsgState : NetMessage // TODO PVS make this a cvar // TODO PVS figure out optimal value public const int CompressionThreshold = 256; + private const int CompressedSizeEstimateDivisor = 4; public override MsgGroups MsgGroup => MsgGroups.Entity; @@ -33,6 +34,38 @@ public sealed class MsgState : NetMessage internal bool ForceSendReliably; + public override int EstimateBufferSize() + { + var uncompressedLength = (int) StateStream.Length; + // We have no idea what the compressed size will be but we'll try and guess slightly over + // mostly want to avoid a small initial size and then immediately copying. + var payloadLength = uncompressedLength > CompressionThreshold + ? Math.Max(CompressionThreshold, uncompressedLength / CompressedSizeEstimateDivisor) + : uncompressedLength; + var compressedLength = uncompressedLength > CompressionThreshold ? payloadLength : 0; + + // Message ID + two variable-length size prefixes + payload. + return 1 + + VariableInt32Size(uncompressedLength) + + VariableInt32Size(compressedLength) + + payloadLength; + } + + private static int VariableInt32Size(int value) + { + // Look at NetSerializer if you want to understand the zigzag. + var zigzag = (uint) ((value << 1) ^ (value >> 31)); + var size = 1; + + while (zigzag >= 0x80) + { + zigzag >>= 7; + size++; + } + + return size; + } + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { MsgSize = buffer.LengthBytes; diff --git a/Robust.Shared/Network/NetEncryption.cs b/Robust.Shared/Network/NetEncryption.cs index 625df53cf2a..e65d35af4a5 100644 --- a/Robust.Shared/Network/NetEncryption.cs +++ b/Robust.Shared/Network/NetEncryption.cs @@ -9,6 +9,8 @@ namespace Robust.Shared.Network; internal sealed class NetEncryption { + public const int EncryptionOverhead = sizeof(ulong) + CryptoAeadXChaCha20Poly1305Ietf.AddBytes; + // Use a counter for nonces. The counter is 64-bit, I will be impressed if you ever manage to run it out. // 64-bit counter (incl over the wire) is fine, don't need the whole 192-bit. // Server starts at 0, client starts at 1, increment by two. @@ -31,7 +33,7 @@ public unsafe void Encrypt(NetOutgoingMessage message) var nonce = Interlocked.Add(ref _nonce, 2); var lengthBytes = message.LengthBytes; - var encryptedSize = CryptoAeadXChaCha20Poly1305Ietf.AddBytes + lengthBytes + sizeof(ulong); + var encryptedSize = lengthBytes + EncryptionOverhead; var data = message.Data.AsSpan(0, lengthBytes); diff --git a/Robust.Shared/Network/NetManager.Send.cs b/Robust.Shared/Network/NetManager.Send.cs index 3da72909ed9..9d86bd029f1 100644 --- a/Robust.Shared/Network/NetManager.Send.cs +++ b/Robust.Shared/Network/NetManager.Send.cs @@ -62,7 +62,7 @@ private void CoreSendMessage( return; } - var packet = BuildMessage(message, channel.Connection.Peer); + var packet = BuildMessage(message, channel); var method = message.DeliveryMethod; var seqChannel = message.SequenceChannel; diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 7df4fe1e6a6..ad9c541fc8c 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -1227,9 +1227,14 @@ public T CreateNetMessage() return new T(); } - private NetOutgoingMessage BuildMessage(NetMessage message, NetPeer peer) + private NetOutgoingMessage BuildMessage(NetMessage message, NetChannel channel) { - var packet = peer.CreateMessage(4); + var initialCapacity = message.EstimateBufferSize(); + + if (channel.Encryption != null) + initialCapacity += NetEncryption.EncryptionOverhead; + + var packet = channel.Connection.Peer.CreateMessage(initialCapacity); if (!_strings.TryFindStringId(message.MsgName, out int msgId)) throw new NetManagerException( diff --git a/Robust.Shared/Network/NetMessage.cs b/Robust.Shared/Network/NetMessage.cs index 83b0f805ca6..7efdf23939b 100644 --- a/Robust.Shared/Network/NetMessage.cs +++ b/Robust.Shared/Network/NetMessage.cs @@ -86,6 +86,12 @@ protected NetMessage() /// public abstract void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer); + /// + /// Estimated size of the serialized payload, excluding transport encryption overhead. + /// Used to size Lidgren's outgoing buffer before . + /// + public virtual int EstimateBufferSize() => 4; + public virtual NetDeliveryMethod DeliveryMethod { get From 6052c50264de3ebb00d046fc56c49421cd7a5e3a Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:07:08 +1000 Subject: [PATCH 151/178] Dispose PVS session statestream on disconnect (#6663) Dispose session statestream on ClearSendHistory I got no idea how this happened but praying this fixes it. --- Robust.Server/GameStates/PvsSystem.Session.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Robust.Server/GameStates/PvsSystem.Session.cs b/Robust.Server/GameStates/PvsSystem.Session.cs index 9e3a87dee40..c724dba6bde 100644 --- a/Robust.Server/GameStates/PvsSystem.Session.cs +++ b/Robust.Server/GameStates/PvsSystem.Session.cs @@ -149,6 +149,9 @@ private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) private void ClearSendHistory(PvsSession session) { + session.StateStream?.Dispose(); + session.StateStream = null; + if (session.Overflow != null) _entDataListPool.Return(session.Overflow.Value.SentEnts); session.Overflow = null; From 07ddeaa8d6205a61eb46a0c9fad850b0c0e712de Mon Sep 17 00:00:00 2001 From: AJCM-git <60196617+AJCM-git@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:17:31 -0400 Subject: [PATCH 152/178] File Dialog: Expose file name of the selected file (#6755) * FileDialog cleanup and extra functionality * this was for testing ooops * stringing things along --- .../UserInterface/FileDialogManager.cs | 72 +++++++---- .../UserInterface/IFileDialogManager.cs | 122 +++++++++++------- 2 files changed, 120 insertions(+), 74 deletions(-) diff --git a/Robust.Client/UserInterface/FileDialogManager.cs b/Robust.Client/UserInterface/FileDialogManager.cs index 7921c38f8cd..27342c8755b 100644 --- a/Robust.Client/UserInterface/FileDialogManager.cs +++ b/Robust.Client/UserInterface/FileDialogManager.cs @@ -9,28 +9,60 @@ namespace Robust.Client.UserInterface; internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManager { - public async Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null) + private static FileShare Validate(FileAccess access, FileShare? share) { if ((access & FileAccess.ReadWrite) != access) throw new ArgumentException("Invalid file access specified"); var realShare = share ?? (access == FileAccess.Read ? FileShare.Read : FileShare.None); + if ((realShare & (FileShare.ReadWrite | FileShare.Delete)) != realShare) throw new ArgumentException("Invalid file share specified"); - string? name; - if (clyde.FileDialogImpl is { } clydeImpl) - name = await clydeImpl.OpenFile(filters); - else - return null; + return realShare; + } + + private async Task Prompt(bool isSave, FileDialogFilters? filters) + { + if (clyde.FileDialogImpl is { } impl) + return isSave ? await impl.SaveFile(filters) : await impl.OpenFile(filters); + + return null; + } - if (name == null) - return null; + public async Task<(Stream?, string?)> GetFileAndName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + var name = await Prompt(false, filters); - return File.Open(name, FileMode.Open, access, realShare); + if (name == null) return (null, null); + + var path = Path.GetFileName(name); + return (File.Open(name, FileMode.Open, access, Validate(access, share)), path); + } + + public async Task GetName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + Validate(access, share); + var name = await Prompt(false, filters); + + return name == null ? null : Path.GetFileName(name); + } + + public async Task OpenFile( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null) + { + var realShare = Validate(access, share); + var name = await Prompt(false, filters); + + return name == null ? null : File.Open(name, FileMode.Open, access, realShare); } public async Task<(Stream, bool)?> SaveFile( @@ -39,20 +71,10 @@ internal sealed class FileDialogManager(IClydeInternal clyde) : IFileDialogManag FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None) { - if ((access & FileAccess.ReadWrite) != access) - throw new ArgumentException("Invalid file access specified"); - - if ((share & (FileShare.ReadWrite | FileShare.Delete)) != share) - throw new ArgumentException("Invalid file share specified"); - - string? name; - if (clyde.FileDialogImpl is { } clydeImpl) - name = await clydeImpl.SaveFile(filters); - else - return null; + Validate(access, share); + var name = await Prompt(true, filters); - if (name == null) - return null; + if (name == null) return null; try { diff --git a/Robust.Client/UserInterface/IFileDialogManager.cs b/Robust.Client/UserInterface/IFileDialogManager.cs index ab0ef06eada..9dcdc9a85ea 100644 --- a/Robust.Client/UserInterface/IFileDialogManager.cs +++ b/Robust.Client/UserInterface/IFileDialogManager.cs @@ -2,60 +2,84 @@ using System.Threading.Tasks; using Robust.Client.Graphics; -namespace Robust.Client.UserInterface +namespace Robust.Client.UserInterface; + +/// +/// Manager for opening of file dialogs. +/// +/// +/// File dialogs are native to the OS being ran on. +/// All operations are asynchronous to prevent locking up the main thread while the user makes his pick. +/// +[NotContentImplementable] +public interface IFileDialogManager { /// - /// Manager for opening of file dialogs. + /// Open a file dialog used for opening a single file and getting its filename. /// - /// - /// File dialogs are native to the OS being ran on. - /// All operations are asynchronous to prevent locking up the main thread while the user makes his pick. - /// - [NotContentImplementable] - public interface IFileDialogManager - { - /// - /// Open a file dialog used for opening a single file. - /// - /// - /// The file stream for the file the user opened. - /// if the user cancelled the action. - /// - /// Filters for file types that the user can select. - /// What access is desired from the file operation. - /// - /// What sharing mode is desired from the file operation. - /// If null is provided and is , - /// is selected, otherwise . - /// - Task OpenFile( - FileDialogFilters? filters = null, - FileAccess access = FileAccess.ReadWrite, - FileShare? share = null); + /// + /// The file stream and name for the file the user opened. + /// if the user canceled the action. + /// + /// Filters for file types that the user can select. + /// What access is desired from the file operation. + /// + /// What sharing mode is desired from the file operation. + /// If null is provided and is , + /// is selected, otherwise . + /// + Task<(Stream?, string?)> GetFileAndName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); - /// - /// Open a file dialog used for saving a single file. - /// - /// - /// The file stream the user chose to save to, and whether the file already existed. - /// Null if the user cancelled the action. - /// - /// Should we truncate an existing file to 0-size then write or append. - /// What access is desired from the file operation. - /// Sharing mode for the opened file. - Task<(Stream fileStream, bool alreadyExisted)?> SaveFile( - FileDialogFilters? filters = null, - bool truncate = true, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.None); - } + /// + /// Open a file dialog used for getting the file name of the selected file. + /// + /// + /// The file name for the file the user opened. + /// if the user canceled the action. + /// + /// + Task GetName( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); /// - /// Internal implementation interface used to connect and . + /// Open a file dialog used for opening the selected file. /// - internal interface IFileDialogManagerImplementation - { - Task OpenFile(FileDialogFilters? filters); - Task SaveFile(FileDialogFilters? filters); - } + /// + /// The file stream for the file the user opened. + /// if the user cancelled the action. + /// + /// + Task OpenFile( + FileDialogFilters? filters = null, + FileAccess access = FileAccess.ReadWrite, + FileShare? share = null); + + /// + /// Open a file dialog used for saving a single file. + /// + /// + /// The file stream the user chose to save to, and whether the file already existed. + /// Null if the user canceled the action. + /// + /// Should we truncate an existing file to 0-size then write or append. + /// + Task<(Stream fileStream, bool alreadyExisted)?> SaveFile( + FileDialogFilters? filters = null, + bool truncate = true, + FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.None); +} + +/// +/// Internal implementation interface used to connect and . +/// +internal interface IFileDialogManagerImplementation +{ + Task OpenFile(FileDialogFilters? filters); + Task SaveFile(FileDialogFilters? filters); } From 3211b437c918fe9263c24a8e4385dee7ed206564 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:37:48 +1000 Subject: [PATCH 153/178] Cache GetAllChildren (#6766) * Cache reflection child type lookups * field --- Robust.Shared/Reflection/ReflectionManager.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/Reflection/ReflectionManager.cs b/Robust.Shared/Reflection/ReflectionManager.cs index b1ce6ec669e..ac866a881de 100644 --- a/Robust.Shared/Reflection/ReflectionManager.cs +++ b/Robust.Shared/Reflection/ReflectionManager.cs @@ -43,8 +43,11 @@ public abstract partial class ReflectionManager : IReflectionManager private readonly ReaderWriterLockSlim _yamlTypeTagCacheLock = new(); private readonly List _getAllTypesCache = new(); + private readonly Dictionary<(Type BaseType, bool Inclusive), Type[]> _getAllChildrenCache = new(); private ISawmill _sawmill = default!; + private readonly List _childrenCache = new(); + public void Initialize() { _sawmill = _logMan.GetSawmill("Reflection"); @@ -61,6 +64,12 @@ public IEnumerable GetAllChildren(Type baseType, bool inclusive = false) { EnsureGetAllTypesCache(); + var key = (baseType, inclusive); + if (_getAllChildrenCache.TryGetValue(key, out var cached)) + return cached; + + _childrenCache.Clear(); + foreach (var type in _getAllTypesCache) { if (!baseType.IsAssignableFrom(type) || type.IsAbstract) @@ -69,8 +78,12 @@ public IEnumerable GetAllChildren(Type baseType, bool inclusive = false) if (baseType == type && !inclusive) continue; - yield return type; + _childrenCache.Add(type); } + + cached = _childrenCache.ToArray(); + _getAllChildrenCache.Add(key, cached); + return cached; } private void EnsureGetAllTypesCache() @@ -114,6 +127,7 @@ public void LoadAssemblies(IEnumerable assemblies) this.assemblies.AddRange(assembliesArray); _getAllTypesCache.Clear(); + _getAllChildrenCache.Clear(); OnAssemblyAdded?.Invoke(this, new ReflectionUpdateEventArgs(this)); } From 205a8a6479e9218dabc37ebc280ba05e1e199e50 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:48:50 +1000 Subject: [PATCH 154/178] Pool PVS entity state collections (#6752) --- .../GameStates/PvsSystem.GetStates.cs | 11 +++-- Robust.Server/GameStates/PvsSystem.Pooling.cs | 41 +++++++++++++++++++ .../GameStates/PvsSystem.Serialize.cs | 2 +- .../GameStates/PvsSystem.ToSendSet.cs | 4 ++ .../Replays/ReplayRecordingManager.cs | 4 +- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Robust.Server/GameStates/PvsSystem.GetStates.cs b/Robust.Server/GameStates/PvsSystem.GetStates.cs index 87786ceacef..b2247c32c60 100644 --- a/Robust.Server/GameStates/PvsSystem.GetStates.cs +++ b/Robust.Server/GameStates/PvsSystem.GetStates.cs @@ -22,10 +22,10 @@ internal sealed partial class PvsSystem /// New entity State for the given entity. private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta) { - var changed = new List(); + var changed = GetComponentChangeList(meta.NetComponents.Count); bool sendCompList = meta.LastComponentRemoved > fromTick; - HashSet? netComps = sendCompList ? new() : null; + HashSet? netComps = sendCompList ? GetNetComponentSet() : null; var stateEv = new ComponentGetState(player, fromTick); foreach (var (netId, component) in meta.NetComponents) @@ -83,10 +83,10 @@ private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta) { var bus = EntityManager.EventBusInternal; - var changed = new List(); + var changed = GetComponentChangeList(meta.NetComponents.Count); var stateEv = new ComponentGetState(player, GameTick.Zero); - HashSet netComps = new(); + HashSet netComps = GetNetComponentSet(); foreach (var (netId, component) in meta.NetComponents) { @@ -197,6 +197,7 @@ private void GetAllEntityStates(PvsSession pvsSession) Last modified: {md.EntityLastModifiedTick} Metadata last modified: {md.LastModifiedTick} Transform last modified: {Transform(uid).LastModifiedTick}"); + ReturnEntityState(state); continue; } @@ -217,6 +218,8 @@ private void GetAllEntityStates(PvsSession pvsSession) var state = GetEntityState(session, uid, fromTick, md); if (!state.Empty) pvsSession.States.Add(state); + else + ReturnEntityState(state); } } } diff --git a/Robust.Server/GameStates/PvsSystem.Pooling.cs b/Robust.Server/GameStates/PvsSystem.Pooling.cs index 871bd8f69a6..49c5735a915 100644 --- a/Robust.Server/GameStates/PvsSystem.Pooling.cs +++ b/Robust.Server/GameStates/PvsSystem.Pooling.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Extensions.ObjectPool; using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Utility; using SharpZstd.Interop; @@ -19,9 +20,15 @@ internal sealed partial class PvsSystem private readonly ObjectPool> _entDataListPool = new DefaultObjectPool>(new ListPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _componentChangeListPool + = new DefaultObjectPool>(new ListPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _uidSetPool = new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize); + private readonly ObjectPool> _netComponentSetPool + = new DefaultObjectPool>(new SetPolicy(), MaxVisPoolSize); + private readonly ObjectPool _chunkPool = new DefaultObjectPool(new PvsChunkPolicy(), 256); @@ -63,4 +70,38 @@ private sealed class PvsThreadResources CompressionContext.Dispose(); } } + + private List GetComponentChangeList(int capacity) + { + var list = _componentChangeListPool.Get(); + list.EnsureCapacity(capacity); + return list; + } + + private HashSet GetNetComponentSet() + { + return _netComponentSetPool.Get(); + } + + private void ReturnEntityState(EntityState state) + { + if (state.ComponentChanges.Value is List changes) + _componentChangeListPool.Return(changes); + + if (state.NetComponents is { } netComps) + { + _netComponentSetPool.Return(netComps); + state.NetComponents = null; + } + } + + internal void ClearSessionState(PvsSession session) + { + foreach (var state in session.States) + { + ReturnEntityState(state); + } + + session.ClearState(); + } } diff --git a/Robust.Server/GameStates/PvsSystem.Serialize.cs b/Robust.Server/GameStates/PvsSystem.Serialize.cs index 25a72ec864f..7ffb6e79ec3 100644 --- a/Robust.Server/GameStates/PvsSystem.Serialize.cs +++ b/Robust.Server/GameStates/PvsSystem.Serialize.cs @@ -71,6 +71,6 @@ private void SerializeSessionState(PvsSession data) _serializer.SerializeDirect(data.StateStream, data.State); } - data.ClearState(); + ClearSessionState(data); } } diff --git a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs index 99e3e86b5c0..00aaa7185bc 100644 --- a/Robust.Server/GameStates/PvsSystem.ToSendSet.cs +++ b/Robust.Server/GameStates/PvsSystem.ToSendSet.cs @@ -112,6 +112,8 @@ private void AddEntity(PvsSession session, ref PvsChunk.ChunkEntity ent, ref Pvs if (!entState.Empty) session.States.Add(entState); + else + ReturnEntityState(entState); } /// @@ -189,6 +191,8 @@ private bool AddEntity(PvsSession session, Entity entity, Gam if (!entState.Empty) session.States.Add(entState); + else + ReturnEntityState(entState); return true; } diff --git a/Robust.Server/Replays/ReplayRecordingManager.cs b/Robust.Server/Replays/ReplayRecordingManager.cs index 47c7dc74a15..96549589298 100644 --- a/Robust.Server/Replays/ReplayRecordingManager.cs +++ b/Robust.Server/Replays/ReplayRecordingManager.cs @@ -47,7 +47,7 @@ public void Update() _pvs.ComputeSessionState(_pvsSession); Update(_pvsSession.State); - _pvsSession.ClearState(); + _pvs.ClearSessionState(_pvsSession); _pvsSession.LastReceivedAck = Timing.CurTick; } @@ -55,6 +55,6 @@ protected override void Reset() { base.Reset(); _pvsSession.LastReceivedAck = GameTick.Zero; - _pvsSession.ClearState(); + _pvs.ClearSessionState(_pvsSession); } } From 4e11a3892ed7890fb62ce62925729f29b41a6041 Mon Sep 17 00:00:00 2001 From: B_Kirill <153602297+B-Kirill@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:00:18 +1000 Subject: [PATCH 155/178] Add API for loading OwnedTexture with texture parameters (#6794) --- .../ResourceTypes/TextureResource.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs index d0ae97c601f..c450c166e24 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs @@ -88,6 +88,18 @@ internal void LoadFinish(IResourceCache cache, LoadStepData data) data.Image.Dispose(); } + /// + /// Loads a texture from path as an . + /// Texture parameters are taken from a matching .yml file if present. + /// + public static OwnedTexture LoadOwnedTexture(IResourceManager resManager, IClyde clyde, ResPath path) + { + var loadParams = TryLoadTextureParameters(resManager, path) ?? TextureLoadParameters.Default; + using var stream = resManager.ContentFileRead(path); + using var image = Image.Load(stream); + return clyde.LoadTextureFromImage(image, path.ToString(), loadParams); + } + private static TextureLoadParameters? TryLoadTextureParameters(IResourceManager cache, ResPath path) { var metaPath = path.WithName(path.Filename + ".yml"); From 62e3bdbd2cbc91b9c50c60b5fd5865e5d33139a1 Mon Sep 17 00:00:00 2001 From: Centronias Date: Wed, 15 Jul 2026 22:17:13 -0700 Subject: [PATCH 156/178] Replace overly restrictive (and sometimes wrong?) cast in serialization generator (#6796) --- Robust.Serialization.Generator/Generator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index ec9c3f9e7d0..118c2be2c1a 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -108,7 +108,7 @@ private static (string, string)? GenerateForDataDefinition( var containingTypesEnd = new StringBuilder(); foreach (var parent in containingTypes) { - var syntax = (ClassDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(); + var syntax = (TypeDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(); if (!IsPartial(syntax)) { nonPartial = true; From e6a7849fe3f53613adf801c8eaf190f8c4074e97 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:17:55 +1000 Subject: [PATCH 157/178] Skip log formatting if level not enabled (#6764) --- Robust.Shared/Log/LogManager.Sawmill.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Log/LogManager.Sawmill.cs b/Robust.Shared/Log/LogManager.Sawmill.cs index 60ec8f92a86..c25e1189b52 100644 --- a/Robust.Shared/Log/LogManager.Sawmill.cs +++ b/Robust.Shared/Log/LogManager.Sawmill.cs @@ -78,19 +78,22 @@ public bool IsLogLevelEnabled(LogLevel level) public void Log(LogLevel level, Exception? exception, string message, params object?[] args) { + if (!IsLogLevelEnabled(level)) + return; + if (!_sLogger.BindMessageTemplate(message, args, out var parsedTemplate, out var properties)) return; var msg = new LogEvent(DateTimeOffset.Now, level.ToSerilog(), exception, parsedTemplate, properties); - if (!IsLogLevelEnabled(level)) - return; - LogInternal(Name, msg); } public void Log(LogLevel level, string message, params object?[] args) { + if (!IsLogLevelEnabled(level)) + return; + if (args.Length != 0 && message.Contains("{0")) { // Fallback for logs that still use the string.Format approach. From 7454c6b3167dc2235f3dae8a4b32f1ffe48333df Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:34:56 +1000 Subject: [PATCH 158/178] Fix serialization normalize performance (#6797) --- Robust.Serialization.Generator/Generator.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 118c2be2c1a..54f95d742db 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; using static Robust.Roslyn.Shared.DataDefinitionHelper; using static Robust.Serialization.Generator.CustomSerializerType; using static Robust.Serialization.Generator.Types; @@ -71,7 +72,7 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) if (!done.Add(name)) continue; - sourceContext.AddSource(name, code); + sourceContext.AddSource(name, SourceText.From(code, Encoding.UTF8)); } } ); @@ -171,14 +172,7 @@ private static (string, string)? GenerateForDataDefinition( {{containingTypesEnd}} """); - return ($"{symbolName}.g.cs", NormalizeSource(builder.ToString())); - } - - private static string NormalizeSource(string source) - { - return SyntaxFactory.ParseCompilationUnit(source) - .NormalizeWhitespace() - .ToFullString(); + return ($"{symbolName}.g.cs", builder.ToString()); } private static void GetDataFields( From 91f64a7f6c2af2be4c9660f9fb67de710d697ae4 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:37:56 +1000 Subject: [PATCH 159/178] Add sundries + static sundries to grid traversal (#6653) --- RELEASE-NOTES.md | 2 +- .../Physics/GridMovement_Test.cs | 42 +++++++++++++++++++ .../Physics/Systems/SharedBroadphaseSystem.cs | 20 +++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 053e4692b95..670b33eecea 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -43,7 +43,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* Static + StaticSundries will now also be considered for grid-traversal when a grid moves over these entities. ### Other diff --git a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs index 6f6601c8955..371134d79fc 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs @@ -74,4 +74,46 @@ await server.WaitAssertion(() => Assert.That(onGridBody.ContactCount, Is.EqualTo(1)); }); } + + [TestCase(true)] + [TestCase(false)] + public async Task TestMovingGridAttachesMapEntity(bool canCollide) + { + var server = StartServer(); + + await server.WaitIdleAsync(); + + var systems = server.ResolveDependency(); + var fixtureSystem = systems.GetEntitySystem(); + var mapManager = server.ResolveDependency(); + var entManager = server.ResolveDependency(); + var physSystem = systems.GetEntitySystem(); + var transformSystem = entManager.EntitySysManager.GetEntitySystem(); + var mapSystem = entManager.EntitySysManager.GetEntitySystem(); + + await server.WaitAssertion(() => + { + entManager.System().CreateMap(out var mapId); + var grid = mapManager.CreateGridEntity(mapId); + mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); + + var entity = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10.5f, 10.5f), mapId)); + var body = entManager.AddComponent(entity); + physSystem.SetBodyType(entity, BodyType.Dynamic, body: body); + + var shape = new PolygonShape(); + shape.SetAsBox(0.25f, 0.25f); + fixtureSystem.CreateFixture(entity, "fix1", new Fixture(shape, 0, 0, false), body: body); + physSystem.SetCanCollide(entity, canCollide, body: body); + + var xform = entManager.GetComponent(entity); + Assert.That(xform.ParentUid, Is.Not.EqualTo(grid.Owner)); + + physSystem.Update(0.001f); + transformSystem.SetLocalPosition(grid.Owner, new Vector2(10f, 10f)); + physSystem.Update(0.001f); + + Assert.That(xform.ParentUid, Is.EqualTo(grid.Owner)); + }); + } } diff --git a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs index 17d2108adef..0a15fd2c9ec 100644 --- a/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs +++ b/Robust.Shared/Physics/Systems/SharedBroadphaseSystem.cs @@ -32,6 +32,7 @@ public abstract partial class SharedBroadphaseSystem : EntitySystem private EntityQuery _xformQuery; private readonly HashSet _gridMoveBuffer = new(); + private readonly HashSet _gridSundriesMoveBuffer = new(); private float _frameTime; @@ -105,6 +106,7 @@ private void FindGridContacts(HashSet movedGrids) // we move over is getting checked for collisions, and putting it on the movebuffer is the easiest way to do so. var moveBuffer = _physicsSystem.MoveBuffer; _gridMoveBuffer.Clear(); + _gridSundriesMoveBuffer.Clear(); foreach (var gridUid in movedGrids) { @@ -121,6 +123,10 @@ private void FindGridContacts(HashSet movedGrids) QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB); QueryMapBroadphase(mapBroadphase.StaticTree, ref state, enlargedAABB); + + var sundriesState = _gridSundriesMoveBuffer; + QueryMapSundries(mapBroadphase.SundriesTree, ref sundriesState, enlargedAABB); + QueryMapSundries(mapBroadphase.StaticSundriesTree, ref sundriesState, enlargedAABB); } foreach (var proxy in _gridMoveBuffer) @@ -129,6 +135,11 @@ private void FindGridContacts(HashSet movedGrids) // If something is in our AABB then try grid traversal for it _traversal.CheckTraverse((proxy.Entity, _xformQuery.GetComponent(proxy.Entity))); } + + foreach (var uid in _gridSundriesMoveBuffer) + { + _traversal.CheckTraverse((uid, _xformQuery.GetComponent(uid))); + } } private float GetBroadphaseExpand(PhysicsComponent body, float frameTime) @@ -159,6 +170,15 @@ private void QueryMapBroadphase(IBroadPhase broadPhase, }, enlargedAABB, true); } + private void QueryMapSundries(DynamicTree sundriesTree, ref HashSet state, Box2 enlargedAABB) + { + sundriesTree.QueryAabb(ref state, static (ref HashSet moveBuffer, in EntityUid value) => + { + moveBuffer.Add(value); + return true; + }, enlargedAABB, true); + } + /// /// Go through every single created, moved, or touched proxy on the map and try to find any new contacts that should be created. /// From 423ca3ba1b42db7a789cfc6912f05899567bea84 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:54:18 +1000 Subject: [PATCH 160/178] Filter datadef types by attributes (#6798) datadef --- Robust.Serialization.Generator/Generator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 54f95d742db..a7911fc3343 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -42,7 +42,7 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) { IncrementalValuesProvider<(string name, string code)?> dataDefinitions = initContext.SyntaxProvider .CreateSyntaxProvider( - static (node, _) => node is TypeDeclarationSyntax, + static (node, _) => IsCandidateTypeDeclaration(node), static (context, _) => { var type = (TypeDeclarationSyntax)context.Node; @@ -78,6 +78,12 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) ); } + private static bool IsCandidateTypeDeclaration(SyntaxNode node) + { + return node is TypeDeclarationSyntax { AttributeLists.Count: > 0 } and not InterfaceDeclarationSyntax || + node is TypeDeclarationSyntax { BaseList: not null } and not InterfaceDeclarationSyntax; + } + private static (string, string)? GenerateForDataDefinition( TypeDeclarationSyntax declaration, ITypeSymbol type, From 681fa0b5d152a63e9c68df977a8ba728d5bb6d97 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:57:36 +1000 Subject: [PATCH 161/178] Test fix (#6799) --- Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs index 371134d79fc..bb7d23fbbe4 100644 --- a/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs +++ b/Robust.Shared.IntegrationTests/Physics/GridMovement_Test.cs @@ -85,7 +85,6 @@ public async Task TestMovingGridAttachesMapEntity(bool canCollide) var systems = server.ResolveDependency(); var fixtureSystem = systems.GetEntitySystem(); - var mapManager = server.ResolveDependency(); var entManager = server.ResolveDependency(); var physSystem = systems.GetEntitySystem(); var transformSystem = entManager.EntitySysManager.GetEntitySystem(); @@ -93,8 +92,8 @@ public async Task TestMovingGridAttachesMapEntity(bool canCollide) await server.WaitAssertion(() => { - entManager.System().CreateMap(out var mapId); - var grid = mapManager.CreateGridEntity(mapId); + mapSystem.CreateMap(out var mapId); + var grid = mapSystem.CreateGridEntity(mapId); mapSystem.SetTile(grid, Vector2i.Zero, new Tile(1)); var entity = entManager.SpawnEntity(null, new MapCoordinates(new Vector2(10.5f, 10.5f), mapId)); From 15a7ed295cb092fc8e7d4d4f1b34d9d61de79d33 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:16:35 +1000 Subject: [PATCH 162/178] Fix DistanceProxy.Set allocation spam (#6770) --- .../Collision/CollisionManager.Overlap.cs | 4 ++-- .../Physics/Collision/DistanceProxy.cs | 18 ++++++++---------- .../Systems/SharedPhysicsSystem.Queries.cs | 15 +++++++++------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs b/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs index bdd41857eeb..a6a816a67bf 100644 --- a/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs +++ b/Robust.Shared/Physics/Collision/CollisionManager.Overlap.cs @@ -18,8 +18,8 @@ public bool TestOverlap(T shapeA, int indexA, U shapeB, int indexB, in Tra { var input = new DistanceInput(); - input.ProxyA.Set(shapeA, indexA); - input.ProxyB.Set(shapeB, indexB); + input.ProxyA.Set(in shapeA, indexA); + input.ProxyB.Set(in shapeB, indexB); input.TransformA = xfA; input.TransformB = xfB; input.UseRadii = true; diff --git a/Robust.Shared/Physics/Collision/DistanceProxy.cs b/Robust.Shared/Physics/Collision/DistanceProxy.cs index 6f8a70b0122..07eb2d18faf 100644 --- a/Robust.Shared/Physics/Collision/DistanceProxy.cs +++ b/Robust.Shared/Physics/Collision/DistanceProxy.cs @@ -39,7 +39,7 @@ internal ref struct DistanceProxy { internal float Radius; internal ReadOnlySpan Vertices; - internal FixedArray2 Buffer; + internal FixedArray8 Buffer; // GJK using Voronoi regions (Christer Ericson) and Barycentric coordinates. @@ -54,7 +54,7 @@ internal DistanceProxy(ReadOnlySpan vertices, float radius) /// must remain in scope while the proxy is in use. /// /// The shape. - internal void Set(T shape, int index) where T : IPhysShape + internal void Set(in T shape, int index) where T : IPhysShape { switch (shape.ShapeType) { @@ -68,16 +68,14 @@ internal void Set(T shape, int index) where T : IPhysShape case ShapeType.Polygon: if (shape is Polygon poly) { - Span verts = new Vector2[poly.VertexCount]; - poly._vertices.AsSpan[..poly.VertexCount].CopyTo(verts); - Vertices = verts; + poly._vertices.AsSpan[..poly.VertexCount].CopyTo(Buffer.AsSpan); + Vertices = Buffer.AsSpan[..poly.VertexCount]; Radius = poly.Radius; } else if (shape is SlimPolygon fast) { - Span verts = new Vector2[fast.VertexCount]; - fast._vertices.AsSpan[..fast.VertexCount].CopyTo(verts); - Vertices = verts; + fast._vertices.AsSpan[..fast.VertexCount].CopyTo(Buffer.AsSpan); + Vertices = Buffer.AsSpan[..fast.VertexCount]; Radius = fast.Radius; } else @@ -95,7 +93,7 @@ internal void Set(T shape, int index) where T : IPhysShape Buffer._00 = chain.Vertices[index]; Buffer._01 = index + 1 < chain.Vertices.Length ? chain.Vertices[index + 1] : chain.Vertices[0]; - Vertices = Buffer.AsSpan; + Vertices = Buffer.AsSpan[..2]; Radius = chain.Radius; break; @@ -104,7 +102,7 @@ internal void Set(T shape, int index) where T : IPhysShape Buffer._00 = edge.Vertex1; Buffer._01 = edge.Vertex2; - Vertices = Buffer.AsSpan; + Vertices = Buffer.AsSpan[..2]; Radius = edge.Radius; break; diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs index b8ac8c333cb..b3b24b8de44 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Queries.cs @@ -553,18 +553,20 @@ public bool TryGetNearest(EntityUid uidA, EntityUid uidB, if (bodyA.Hard && !fixtureA.Hard) continue; - for (var i = 0; i < fixtureA.Shape.ChildCount; i++) + var shapeA = fixtureA.Shape; + for (var i = 0; i < shapeA.ChildCount; i++) { - input.ProxyA.Set(fixtureA.Shape, i); + input.ProxyA.Set(in shapeA, i); foreach (var fixtureB in managerB.Fixtures.Values) { if (bodyB.Hard && !fixtureB.Hard) continue; - for (var j = 0; j < fixtureB.Shape.ChildCount; j++) + var shapeB = fixtureB.Shape; + for (var j = 0; j < shapeB.ChildCount; j++) { - input.ProxyB.Set(fixtureB.Shape, j); + input.ProxyB.Set(in shapeB, j); DistanceManager.ComputeDistance(out var output, out _, input); if (distance < output.Distance) @@ -626,8 +628,9 @@ public bool TryGetNearest(EntityUid uid, MapCoordinates coordinates, DebugTools.Assert(fixtureA.ProxyCount <= 1); - input.ProxyA.Set(fixtureA.Shape, 0); - input.ProxyB.Set(pointShape, 0); + var shapeA = fixtureA.Shape; + input.ProxyA.Set(in shapeA, 0); + input.ProxyB.Set(in pointShape, 0); DistanceManager.ComputeDistance(out var output, out _, input); if (distance < output.Distance) From f44d7e00c3baa672787fa7d8c0bae21240d7f194 Mon Sep 17 00:00:00 2001 From: eoineoineoin Date: Thu, 16 Jul 2026 08:22:55 +0100 Subject: [PATCH 163/178] Defer UI operations until UI system runs frame update (#6789) --- .../Systems/SharedUserInterfaceSystem.cs | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 1d43809f1d3..7126f470e49 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -34,10 +34,21 @@ public abstract partial class SharedUserInterfaceSystem : EntitySystem private ActorRangeCheckJob _rangeJob; + /// Update type to apply + private enum QueuedUpdate + { + /// Upen a UI + Open, + /// Apply new state to the UI + ApplyState, + /// Close the UI + Close + }; + /// /// Defer BUIs during state handling so client doesn't spam a BUI constantly during prediction. /// - private readonly List<(BoundUserInterface Bui, bool value)> _queuedBuis = new(); + private readonly List<(BoundUserInterface bui, QueuedUpdate updateType)> _queuedBuis = new(); public override void Initialize() { @@ -80,9 +91,9 @@ public override void Initialize() SubscribeLocalEvent(OnActorShutdown); } - private void AddQueued(BoundUserInterface bui, bool value) + private void AddQueued(BoundUserInterface bui, QueuedUpdate type) { - _queuedBuis.Add((bui, value)); + _queuedBuis.Add((bui, type)); } /// @@ -239,7 +250,7 @@ private void CloseUiInternal(Entity ent, Enum key, Enti if (ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui)) { - AddQueued(cBui, false); + AddQueued(cBui, QueuedUpdate.Close); } if (ent.Comp.Actors.Count == 0) @@ -281,7 +292,7 @@ private void OnUserInterfaceStartup(Entity ent, ref Comp // PlayerAttachedEvent will catch some of these. foreach (var (key, bui) in ent.Comp.ClientOpenInterfaces) { - AddQueued(bui, true); + AddQueued(bui, QueuedUpdate.Open); } } @@ -301,7 +312,7 @@ protected void OnUserInterfaceShutdown(Entity ent, ref C DebugTools.Assert(!ent.Comp.Actors.ContainsKey(key)); } - DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, false)))); + DebugTools.Assert(ent.Comp.ClientOpenInterfaces.Values.All(x => _queuedBuis.Contains((x, QueuedUpdate.Close)))); } private void OnUserInterfaceGetState(Entity ent, ref ComponentGetState args) @@ -463,7 +474,7 @@ private void OnUserInterfaceHandleState(Entity ent, ref } var bui = ent.Comp.ClientOpenInterfaces[key]; - AddQueued(bui, false); + AddQueued(bui, QueuedUpdate.Close); } } @@ -490,9 +501,7 @@ private void OnUserInterfaceHandleState(Entity ent, ref if (!ent.Comp.ClientOpenInterfaces.TryGetValue(key, out var cBui) || !cBui.IsOpened) continue; - cBui.State = buiState; - cBui.UpdateState(buiState); - cBui.Update(); + AddQueued(cBui, QueuedUpdate.ApplyState); } } @@ -520,7 +529,7 @@ private void EnsureClientBui(Entity entity, Enum key, In // Existing BUI just keep it. if (entity.Comp.ClientOpenInterfaces.TryGetValue(key, out var existing)) { - _queuedBuis.Remove((existing, false)); + _queuedBuis.Remove((existing, QueuedUpdate.Close)); return; } @@ -545,7 +554,7 @@ private void EnsureClientBui(Entity entity, Enum key, In if (!open) return; - AddQueued(boundUserInterface, true); + AddQueued(boundUserInterface, QueuedUpdate.Open); } /// @@ -1102,15 +1111,18 @@ public override void Update(float frameTime) { if (_timing.IsFirstTimePredicted) { - foreach (var (bui, open) in _queuedBuis) + foreach (var (bui, updateType) in _queuedBuis) { - if (open) + if (updateType == QueuedUpdate.Open || updateType == QueuedUpdate.ApplyState) { #if EXCEPTION_TOLERANCE try { #endif - bui.Open(); + if (updateType == QueuedUpdate.Open) + { + bui.Open(); + } if (UIQuery.TryComp(bui.Owner, out var uiComp)) { From 17b66c574eb5edec31fe753c447d18f466c681d3 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:28:34 +1000 Subject: [PATCH 164/178] Fix SetUniformDirect allocation spam (#6769) --- Robust.Client/Graphics/Clyde/Clyde.Helpers.cs | 34 +++++++++++++++++++ .../Clyde/GLObjects/Clyde.ShaderProgram.cs | 17 ++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs index a71b0e827ee..7c2ea83413c 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Helpers.cs @@ -12,6 +12,8 @@ namespace Robust.Client.Graphics.Clyde { internal sealed partial class Clyde { + private unsafe delegate* unmanaged _glUniformMatrix3fv; + private void GLClearColor(Color color) { GL.ClearColor(color.R, color.G, color.B, color.A); @@ -290,5 +292,37 @@ private nint LoadGLProc(string name) return proc; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void UniformMatrix3fv(int location, in Matrix3x2 value) + { + // OpenTK's pointer overload allocates a RuntimePtr on every call in debug builds. + // Bypass the binding for this hot path until OpenTK is upgraded. + var func = _glUniformMatrix3fv; + if (func == null) + { + func = (delegate* unmanaged) LoadGLProc("glUniformMatrix3fv"); + _glUniformMatrix3fv = func; + } + + // We put the rows of the input matrix into the columns of our GPU matrices. + // This transpose is required, as in C#, we premultiply vectors with matrices + // (vM) while GL postmultiplies vectors with matrices (Mv); however, since + // Matrix3x2 is stored row-major and GL uses column-major, the memory layout + // is the same apart from Matrix3x2's implicit column. + // Assign these individually instead of using a stackalloc initializer because in debug it is allocating yipee. + float* matrix = stackalloc float[9]; + matrix[0] = value.M11; + matrix[1] = value.M12; + matrix[2] = 0; + matrix[3] = value.M21; + matrix[4] = value.M22; + matrix[5] = 0; + matrix[6] = value.M31; + matrix[7] = value.M32; + matrix[8] = 1; + + func(location, 1, 0, matrix); + } } } diff --git a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs index c70ec3734f1..2d7042981d8 100644 --- a/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs +++ b/Robust.Client/Graphics/Clyde/GLObjects/Clyde.ShaderProgram.cs @@ -258,20 +258,9 @@ public void SetUniform(int uniformName, in Matrix3x2 matrix) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private unsafe void SetUniformDirect(int slot, in Matrix3x2 value) - { - // We put the rows of the input matrix into the columns of our GPU matrices - // this transpose is required, as in C#, we premultiply vectors with matrices - // (vM) while GL postmultiplies vectors with matrices (Mv); however, since - // the Matrix3x2 data is stored row-major, and GL uses column-major, the - // memory layout is the same (or would be, if Matrix3x2 didn't have an - // implicit column) - var buf = stackalloc float[9]{ - value.M11, value.M12, 0, - value.M21, value.M22, 0, - value.M31, value.M32, 1 - }; - GL.UniformMatrix3(slot, 1, false, (float*)buf); + private void SetUniformDirect(int slot, in Matrix3x2 value) + { + _clyde.UniformMatrix3fv(slot, value); _clyde.CheckGlError(); } From 9ceb7c5b434c3d3207a568eb0babe2b022776590 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:28:57 +1000 Subject: [PATCH 165/178] Workflow standardisation + pruning (#6726) --- .github/workflows/build-all-configurations.yml | 5 +++++ .github/workflows/build-test.yml | 5 +++++ .github/workflows/test-content.yml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/.github/workflows/build-all-configurations.yml b/.github/workflows/build-all-configurations.yml index 2f209bda4ef..fc969e44672 100644 --- a/.github/workflows/build-all-configurations.yml +++ b/.github/workflows/build-all-configurations.yml @@ -4,8 +4,13 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: strategy: diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 7248d80cfd9..3a0d8a41cd0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -4,8 +4,13 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: strategy: diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index 7a43626d327..2c751ffd986 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -4,8 +4,13 @@ on: push: branches: [master] pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name != 'merge_group' }} + jobs: build: runs-on: ubuntu-latest From a60d2df5b0d5c4ef3f0c152109e2b32ec4fb2841 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:56:05 +1000 Subject: [PATCH 166/178] Cache datadef types (#6801) --- .../DataDefinition.cs | 45 ++++++++++- Robust.Serialization.Generator/Generator.cs | 78 ++++++++++++++----- Robust.Serialization.Generator/Types.cs | 12 --- 3 files changed, 102 insertions(+), 33 deletions(-) diff --git a/Robust.Serialization.Generator/DataDefinition.cs b/Robust.Serialization.Generator/DataDefinition.cs index cd851ea8702..8f02c82a46d 100644 --- a/Robust.Serialization.Generator/DataDefinition.cs +++ b/Robust.Serialization.Generator/DataDefinition.cs @@ -1,5 +1,7 @@ using Microsoft.CodeAnalysis; +using Robust.Roslyn.Shared; + namespace Robust.Serialization.Generator; public sealed record DataDefinition( @@ -8,4 +10,45 @@ public sealed record DataDefinition( List Fields, bool HasHooks, bool InvalidFields, - bool IsRecord); + bool IsRecord) +{ + private readonly Dictionary _classificationCache = + new(SymbolEqualityComparer.Default); + + private bool _firstDataDefinitionBaseTypeResolved; + private ITypeSymbol? _firstDataDefinitionBaseType; + + internal bool IsDataDefinition(ITypeSymbol? type, out bool isDataRecord) + { + if (type == null) + { + isDataRecord = false; + return false; + } + + if (!_classificationCache.TryGetValue(type, out var result)) + { + result = (DataDefinitionHelper.IsDataDefinition(type, out var record), record); + _classificationCache.Add(type, result); + } + + isDataRecord = result.Record; + return result.Definition; + } + + internal ITypeSymbol? GetFirstDataDefinitionBaseType() + { + if (_firstDataDefinitionBaseTypeResolved) + return _firstDataDefinitionBaseType; + + _firstDataDefinitionBaseTypeResolved = true; + var parent = Type; + while ((parent = parent.BaseType) != null) + { + if (IsDataDefinition(parent, out _)) + return _firstDataDefinitionBaseType = parent; + } + + return null; + } +} diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index a7911fc3343..5348dcca353 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -43,18 +43,20 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) IncrementalValuesProvider<(string name, string code)?> dataDefinitions = initContext.SyntaxProvider .CreateSyntaxProvider( static (node, _) => IsCandidateTypeDeclaration(node), - static (context, _) => + static (context, cancellationToken) => { var type = (TypeDeclarationSyntax)context.Node; - var symbol = (ITypeSymbol)context.SemanticModel.GetDeclaredSymbol(type)!; + if (context.SemanticModel.GetDeclaredSymbol(type, cancellationToken) is not INamedTypeSymbol symbol) + return null; if (symbol.TypeKind == TypeKind.Interface || + !IsCanonicalCandidateDeclaration(type, symbol, cancellationToken) || !IsDataDefinition(symbol, out var isDataRecord)) { return null; } - return GenerateForDataDefinition(type, symbol, isDataRecord); + return GenerateForDataDefinition(type, symbol, isDataRecord, cancellationToken); } ) .Where(static type => type != null); @@ -84,10 +86,46 @@ private static bool IsCandidateTypeDeclaration(SyntaxNode node) node is TypeDeclarationSyntax { BaseList: not null } and not InterfaceDeclarationSyntax; } + private static bool IsCanonicalCandidateDeclaration( + TypeDeclarationSyntax declaration, + INamedTypeSymbol symbol, + CancellationToken cancellationToken) + { + TypeDeclarationSyntax? canonical = null; + + foreach (var reference in symbol.DeclaringSyntaxReferences) + { + if (reference.GetSyntax(cancellationToken) is not TypeDeclarationSyntax syntax || + !IsCandidateTypeDeclaration(syntax)) + { + continue; + } + + if (canonical == null || CompareDeclarations(syntax, canonical) < 0) + canonical = syntax; + } + + return canonical == declaration; + } + + private static int CompareDeclarations(TypeDeclarationSyntax left, TypeDeclarationSyntax right) + { + var pathComparison = string.Compare( + left.SyntaxTree.FilePath, + right.SyntaxTree.FilePath, + StringComparison.Ordinal); + + if (pathComparison != 0) + return pathComparison; + + return left.SpanStart.CompareTo(right.SpanStart); + } + private static (string, string)? GenerateForDataDefinition( TypeDeclarationSyntax declaration, - ITypeSymbol type, - bool isDataRecord) + INamedTypeSymbol type, + bool isDataRecord, + CancellationToken cancellationToken) { var builder = new StringBuilder(); var containingTypes = new Stack(); @@ -115,7 +153,7 @@ private static (string, string)? GenerateForDataDefinition( var containingTypesEnd = new StringBuilder(); foreach (var parent in containingTypes) { - var syntax = (TypeDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(); + var syntax = (TypeDeclarationSyntax)parent.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken); if (!IsPartial(syntax)) { nonPartial = true; @@ -339,11 +377,11 @@ private static string GetConstructors(DataDefinition definition) ? "private" : "protected"; - var copyBaseCall = IsDataDefinition(definition.Type.BaseType, out _) + var copyBaseCall = definition.IsDataDefinition(definition.Type.BaseType, out _) ? "base(ISerializationGeneratedCopy, source, serialization, hookCtx, context)" : "this()"; - var readBaseCall = IsDataDefinition(definition.Type.BaseType, out _) + var readBaseCall = definition.IsDataDefinition(definition.Type.BaseType, out _) ? "base(ISerializationGeneratedRead, mappingDataNode, serialization, hookCtx, context)" : "this()"; @@ -486,7 +524,7 @@ private static string GetReadBody(DataDefinition definition, string targetPrefix method = $"ReadEnum<{fieldTypeName}>"; nullableString = string.Empty; } - else if (field.Type.IsValueType && IsDataDefinition(field.Type, out _)) + else if (field.Type.IsValueType && definition.IsDataDefinition(field.Type, out _)) { method = $"ReadStructDefinition<{fieldTypeName}>"; nullableString = string.Empty; @@ -508,7 +546,7 @@ private static string GetReadBody(DataDefinition definition, string targetPrefix nullableString = string.Empty; } } - else if (IsDataDefinition(field.Type, out _) && field.Type.TypeKind != TypeKind.Interface) + else if (definition.IsDataDefinition(field.Type, out _) && field.Type.TypeKind != TypeKind.Interface) { method = $"ReadDefinition<{fieldTypeName}>"; } @@ -588,7 +626,7 @@ private static string GetInstantiators(DataDefinition definition) var builder = new StringBuilder(); var modifiers = string.Empty; - if (GetFirstDataDefinitionBaseType(definition.Type) != null) + if (definition.GetFirstDataDefinitionBaseType() != null) modifiers = "override "; else if (IsVirtualClass(definition.Type)) modifiers = "virtual "; @@ -701,7 +739,7 @@ private static string GetValidator(DataDefinition definition) builder.AppendLine("}"); } - if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) builder.AppendLine($"{baseType.ToDisplayString()}.Validate(nodes, node, serialization, context);"); return $$""" @@ -721,7 +759,7 @@ private static string GetCopiers(DataDefinition definition) var baseDefinition = false; while (baseType != null) { - if (!baseDefinition && IsDataDefinition(baseType, out _)) + if (!baseDefinition && definition.IsDataDefinition(baseType, out _)) baseDefinition = true; GetCopierMethod(definition, baseType, baseType.ToDisplayString(), true, builder, requiredFields); @@ -741,7 +779,7 @@ private static void GetCopierMethod( StringBuilder builder, string requiredFields) { - if (!IsDataDefinition(type, out _)) + if (!definition.IsDataDefinition(type, out _)) return; var sameType = definition.Type.Equals(type, SymbolEqualityComparer.Default) && @@ -805,7 +843,7 @@ private static void GetCopierMethod( } else { - var baseCopy = IsDataDefinition(definition.Type.BaseType, out _) + var baseCopy = definition.IsDataDefinition(definition.Type.BaseType, out _) ? $$""" var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; base.Copy(ref definitionCast, serialization, hookCtx, context); @@ -851,7 +889,7 @@ private static string GetReader(DataDefinition definition) string body; if (definition.Type.IsAbstract) { - var baseRead = IsDataDefinition(definition.Type.BaseType, out _) + var baseRead = definition.IsDataDefinition(definition.Type.BaseType, out _) ? $$""" var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); @@ -881,7 +919,7 @@ private static string GetReader(DataDefinition definition) } else { - var baseRead = IsDataDefinition(definition.Type.BaseType, out _) + var baseRead = definition.IsDataDefinition(definition.Type.BaseType, out _) ? $$""" var definitionCast = ({{definition.Type.BaseType!.ToDisplayString()}})target; {{definition.Type.BaseType!.ToDisplayString()}}.Read(ref definitionCast, mappingDataNode, serialization, hookCtx, context); @@ -1042,7 +1080,7 @@ private static string GetWriter(DataDefinition definition) } - if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) { var baseTypeName = baseType.ToDisplayString(); builder.AppendLine( @@ -1102,7 +1140,7 @@ private static string GetFieldDefinitions(DataDefinition definition) fieldTags.Add($"\"{field.Attribute.Tag}\""); } - if (GetFirstDataDefinitionBaseType(definition.Type) is { } baseType) + if (definition.GetFirstDataDefinitionBaseType() is { } baseType) builder.AppendLine( $"{baseType.ToDisplayString()}.GetFieldDefinitions(instance, fields, [{string.Join(", ", fieldTags)}]);"); @@ -1230,7 +1268,7 @@ private static StringBuilder GetCopyBody(DataDefinition definition, string targe { """); - if (IsDataDefinition(type, out _) && !type.IsAbstract && + if (definition.IsDataDefinition(type, out _) && !type.IsAbstract && type is not INamedTypeSymbol { TypeKind: TypeKind.Interface }) { var nullable = !type.IsValueType || IsNullableType(type); diff --git a/Robust.Serialization.Generator/Types.cs b/Robust.Serialization.Generator/Types.cs index e029835a333..2a0da9c32c0 100644 --- a/Robust.Serialization.Generator/Types.cs +++ b/Robust.Serialization.Generator/Types.cs @@ -403,18 +403,6 @@ internal static string GetSetsRequiredAttributeOrEmpty(ITypeSymbol type) return setsRequired; } - internal static ITypeSymbol? GetFirstDataDefinitionBaseType(ITypeSymbol type) - { - var parent = type; - while ((parent = parent.BaseType) != null) - { - if (IsDataDefinition(parent, out _)) - return parent; - } - - return null; - } - internal static IEnumerable<(ISymbol Field, ITypeSymbol Type, DataFieldAttribute Attribute)> GetAllDataFields(ITypeSymbol? definition, bool isDataRecord) { while (definition != null) From 9e1c568cdb380bd290dec3abf3d68e8f775f63d4 Mon Sep 17 00:00:00 2001 From: mirrorcult <19853115+mirrorcult@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:56:33 +0000 Subject: [PATCH 167/178] Allow content to read actual auxiliary off of `AudioAuxiliaryComponent` (#6800) --- Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs b/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs index a1648d655b5..b02017926e1 100644 --- a/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs +++ b/Robust.Shared/Audio/Components/AudioAuxiliaryComponent.cs @@ -20,5 +20,5 @@ public sealed partial class AudioAuxiliaryComponent : Component public EntityUid? Effect; [ViewVariables] - internal IAuxiliaryAudio Auxiliary = new DummyAuxiliaryAudio(); + public IAuxiliaryAudio Auxiliary = new DummyAuxiliaryAudio(); } From 651e9034dd6e0dc107dd9c7352bdf3c89d382236 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:14:32 +1000 Subject: [PATCH 168/178] Cache grafana histograms (#6767) --- .../GameObjects/ClientEntityManager.cs | 15 +++++++++- .../GameObjects/ServerEntityManager.cs | 15 +++++++++- Robust.Shared/GameObjects/EntityManager.cs | 28 ++++++++++++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Robust.Client/GameObjects/ClientEntityManager.cs b/Robust.Client/GameObjects/ClientEntityManager.cs index ff9c38ba3de..4d65c2efefc 100644 --- a/Robust.Client/GameObjects/ClientEntityManager.cs +++ b/Robust.Client/GameObjects/ClientEntityManager.cs @@ -31,6 +31,8 @@ public sealed partial class ClientEntityManager : EntityManager, IClientEntityMa private readonly Queue _queuedPredictedDeletions = new(); private readonly HashSet _queuedPredictedDeletionsSet = new(); + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entityNetHistogram; public override void Initialize() { @@ -206,7 +208,9 @@ public void SetupNetworking() public override void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntityNet").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entityNetHistogram?.NewTimer()) { while (_queue.Count != 0 && _queue.Peek().msg.SourceTick <= _gameTiming.LastRealTick) { @@ -219,6 +223,15 @@ public override void TickUpdate(float frameTime, bool noPredictions, Histogram? base.TickUpdate(frameTime, noPredictions, histogram); } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entityNetHistogram = histogram?.WithLabels("EntityNet"); + } + internal override void ProcessQueueudDeletions() { base.ProcessQueueudDeletions(); diff --git a/Robust.Server/GameObjects/ServerEntityManager.cs b/Robust.Server/GameObjects/ServerEntityManager.cs index 9721093413d..ad138596d8d 100644 --- a/Robust.Server/GameObjects/ServerEntityManager.cs +++ b/Robust.Server/GameObjects/ServerEntityManager.cs @@ -44,6 +44,8 @@ public sealed partial class ServerEntityManager : EntityManager, IServerEntityMa private ISawmill _netEntSawmill = default!; private PvsSystem _pvs = default!; + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entityNetHistogram; public override void Initialize() { @@ -159,7 +161,9 @@ public void SetupNetworking() /// public override void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntityNet").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entityNetHistogram?.NewTimer()) { while (_queue.Count != 0 && _queue.Peek().SourceTick <= _gameTiming.CurTick) { @@ -172,6 +176,15 @@ public override void TickUpdate(float frameTime, bool noPredictions, Histogram? EntitiesCount.Set(Entities.Count); } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entityNetHistogram = histogram?.WithLabels("EntityNet"); + } + public uint GetLastMessageSequence(ICommonSession? session) { return session == null ? default : _lastProcessedSequencesCmd.GetValueOrDefault(session); diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index dec2756581a..4e99477aec5 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -72,6 +72,12 @@ public abstract partial class EntityManager : IEntityManager protected readonly Queue QueuedDeletions = new(); protected readonly HashSet QueuedDeletionsSet = new(); + private Histogram? _tickUpdateHistogram; + private Histogram.Child? _entitySystemsHistogram; + private Histogram.Child? _entityEventBusHistogram; + private Histogram.Child? _queuedDeletionHistogram; + private Histogram.Child? _componentCullHistogram; + private EntityDiffContext _context = new(); /// @@ -272,31 +278,45 @@ public virtual void Cleanup() public virtual void TickUpdate(float frameTime, bool noPredictions, Histogram? histogram) { - using (histogram?.WithLabels("EntitySystems").NewTimer()) + UpdateTickHistogram(histogram); + + using (_entitySystemsHistogram?.NewTimer()) using (_prof.Group("Systems")) { _entitySystemManager.TickUpdate(frameTime, noPredictions); } - using (histogram?.WithLabels("EntityEventBus").NewTimer()) + using (_entityEventBusHistogram?.NewTimer()) using (_prof.Group("Events")) { EventBusInternal.ProcessEventQueue(); } - using (histogram?.WithLabels("QueuedDeletion").NewTimer()) + using (_queuedDeletionHistogram?.NewTimer()) using (_prof.Group("QueueDel")) { ProcessQueueudDeletions(); } - using (histogram?.WithLabels("ComponentCull").NewTimer()) + using (_componentCullHistogram?.NewTimer()) using (_prof.Group("ComponentCull")) { CullRemovedComponents(); } } + private void UpdateTickHistogram(Histogram? histogram) + { + if (ReferenceEquals(_tickUpdateHistogram, histogram)) + return; + + _tickUpdateHistogram = histogram; + _entitySystemsHistogram = histogram?.WithLabels("EntitySystems"); + _entityEventBusHistogram = histogram?.WithLabels("EntityEventBus"); + _queuedDeletionHistogram = histogram?.WithLabels("QueuedDeletion"); + _componentCullHistogram = histogram?.WithLabels("ComponentCull"); + } + internal virtual void ProcessQueueudDeletions() { while (QueuedDeletions.TryDequeue(out var uid)) From a978e33560c15ef52705b41a977fe85b256d5ce3 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:14:41 +1000 Subject: [PATCH 169/178] Cache entity system profile names (#6765) --- .../GameObjects/EntitySystemManager.cs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/Robust.Shared/GameObjects/EntitySystemManager.cs b/Robust.Shared/GameObjects/EntitySystemManager.cs index 05cb5708ba4..edae00b15c4 100644 --- a/Robust.Shared/GameObjects/EntitySystemManager.cs +++ b/Robust.Shared/GameObjects/EntitySystemManager.cs @@ -65,7 +65,7 @@ public IDependencyCollection DependencyCollection private bool _initialized; [ViewVariables] private UpdateReg[] _updateOrder = Array.Empty(); - [ViewVariables] private IEntitySystem[] _frameUpdateOrder = Array.Empty(); + [ViewVariables] private FrameUpdateReg[] _frameUpdateOrder = Array.Empty(); public bool MetricsEnabled { get; set; } @@ -221,12 +221,24 @@ public void Initialize(bool discover = true) // Create update order for entity systems. var (fUpdate, update) = CalculateUpdateOrder(_systemTypes, subTypes, SystemDependencyCollection); - _frameUpdateOrder = fUpdate.ToArray(); _updateOrder = update - .Select(s => new UpdateReg + .Select(s => + { + var profileName = s.GetType().Name; + return new UpdateReg + { + System = s, + ProfileName = profileName, + Monitor = _tickUsageHistogram.WithLabels(profileName) + }; + }) + .ToArray(); + + _frameUpdateOrder = fUpdate + .Select(s => new FrameUpdateReg { System = s, - Monitor = _tickUsageHistogram.WithLabels(s.GetType().Name) + ProfileName = s.GetType().Name }) .ToArray(); @@ -310,7 +322,7 @@ public void Clear() _extraLoadedTypes.Clear(); _systemTypes.Clear(); _updateOrder = Array.Empty(); - _frameUpdateOrder = Array.Empty(); + _frameUpdateOrder = Array.Empty(); _initialized = false; SystemDependencyCollection?.Clear(); } @@ -331,7 +343,7 @@ public void TickUpdate(float frameTime, bool noPredictions) try { #endif - using (_profManager.Value(updReg.System.GetType().Name)) + using (_profManager.Value(updReg.ProfileName)) { updReg.System.Update(frameTime); } @@ -353,15 +365,15 @@ public void TickUpdate(float frameTime, bool noPredictions) /// public void FrameUpdate(float frameTime) { - foreach (var system in _frameUpdateOrder) + foreach (var updReg in _frameUpdateOrder) { #if EXCEPTION_TOLERANCE try { #endif - using (_profManager.Value(system.GetType().Name)) + using (_profManager.Value(updReg.ProfileName)) { - system.FrameUpdate(frameTime); + updReg.System.FrameUpdate(frameTime); } #if EXCEPTION_TOLERANCE } @@ -429,12 +441,24 @@ private static bool NeedsFrameUpdate(Type type) return mFrameUpdate!.DeclaringType != typeof(EntitySystem); } - internal IEnumerable FrameUpdateOrder => _frameUpdateOrder.Select(c => c.GetType()); + internal IEnumerable FrameUpdateOrder => _frameUpdateOrder.Select(c => c.System.GetType()); internal IEnumerable TickUpdateOrder => _updateOrder.Select(c => c.System.GetType()); + private struct FrameUpdateReg + { + [ViewVariables] public IEntitySystem System; + [ViewVariables] public string ProfileName; + + public override string? ToString() + { + return System.ToString(); + } + } + private struct UpdateReg { [ViewVariables] public IEntitySystem System; + [ViewVariables] public string ProfileName; [ViewVariables] public Histogram.Child Monitor; public override string? ToString() From 36969713a3be971ee74d52b4e518da949846a095 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:29:07 +1000 Subject: [PATCH 170/178] Fix physics hull allocs (#6802) --- Robust.Shared/Physics/Collision/InternalPhysicsHull.cs | 7 +++---- Robust.Shared/Physics/PhysicsHull.cs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs b/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs index c28614d499d..13f1bde246a 100644 --- a/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs +++ b/Robust.Shared/Physics/Collision/InternalPhysicsHull.cs @@ -10,7 +10,9 @@ namespace Robust.Shared.Physics; /// internal ref struct InternalPhysicsHull { - public Span Points; + private FixedArray8 _points; + + public Span Points => field.IsEmpty ? _points.AsSpan : field; public int Count; internal InternalPhysicsHull(Span vertices, int count) : this() @@ -66,7 +68,6 @@ private static InternalPhysicsHull RecurseHull(Vector2 p1, Vector2 p2, Span points, int return hull; } - hull.Points = new Vector2[PhysicsConstants.MaxPolygonVertices]; - // stitch hulls together, preserving CCW winding order hull.Points[hull.Count++] = p1; diff --git a/Robust.Shared/Physics/PhysicsHull.cs b/Robust.Shared/Physics/PhysicsHull.cs index 2145576c19a..aa66d6daa87 100644 --- a/Robust.Shared/Physics/PhysicsHull.cs +++ b/Robust.Shared/Physics/PhysicsHull.cs @@ -8,6 +8,6 @@ public struct PhysicsHull public static Span ComputePoints(ReadOnlySpan points, int count) { var hull = InternalPhysicsHull.ComputeHull(points, count); - return hull.Points; + return hull.Count == 0 ? Span.Empty : hull.Points.ToArray(); } } From ab4db16d08ccefe06354b447c3c59ec835b67b14 Mon Sep 17 00:00:00 2001 From: Illiux Date: Thu, 16 Jul 2026 02:32:20 -0700 Subject: [PATCH 171/178] Fix calling Dirty+DiryField on same tick (#6685) Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- .../ComponentNetworkGenerator.cs | 15 +- .../GameState/AutoNetworkingTest.cs | 284 +++++++++++++----- .../UserInterface/UserInterfaceComponent.cs | 4 +- .../EntityManager.ComponentDeltas.cs | 44 +-- .../GameObjects/EntityManager.Components.cs | 1 + Robust.Shared/GameObjects/EntityManager.cs | 67 +++-- Robust.Shared/GameObjects/IEntityManager.cs | 3 - .../Systems/SharedUserInterfaceSystem.cs | 41 ++- .../Components/PhysicsComponent.Physics.cs | 4 +- .../Systems/SharedPhysicsSystem.Components.cs | 6 +- 10 files changed, 316 insertions(+), 153 deletions(-) diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index 6f31dc6d4c7..52780ea5875 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -526,13 +526,15 @@ public void ApplyToFullState({stateName} fullState) deltaNetRegister = $@"EntityManager.ComponentFactory.RegisterNetworkedFields<{classSymbol}>({fieldsStr});"; deltaGetState = @$"// Delta state - if (component is IComponentDelta delta && args.FromTick > component.CreationTick && delta.LastFieldUpdate >= args.FromTick) + if (component is IComponentDelta delta) {{ - var fields = EntityManager.GetModifiedFields(component, args.FromTick); + var aspects = EntityManager.GetModifiedAspects(component, args.FromTick); // Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state. - switch (fields) - {{{deltaGetFields} + switch (aspects) + {{ + case >= DeltaAspect.Unclassified: + break;{deltaGetFields} default: break; }} @@ -541,10 +543,9 @@ public void ApplyToFullState({stateName} fullState) deltaInterface = " : IComponentDelta"; deltaCompFields = @$"/// - public GameTick LastFieldUpdate {{ get; set; }} = GameTick.Zero; - + public GameTick LastUnclassifiedDirty {{ get; set; }} /// - public GameTick[] LastModifiedFields {{ get; set; }} = Array.Empty();"; + public GameTick[] LastModifiedFields {{ get; set; }}"; } string handleState; diff --git a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs index 56845abec38..e0a7a5fd075 100644 --- a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs @@ -1,5 +1,3 @@ -using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared; using Robust.Shared.Analyzers; @@ -8,6 +6,7 @@ using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Timing; namespace Robust.UnitTesting.Shared.GameState; @@ -16,7 +15,6 @@ internal sealed partial class AutoNetworkingTests : RobustIntegrationTest /// /// Does basic testing for AutoNetworkedFieldAttribute and AutoGenerateComponentStateAttribute /// to make sure the datafields are correctly networked to the client when dirtied. - /// TODO: Add test for field deltas. /// [Test] public async Task AutoNetworkingTest() @@ -42,16 +40,28 @@ public async Task AutoNetworkingTest() await RunTicks(); - // Spawn entities + // Spawn entities. var coords = new EntityCoordinates(map, default); EntityUid player = default; EntityUid cPlayer = default; + EntityUid clientEnt1 = default; + EntityUid clientEnt2 = default; + EntityUid clientEnt3 = default; + EntityUid clientEnt4 = default; + EntityUid clientEnt5 = default; + EntityUid clientEnt6 = default; EntityUid serverEnt1 = default; EntityUid serverEnt2 = default; EntityUid serverEnt3 = default; - NetEntity serverNet1 = default; - NetEntity serverNet2 = default; - NetEntity serverNet3 = default; + EntityUid serverEnt4 = default; + EntityUid serverEnt5 = default; + EntityUid serverEnt6 = default; + NetEntity netEnt1 = default; + NetEntity netEnt2 = default; + NetEntity netEnt3 = default; + NetEntity netEnt4 = default; + NetEntity netEnt5 = default; + NetEntity netEnt6 = default; await server.WaitPost(() => { @@ -65,101 +75,161 @@ await server.WaitPost(() => serverEnt1 = server.EntMan.SpawnAttachedTo(null, coords); serverEnt2 = server.EntMan.SpawnAttachedTo(null, coords); serverEnt3 = server.EntMan.SpawnAttachedTo(null, coords); - serverNet1 = server.EntMan.GetNetEntity(serverEnt1); - serverNet2 = server.EntMan.GetNetEntity(serverEnt2); - serverNet3 = server.EntMan.GetNetEntity(serverEnt3); - - // Setup components + serverEnt4 = server.EntMan.SpawnAttachedTo(null, coords); + serverEnt5 = server.EntMan.SpawnAttachedTo(null, coords); + serverEnt6 = server.EntMan.SpawnAttachedTo(null, coords); + netEnt1 = server.EntMan.GetNetEntity(serverEnt1); + netEnt2 = server.EntMan.GetNetEntity(serverEnt2); + netEnt3 = server.EntMan.GetNetEntity(serverEnt3); + netEnt4 = server.EntMan.GetNetEntity(serverEnt4); + netEnt5 = server.EntMan.GetNetEntity(serverEnt5); + netEnt6 = server.EntMan.GetNetEntity(serverEnt6); + + // Setup components. server.EntMan.EnsureComponent(serverEnt1); server.EntMan.EnsureComponent(serverEnt2); server.EntMan.EnsureComponent(serverEnt3); + server.EntMan.EnsureComponent(serverEnt4); + server.EntMan.EnsureComponent(serverEnt5); + server.EntMan.EnsureComponent(serverEnt6); }); await RunTicks(); - // check client + // Check client. await client.WaitPost(() => { - // Get the client-side entities + // Get the client-side entities. cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); - var clientEnt1 = client.EntMan.GetEntity(serverNet1); - var clientEnt2 = client.EntMan.GetEntity(serverNet2); - var clientEnt3 = client.EntMan.GetEntity(serverNet3); - - // Check player got properly attached + clientEnt1 = client.EntMan.GetEntity(netEnt1); + clientEnt2 = client.EntMan.GetEntity(netEnt2); + clientEnt3 = client.EntMan.GetEntity(netEnt3); + clientEnt4 = client.EntMan.GetEntity(netEnt4); + clientEnt5 = client.EntMan.GetEntity(netEnt5); + clientEnt6 = client.EntMan.GetEntity(netEnt6); + + // Check that the player got properly attached. Assert.That(client.AttachedEntity, Is.EqualTo(cPlayer)); Assert.That(client.EntMan.EntityExists(cPlayer)); - // Get the client-side components - Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1)); - Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2)); - Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3)); - - // All datafields should be the default value - Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(1)); - Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2)); - - Assert.That(cmpClient2?.ChildNetworked, Is.EqualTo(1)); - Assert.That(cmpClient2?.Child, Is.EqualTo(2)); - Assert.That(cmpClient2?.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpClient2?.Parent, Is.EqualTo(4)); - - Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpClient3?.Parent, Is.EqualTo(4)); + // Get the client-side components. + Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? clientComp1)); + Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? clientComp2)); + Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? clientComp3)); + Assert.That(client.EntMan.TryGetComponent(clientEnt4, out AutoNetworkingTestFieldDeltaComponent? clientComp4)); + Assert.That(client.EntMan.TryGetComponent(clientEnt5, out AutoNetworkingTestFieldDeltaComponent? clientComp5)); + Assert.That(client.EntMan.TryGetComponent(clientEnt6, out AutoNetworkingTestFieldDeltaComponent? clientComp6)); + + // All datafields should be the default value. + Assert.That(clientComp1?.IsNetworked, Is.EqualTo(1)); + Assert.That(clientComp1?.NotNetworked, Is.EqualTo(2)); + + Assert.That(clientComp2?.ChildNetworked, Is.EqualTo(1)); + Assert.That(clientComp2?.Child, Is.EqualTo(2)); + Assert.That(clientComp2?.ParentNetworked, Is.EqualTo(3)); + Assert.That(clientComp2?.Parent, Is.EqualTo(4)); + + Assert.That(clientComp3?.ParentNetworked, Is.EqualTo(3)); + Assert.That(clientComp3?.Parent, Is.EqualTo(4)); + + Assert.That(clientComp4?.Field1, Is.EqualTo(1)); + Assert.That(clientComp4?.Field2, Is.EqualTo(2)); + Assert.That(clientComp4?.Field3, Is.EqualTo(3)); + + Assert.That(clientComp5?.Field1, Is.EqualTo(1)); + Assert.That(clientComp5?.Field2, Is.EqualTo(2)); + Assert.That(clientComp5?.Field3, Is.EqualTo(3)); + + Assert.That(clientComp6?.Field1, Is.EqualTo(1)); + Assert.That(clientComp6?.Field2, Is.EqualTo(2)); + Assert.That(clientComp6?.Field3, Is.EqualTo(3)); }); - // make changes on the server + // Make changes on the server. await server.WaitPost(() => { - // Get the server-side components - var cmpServer1 = server.EntMan.GetComponent(serverEnt1); - var cmpServer2 = server.EntMan.GetComponent(serverEnt2); - var cmpServer3 = server.EntMan.GetComponent(serverEnt3); + // Get the server-side components. + var serverComp1 = server.EntMan.GetComponent(serverEnt1); + var serverComp2 = server.EntMan.GetComponent(serverEnt2); + var serverComp3 = server.EntMan.GetComponent(serverEnt3); + var serverComp4 = server.EntMan.GetComponent(serverEnt4); + var serverComp5 = server.EntMan.GetComponent(serverEnt5); + var serverComp6 = server.EntMan.GetComponent(serverEnt6); // All datafields should be the default value - Assert.That(cmpServer1.IsNetworked, Is.EqualTo(1)); - Assert.That(cmpServer1.NotNetworked, Is.EqualTo(2)); - - Assert.That(cmpServer2.ChildNetworked, Is.EqualTo(1)); - Assert.That(cmpServer2.Child, Is.EqualTo(2)); - Assert.That(cmpServer2.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpServer2.Parent, Is.EqualTo(4)); - - Assert.That(cmpServer3.ParentNetworked, Is.EqualTo(3)); - Assert.That(cmpServer3.Parent, Is.EqualTo(4)); - - // change the datafields and dirty them - cmpServer1.IsNetworked = 101; - cmpServer1.NotNetworked = 102; - cmpServer2.ChildNetworked = 101; - cmpServer2.Child = 102; - cmpServer2.ParentNetworked = 103; - cmpServer2.Parent = 104; - cmpServer3.ParentNetworked = 103; - cmpServer3.Parent = 104; - - server.EntMan.Dirty(serverEnt1, cmpServer1); - server.EntMan.Dirty(serverEnt2, cmpServer2); - server.EntMan.Dirty(serverEnt3, cmpServer3); + Assert.That(serverComp1.IsNetworked, Is.EqualTo(1)); + Assert.That(serverComp1.NotNetworked, Is.EqualTo(2)); + + Assert.That(serverComp2.ChildNetworked, Is.EqualTo(1)); + Assert.That(serverComp2.Child, Is.EqualTo(2)); + Assert.That(serverComp2.ParentNetworked, Is.EqualTo(3)); + Assert.That(serverComp2.Parent, Is.EqualTo(4)); + + Assert.That(serverComp3.ParentNetworked, Is.EqualTo(3)); + Assert.That(serverComp3.Parent, Is.EqualTo(4)); + + Assert.That(serverComp4?.Field1, Is.EqualTo(1)); + Assert.That(serverComp4?.Field2, Is.EqualTo(2)); + Assert.That(serverComp4?.Field3, Is.EqualTo(3)); + + Assert.That(serverComp5?.Field1, Is.EqualTo(1)); + Assert.That(serverComp5?.Field2, Is.EqualTo(2)); + Assert.That(serverComp5?.Field3, Is.EqualTo(3)); + + Assert.That(serverComp6?.Field1, Is.EqualTo(1)); + Assert.That(serverComp6?.Field2, Is.EqualTo(2)); + Assert.That(serverComp6?.Field3, Is.EqualTo(3)); + + // Test that a field with AutoNetworkedField gets networked. + serverComp1.IsNetworked = 101; + // Test that a field without AutoNetworkedField does not get networked. + serverComp1.NotNetworked = 102; + server.EntMan.Dirty(serverEnt1, serverComp1); + // Test that inherited autonetworked fields get networked. + serverComp2.ChildNetworked = 101; + serverComp2.Child = 102; + serverComp2.ParentNetworked = 103; + serverComp2.Parent = 104; + server.EntMan.Dirty(serverEnt2, serverComp2); + serverComp3.ParentNetworked = 103; + serverComp3.Parent = 104; + server.EntMan.Dirty(serverEnt3, serverComp3); + + // Use an autogenerated delta state to only network a single field. + serverComp4.Field1 = 101; + serverComp4.Field2 = 102; + serverComp4.Field3 = 103; + server.EntMan.DirtyField(serverEnt4, serverComp4, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + + // Check that calling both Dirty and then DirtyField will send a full state. + serverComp5.Field1 = 101; + serverComp5.Field2 = 102; + serverComp5.Field3 = 103; + server.EntMan.DirtyField(serverEnt5, serverComp5, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + server.EntMan.Dirty(serverEnt5, serverComp5); + + // Check that calling both DirtyField and then Dirty will send a full state. + serverComp6.Field1 = 101; + serverComp6.Field2 = 102; + serverComp6.Field3 = 103; + server.EntMan.Dirty(serverEnt6, serverComp6); + server.EntMan.DirtyField(serverEnt6, serverComp6, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); }); await RunTicks(); - // check the client again + // Check the client again. await client.WaitPost(() => { - // Get the client-side entities - cPlayer = client.EntMan.GetEntity(server.EntMan.GetNetEntity(player)); - var clientEnt1 = client.EntMan.GetEntity(serverNet1); - var clientEnt2 = client.EntMan.GetEntity(serverNet2); - var clientEnt3 = client.EntMan.GetEntity(serverNet3); - - // Get the client-side components + // Get the client-side components. Assert.That(client.EntMan.TryGetComponent(clientEnt1, out AutoNetworkingTestComponent? cmpClient1)); Assert.That(client.EntMan.TryGetComponent(clientEnt2, out AutoNetworkingTestChildComponent? cmpClient2)); Assert.That(client.EntMan.TryGetComponent(clientEnt3, out AutoNetworkingTestEmptyChildComponent? cmpClient3)); + Assert.That(client.EntMan.TryGetComponent(clientEnt4, out AutoNetworkingTestFieldDeltaComponent? cmpClient4)); + Assert.That(client.EntMan.TryGetComponent(clientEnt5, out AutoNetworkingTestFieldDeltaComponent? cmpClient5)); + Assert.That(client.EntMan.TryGetComponent(clientEnt6, out AutoNetworkingTestFieldDeltaComponent? cmpClient6)); - // All datafields should be the default value + // Check that the networked datafields have changed. Assert.That(cmpClient1?.IsNetworked, Is.EqualTo(101)); Assert.That(cmpClient1?.NotNetworked, Is.EqualTo(2)); // unchanged @@ -170,6 +240,18 @@ await client.WaitPost(() => Assert.That(cmpClient3?.ParentNetworked, Is.EqualTo(103)); Assert.That(cmpClient3?.Parent, Is.EqualTo(4)); // unchanged + + Assert.That(cmpClient4?.Field1, Is.EqualTo(1)); // not dirtied + Assert.That(cmpClient4?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient4?.Field3, Is.EqualTo(103)); // changed from delta state + + Assert.That(cmpClient5?.Field1, Is.EqualTo(101)); // changed from full state + Assert.That(cmpClient5?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient5?.Field3, Is.EqualTo(103)); // changed from full state + + Assert.That(cmpClient6?.Field1, Is.EqualTo(101)); // changed from full state + Assert.That(cmpClient6?.Field2, Is.EqualTo(2)); // not networked + Assert.That(cmpClient6?.Field3, Is.EqualTo(103)); // changed from full state }); async Task RunTicks() @@ -185,6 +267,49 @@ async Task RunTicks() await server.WaitRunTicks(5); await client.WaitRunTicks(5); } + + /// + /// Tests that field-delta generation treats the from-tick as an already acknowledged state boundary. + /// A field dirtied exactly on the from-tick should not be included in the next delta mask. + /// + [Test] + public async Task AutoNetworkingFieldDeltaFromTickBoundaryTest() + { + using var server = StartServer(); + + await server.WaitIdleAsync(); + + EntityUid entity = default; + await server.WaitPost(() => + { + entity = server.EntMan.Spawn(); + server.EntMan.EnsureComponent(entity); + }); + + await server.WaitRunTicks(1); + + var acknowledgedTick = GameTick.Zero; + await server.WaitPost(() => + { + var component = server.EntMan.GetComponent(entity); + component.Field1 = 101; + server.EntMan.DirtyField(entity, component, nameof(AutoNetworkingTestFieldDeltaComponent.Field1)); + acknowledgedTick = server.Timing.CurTick; + }); + + await server.WaitRunTicks(1); + + IComponentState? state = null; + await server.WaitPost(() => + { + var component = server.EntMan.GetComponent(entity); + component.Field3 = 303; + server.EntMan.DirtyField(entity, component, nameof(AutoNetworkingTestFieldDeltaComponent.Field3)); + state = server.EntMan.GetComponentState(server.EntMan.EventBus, component, null, acknowledgedTick); + }); + + Assert.That(state?.GetType().Name, Is.EqualTo("Field3_FieldComponentState")); + } } [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] @@ -220,3 +345,16 @@ public abstract partial class AutoNetworkingTestParentComponent : Component [DataField] public int Parent = 4; } + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class AutoNetworkingTestFieldDeltaComponent : Component +{ + [DataField, AutoNetworkedField] + public int Field1 = 1; + + [DataField] // Add one field that is not networked to see if the next one get indexed correctly. + public int Field2 = 2; + + [DataField, AutoNetworkedField] + public int Field3 = 3; +} diff --git a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs index cad1dfe3f81..20e61731cf4 100644 --- a/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs +++ b/Robust.Shared/GameObjects/Components/UserInterface/UserInterfaceComponent.cs @@ -13,8 +13,8 @@ namespace Robust.Shared.GameObjects [RegisterComponent, NetworkedComponent, Access(typeof(SharedUserInterfaceSystem))] public sealed partial class UserInterfaceComponent : Component, IComponentDelta { - /// - public GameTick LastFieldUpdate { get; set; } + /// + public GameTick LastUnclassifiedDirty { get; set; } /// public GameTick[] LastModifiedFields { get; set; } = []; diff --git a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs index ae7c772bddf..08660cd8d64 100644 --- a/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs +++ b/Robust.Shared/GameObjects/EntityManager.ComponentDeltas.cs @@ -5,19 +5,26 @@ namespace Robust.Shared.GameObjects; public abstract partial class EntityManager { - public uint GetModifiedFields(IComponentDelta delta, GameTick fromTick) + public static ulong GetModifiedAspects(IComponentDelta delta, GameTick fromTick) { - uint fields = 0; + if (delta.LastUnclassifiedDirty > fromTick) + { + // By returning max value here, we short-circuit the more expensive evaluation below while returning a value + // with the unclassified change bit set. This will over-represent changes, technically, but in this specific + // case components should be doing a full update anyway. + return ulong.MaxValue; + } + ulong fields = 0; for (var i = 0; i < delta.LastModifiedFields.Length; i++) { var lastUpdate = delta.LastModifiedFields[i]; // Field not dirty - if (lastUpdate < fromTick) + if (lastUpdate <= fromTick) continue; - fields |= (uint) (1 << i); + fields |= 1UL << i; } return fields; @@ -34,9 +41,8 @@ public void DirtyField(EntityUid uid, IComponentDelta comp, string fieldName, Me } var curTick = _gameTiming.CurTick; - comp.LastFieldUpdate = curTick; comp.LastModifiedFields[idx] = curTick; - Dirty(uid, comp, metadata); + DirtyInternal(uid, comp, metadata, false); } public virtual void DirtyField(EntityUid uid, T comp, [ValidateMember] string fieldName, MetaDataComponent? metadata = null) @@ -54,9 +60,8 @@ public virtual void DirtyField(EntityUid uid, T comp, [ValidateMember] string } var curTick = _gameTiming.CurTick; - comp.LastFieldUpdate = curTick; comp.LastModifiedFields[idx] = curTick; - Dirty(uid, comp, metadata); + DirtyInternal(uid, comp, metadata, false); } public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? meta, params string[] fields) @@ -73,8 +78,7 @@ public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? met comp.LastModifiedFields[idx] = curTick; } - comp.LastFieldUpdate = curTick; - Dirty(uid, comp, meta); + DirtyInternal(uid, comp, meta, false); } } @@ -83,18 +87,22 @@ public virtual void DirtyFields(EntityUid uid, T comp, MetaDataComponent? met /// public partial interface IComponentDelta : IComponent { - // TODO: This isn't entirely robust but not sure how else to handle this? /// - /// Track last time a field was dirtied. if the full component dirty exceeds this then we send a full state update. + /// The last unclassified modification to this component. /// - public GameTick LastFieldUpdate { get; set; } + public GameTick LastUnclassifiedDirty { get; set; } /// /// Stores the last modified tick for fields. /// - public GameTick[] LastModifiedFields - { - get; - set; - } + public GameTick[] LastModifiedFields { get; set; } +} + +/// +/// Component delta system aspects. These are flags returned via . Fields +/// occupy the lower bits and grow upwards. System aspects occupy the upper bits and grow downwards. +/// +public static class DeltaAspect +{ + public const ulong Unclassified = 1UL << 63; } diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index d6396a0815a..4facdde86f4 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -397,6 +397,7 @@ private void AddComponentInternal(EntityUid uid, T component, ComponentRegist if (component is IComponentDelta delta) { var curTick = _gameTiming.CurTick; + delta.LastUnclassifiedDirty = curTick; delta.LastModifiedFields = new GameTick[reg.NetworkedFields.Length]; Array.Fill(delta.LastModifiedFields, curTick); } diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 4e99477aec5..6ae5ab0257b 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -402,6 +402,21 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo /// public IEnumerable GetEntities() => Entities; + #region Dirtying + + private bool DirtyComponent(IComponent component, bool isUnclassifiedChange = true) + { + var newChange = component.LastModifiedTick != CurrentTick; + component.LastModifiedTick = CurrentTick; + + if (isUnclassifiedChange && component is IComponentDelta delta) + { + delta.LastUnclassifiedDirty = CurrentTick; + } + + return newChange; + } + /// public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = null) { @@ -421,14 +436,16 @@ public virtual void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = nul } /// - [Obsolete("use override with an EntityUid or Entity")] - public void Dirty(IComponent component, MetaDataComponent? meta = null) + public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null) { - Dirty(component.Owner, component, meta); + DirtyInternal(uid, component, meta); } - /// - public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null) + internal void DirtyInternal( + EntityUid uid, + IComponent component, + MetaDataComponent? meta = null, + bool isUnclassifiedChange = true) { DebugTools.Assert(component.GetType().HasCustomAttribute(), $"Attempted to dirty a non-networked component: {component.GetType()}"); @@ -437,15 +454,18 @@ public virtual void Dirty(EntityUid uid, IComponent component, MetaDataComponent if (component.LifeStage >= ComponentLifeStage.Removing || !component.NetSyncEnabled) return; - if (component.LastModifiedTick == CurrentTick) - return; - - DirtyEntity(uid, meta); - component.LastModifiedTick = CurrentTick; + if (DirtyComponent(component, isUnclassifiedChange)) + DirtyEntity(uid, meta); } /// public virtual void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent + { + DirtyInternal(ent, meta); + } + + internal void DirtyInternal(Entity ent, MetaDataComponent? meta = null, bool isUnclassifiedChange = true) + where T : IComponent { DebugTools.Assert(ent.Comp.GetType().HasCustomAttribute(), $"Attempted to dirty a non-networked component: {ent.Comp.GetType()}"); @@ -453,11 +473,8 @@ public virtual void Dirty(Entity ent, MetaDataComponent? meta = null) wher if (ent.Comp.LifeStage >= ComponentLifeStage.Removing || !ent.Comp.NetSyncEnabled) return; - if (ent.Comp.LastModifiedTick == CurrentTick) - return; - - DirtyEntity(ent, meta); - ent.Comp.LastModifiedTick = CurrentTick; + if (DirtyComponent(ent.Comp, isUnclassifiedChange)) + DirtyEntity(ent, meta); } /// @@ -473,8 +490,8 @@ public virtual void Dirty(Entity ent, MetaDataComponent? meta = // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); } /// @@ -493,9 +510,9 @@ public virtual void Dirty(Entity ent, MetaDataComponent? // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; - ent.Comp3.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); + DirtyComponent(ent.Comp3); } /// @@ -517,12 +534,14 @@ public virtual void Dirty(Entity ent, MetaDataCo // We're not gonna bother checking ent.Comp.NetSyncEnabled // chances are at least one of these components didn't get net-sync disabled. DirtyEntity(ent, meta); - ent.Comp1.LastModifiedTick = CurrentTick; - ent.Comp2.LastModifiedTick = CurrentTick; - ent.Comp3.LastModifiedTick = CurrentTick; - ent.Comp4.LastModifiedTick = CurrentTick; + DirtyComponent(ent.Comp1); + DirtyComponent(ent.Comp2); + DirtyComponent(ent.Comp3); + DirtyComponent(ent.Comp4); } + #endregion + public bool TryQueueDeleteEntity(EntityUid? uid) { if (uid == null) diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs index d0763cf1bea..5c9547adc26 100644 --- a/Robust.Shared/GameObjects/IEntityManager.cs +++ b/Robust.Shared/GameObjects/IEntityManager.cs @@ -131,9 +131,6 @@ public partial interface IEntityManager public void DirtyEntity(EntityUid uid, MetaDataComponent? metadata = null); - [Obsolete("use override with an EntityUid")] - public void Dirty(IComponent component, MetaDataComponent? metadata = null); - public void Dirty(EntityUid uid, IComponent component, MetaDataComponent? meta = null); public void Dirty(Entity ent, MetaDataComponent? meta = null) where T : IComponent; diff --git a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs index 7126f470e49..a476fa08fe4 100644 --- a/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedUserInterfaceSystem.cs @@ -317,32 +317,31 @@ protected void OnUserInterfaceShutdown(Entity ent, ref C private void OnUserInterfaceGetState(Entity ent, ref ComponentGetState args) { - if (args.FromTick > ent.Comp.CreationTick && ent.Comp.LastFieldUpdate >= args.FromTick) - { - var fields = EntityManager.GetModifiedFields(ent.Comp, args.FromTick); + var aspects = EntityManager.GetModifiedAspects(ent.Comp, args.FromTick); - switch (fields) + switch (aspects) + { + case >= DeltaAspect.Unclassified: + break; + case 1 << 0: { - case 1 << 0: - { - var state = new UserInterfaceActorsDeltaState(); - AddActors(ent, state.Actors, ref args); + var state = new UserInterfaceActorsDeltaState(); + AddActors(ent, state.Actors, ref args); - args.State = state; - return; - } - case 1 << 2: - { - var states = ent.Comp.States; + args.State = state; + return; + } + case 1 << 2: + { + var states = ent.Comp.States; - // TODO Game State - // Force the client to serialize & de-serialize implicitly generated component states. - if (_netManager.IsClient) - states = new(states); + // TODO Game State + // Force the client to serialize & de-serialize implicitly generated component states. + if (_netManager.IsClient) + states = new(states); - args.State = new UserInterfaceStatesDeltaState {States = states}; - return; - } + args.State = new UserInterfaceStatesDeltaState {States = states}; + return; } } diff --git a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs index 91861776973..88cf18efeac 100644 --- a/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs +++ b/Robust.Shared/Physics/Components/PhysicsComponent.Physics.cs @@ -38,8 +38,8 @@ namespace Robust.Shared.Physics.Components; [RegisterComponent, NetworkedComponent] public sealed partial class PhysicsComponent : Component, IComponentDelta { - public GameTick LastFieldUpdate { get; set; } - public GameTick[] LastModifiedFields { get; set; } = []; + public GameTick LastUnclassifiedDirty { get; set; } + public GameTick[] LastModifiedFields { get; set; } /// /// Has this body been added to an island previously in this tick. diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 4a093b451ca..c3161597184 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -73,7 +73,7 @@ private void OnPhysicsInit(EntityUid uid, PhysicsComponent component, ComponentI private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref ComponentGetState args) { - if (args.FromTick > component.CreationTick && component.LastFieldUpdate >= args.FromTick) + if (component.LastUnclassifiedDirty <= args.FromTick) { var slowPath = false; @@ -81,7 +81,7 @@ private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref Co { var field = component.LastModifiedFields[i]; - if (field < args.FromTick) + if (field <= args.FromTick) continue; slowPath = true; @@ -91,7 +91,7 @@ private void OnPhysicsGetState(EntityUid uid, PhysicsComponent component, ref Co // We can do a smaller delta with no list index overhead. if (!slowPath) { - var angularDirty = component.LastModifiedFields[_angularVelocityIndex] >= args.FromTick; + var angularDirty = component.LastModifiedFields[_angularVelocityIndex] > args.FromTick; if (angularDirty) { From 7c7d1ae4513ba3231a8f21b52e765613b0518f51 Mon Sep 17 00:00:00 2001 From: Whatstone <166147148+whatston3@users.noreply.github.com> Date: Thu, 16 Jul 2026 05:43:54 -0400 Subject: [PATCH 172/178] Cleanup: Warning resolution/suppression (#6721) --- .../Read/SerializationReadBenchmark.cs | 2 +- .../Cef/WebViewManagerCef.Control.cs | 7 ++-- .../Commands/UITestCommand.TabSpriteView.cs | 22 +++++++----- .../GameObjects/EntitySystems/SpriteSystem.cs | 4 +-- Robust.Client/Graphics/Clyde/Clyde.Sprite.cs | 7 ++-- .../Editors/VVPropEditorEntityCoordinates.cs | 4 ++- .../GameObjects/Components/Transform_Test.cs | 23 +++++++------ .../Systems/AnchoredSystemTests.cs | 9 +++-- .../GameObjects/TransformComponent_Tests.cs | 3 +- .../Map/GridRotation_Tests.cs | 11 +++--- .../Prototypes/PrototypeHasCompTest.cs | 4 ++- .../Transform/TransformComponent.cs | 34 +++++++++---------- Robust.Shared/GameObjects/EntityManager.cs | 2 ++ .../Systems/PrototypeReloadSystem.cs | 5 ++- .../SharedTransformSystem.Component.cs | 6 +++- .../SharedTransformSystem.Coordinates.cs | 11 ++++++ Robust.Shared/Map/TileDefinitionManager.cs | 2 ++ .../Systems/SharedPhysicsSystem.Island.cs | 6 ++-- Robust.Shared/Prototypes/EntProtoId.cs | 4 +-- .../PrototypeManager.ValidateFields.cs | 2 ++ 20 files changed, 101 insertions(+), 67 deletions(-) diff --git a/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs b/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs index e1fc2d5c3ca..b2ad198a640 100644 --- a/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs +++ b/Robust.Benchmarks/Serialization/Read/SerializationReadBenchmark.cs @@ -20,7 +20,7 @@ public SerializationReadBenchmark() InitializeSerialization(); StringDataDefNode = new MappingDataNode(); - StringDataDefNode.Add(new ValueDataNode("string"), new ValueDataNode("ABC")); + StringDataDefNode.Add("string", new ValueDataNode("ABC")); var yamlStream = new YamlStream(); yamlStream.Load(new StringReader(SeedDataDefinition.Prototype)); diff --git a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs index 9af640113a6..ebf1782aae8 100644 --- a/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs +++ b/Robust.Client.WebView/Cef/WebViewManagerCef.Control.cs @@ -7,6 +7,7 @@ using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; using SixLabors.ImageSharp.PixelFormats; using Xilium.CefGlue; @@ -19,16 +20,18 @@ internal partial class WebViewManagerCef { private readonly List _activeControls = new(); + private static readonly ProtoId _bgraShader = "bgra"; + public IWebViewControlImpl MakeControlImpl(WebViewControl owner) { - var shader = _prototypeManager.Index("bgra"); + var shader = _prototypeManager.Index(_bgraShader); var shaderInstance = shader.Instance(); var impl = new ControlImpl(this, owner, shaderInstance); _dependencyCollection.InjectDependencies(impl); return impl; } - private sealed class ControlImpl : IWebViewControlImpl + private sealed partial class ControlImpl : IWebViewControlImpl { private static readonly Dictionary KeyMap = new() { diff --git a/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs b/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs index 1ae4557d708..cd44fe6b0d3 100644 --- a/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs +++ b/Robust.Client/Console/Commands/UITestCommand.TabSpriteView.cs @@ -21,6 +21,8 @@ private sealed class TabSpriteView : Control private readonly IEntityManager _entMan; private readonly IGameTiming _timing; + private readonly SpriteSystem _sprite; + private readonly SharedTransformSystem _xform; private readonly BoxContainer _box; private record Entry(EntityUid Uid, SpriteComponent Sprite, TransformComponent Transform, @@ -33,6 +35,8 @@ private record Entry(EntityUid Uid, SpriteComponent Sprite, TransformComponent T public TabSpriteView() { IoCManager.Resolve(ref _entMan, ref _timing); + _sprite = _entMan.System(); + _xform = _entMan.System(); SetValue(TabContainer.TabTitleProperty, nameof(SpriteView)); _box = new BoxContainer { @@ -178,14 +182,14 @@ private List AddEntries() entry = AddEntry("Local Rotation", (e, time) => { - e.Transform.LocalRotation = Angle.FromDegrees(time * _degreesPerSecond); + _xform.SetLocalRotation(e.Uid, Angle.FromDegrees(time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); added.Add(entry); entry = AddEntry("Local Rotation (NoRot)", (e, time) => { - e.Transform.LocalRotation = Angle.FromDegrees(time * _degreesPerSecond); + _xform.SetLocalRotation(e.Uid, Angle.FromDegrees(time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); entry.Sprite.NoRotation = true; @@ -193,7 +197,7 @@ private List AddEntries() entry = AddEntry("Offset", (e, time) => { - e.Sprite.Offset = new Vector2(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0); + _sprite.SetOffset((e.Uid, e.Sprite), new Vector2(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0)); e.View.InvalidateMeasure(); }); added.Add(entry); @@ -201,24 +205,24 @@ private List AddEntries() entry = AddEntry("Scaled", (e, time) => { var theta = (float) Angle.FromDegrees(_degreesPerSecond * time).Theta; - e.Sprite.Scale = Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta)); + _sprite.SetScale((e.Uid, e.Sprite), Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta))); e.View.InvalidateMeasure(); }); added.Add(entry); entry = AddEntry("Sprite Rotation", (e, time) => { - e.Sprite.Rotation = Angle.FromDegrees(time * _degreesPerSecond); + _sprite.SetRotation((e.Uid, e.Sprite), Angle.FromDegrees(time * _degreesPerSecond)); }); added.Add(entry); entry = AddEntry("Combination", (e, time) => { var theta = (float) Angle.FromDegrees(_degreesPerSecond * time * 2).Theta; - e.Sprite.Scale = Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta)); - e.Sprite.Offset = new(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0); - e.Sprite.Rotation = Angle.FromDegrees(0.5 * time * _degreesPerSecond); - e.Transform.LocalRotation = Angle.FromDegrees(0.25 * time * _degreesPerSecond); + _sprite.SetScale((e.Uid, e.Sprite), Vector2.One + new Vector2(0.5f * MathF.Sin(theta), 0.5f * MathF.Cos(theta))); + _sprite.SetOffset((e.Uid, e.Sprite), new(MathF.Sin((float) Angle.FromDegrees(time * _degreesPerSecond)), 0)); + _sprite.SetRotation((e.Uid, e.Sprite), Angle.FromDegrees(0.5 * time * _degreesPerSecond)); + _xform.SetLocalRotationNoLerp(e.Uid, Angle.FromDegrees(0.25 * time * _degreesPerSecond), e.Transform); e.View.InvalidateMeasure(); }); added.Add(entry); diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs index e481a64f398..34b2153a11b 100644 --- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.cs @@ -71,7 +71,7 @@ public bool IsVisible(Layer layer) private void OnInit(EntityUid uid, SpriteComponent component, ComponentInit args) { // I'm not 100% this is needed, but I CBF with this ATM. Somebody kill server sprite component please. - QueueUpdateInert(uid, component); + QueueUpdateIsInert((uid, component)); } private void OnBiasChanged(double value) @@ -215,7 +215,7 @@ public Texture GetFrame(SpriteSpecifier spriteSpec, TimeSpan curTime, bool loop sprite ??= Frame0(spriteSpec); break; case SpriteSpecifier.Texture texture: - sprite = texture.GetTexture(_resourceCache); + sprite = GetTexture(texture); break; default: throw new NotImplementedException(); diff --git a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs index 5489624667b..439e23ebdeb 100644 --- a/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs +++ b/Robust.Client/Graphics/Clyde/Clyde.Sprite.cs @@ -1,12 +1,9 @@ using System; using System.Buffers; -using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; using System.Threading.Tasks; -using Robust.Client.ComponentTrees; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Graphics; @@ -139,11 +136,11 @@ private void ProcessSprites( // To help explain the remainder of this function, it should be functionally equivalent to the following // three lines of code, but has been expanded & simplified to speed up the calculation: // - // (data.WorldPos, data.WorldRot) = batch.Sys.GetWorldPositionRotation(data.Xform, batch.Query); + // (data.WorldPos, data.WorldRot) = batch.Sys.GetWorldPositionRotation(data.Xform); // var spriteWorldBB = data.Sprite.CalculateRotatedBoundingBox(data.WorldPos, data.WorldRot, batch.ViewRotation); // data.SpriteScreenBB = Viewport.GetWorldToLocalMatrix().TransformBox(spriteWorldBB); - var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner, batch.Query); + var (pos, rot) = batch.Sys.GetRelativePositionRotation(data.Xform, batch.TreeOwner); pos = new Vector2( batch.TreePos.X + batch.Cos * pos.X - batch.Sin * pos.Y, batch.TreePos.Y + batch.Sin * pos.X + batch.Cos * pos.Y); diff --git a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs index 840cd81de8a..6898fdd5974 100644 --- a/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs +++ b/Robust.Client/ViewVariables/Editors/VVPropEditorEntityCoordinates.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Numerics; +using Robust.Client.GameObjects; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; @@ -24,6 +25,7 @@ protected override Control MakeUI(object? value) hBoxContainer.AddChild(new Label {Text = "grid: "}); var entityManager = IoCManager.Resolve(); + var xformSystem = entityManager.System(); var gridId = new LineEdit { @@ -31,7 +33,7 @@ protected override Control MakeUI(object? value) HorizontalExpand = true, PlaceHolder = "Grid ID", ToolTip = "Grid ID", - Text = coords.GetGridUid(entityManager)?.ToString() ?? "" + Text = xformSystem.GetGrid(coords)?.ToString() ?? "" }; hBoxContainer.AddChild(gridId); diff --git a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs index 6dcf87edb0c..dc28c8ca57e 100644 --- a/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs +++ b/Robust.Server.IntegrationTests/GameObjects/Components/Transform_Test.cs @@ -190,7 +190,7 @@ public void ParentRotateTest() XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act - parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(parent, new Angle(MathHelper.Pi / 2), parentTrans); //Assert var result = XformSystem.GetWorldPosition(childTrans); @@ -217,7 +217,7 @@ public void ParentTransRotateTest() XformSystem.SetParent(child, childTrans, parent, parentXform: parentTrans); //Act - parentTrans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(parent, new Angle(MathHelper.Pi / 2), parentTrans); //Assert var result = XformSystem.GetWorldPosition(childTrans); @@ -255,7 +255,7 @@ public void PositionCompositionTest() XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act - node1Trans.LocalRotation = new Angle(MathHelper.Pi / 2); + XformSystem.SetLocalRotationNoLerp(node1, new Angle(MathHelper.Pi / 2), node1Trans); //Assert var result = XformSystem.GetWorldPosition(node4Trans); @@ -337,11 +337,12 @@ public void ParentRotationRoundingErrorTest() // Act var oldWpos = XformSystem.GetWorldPosition(node3Trans); + var angle180 = new Angle(MathHelper.Pi); for (var i = 0; i < 100; i++) { - node1Trans.LocalRotation += new Angle(MathHelper.Pi); - node2Trans.LocalRotation += new Angle(MathHelper.Pi); - node3Trans.LocalRotation += new Angle(MathHelper.Pi); + XformSystem.SetLocalRotationNoLerp(node1, node1Trans.LocalRotation + angle180, node1Trans); + XformSystem.SetLocalRotationNoLerp(node2, node2Trans.LocalRotation + angle180, node2Trans); + XformSystem.SetLocalRotationNoLerp(node3, node3Trans.LocalRotation + angle180, node3Trans); } var newWpos = XformSystem.GetWorldPosition(node3Trans); @@ -386,7 +387,7 @@ public void TreeComposeWorldMatricesTest() XformSystem.SetParent(node4, node4Trans, node3, parentXform: node3Trans); //Act - node1Trans.LocalRotation = new Angle(MathHelper.Pi / 6.37); + XformSystem.SetLocalRotation(node1, new Angle(MathHelper.Pi / 6.37), node1Trans); XformSystem.SetWorldPosition(node1, new Vector2(1, 1)); var worldMat = XformSystem.GetWorldMatrix(node4Trans); @@ -425,12 +426,12 @@ public void WorldRotationTest() XformSystem.SetParent(node2, node2Trans, node1, parentXform: node1Trans); XformSystem.SetParent(node3, node3Trans, node2, parentXform: node2Trans); - node1Trans.LocalRotation = Angle.FromDegrees(0); - node2Trans.LocalRotation = Angle.FromDegrees(45); - node3Trans.LocalRotation = Angle.FromDegrees(45); + XformSystem.SetLocalRotationNoLerp(node1, Angle.Zero, node1Trans); + XformSystem.SetLocalRotationNoLerp(node2, Angle.FromDegrees(45), node2Trans); + XformSystem.SetLocalRotationNoLerp(node3, Angle.FromDegrees(45), node3Trans); // Act - node1Trans.LocalRotation = Angle.FromDegrees(135); + XformSystem.SetLocalRotationNoLerp(node1, Angle.FromDegrees(135), node1Trans); // Assert (135 + 45 + 45 = 225) var result = XformSystem.GetWorldRotation(node3Trans); diff --git a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs index d3abfc2e0e1..e4f8e53b330 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/Systems/AnchoredSystemTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using System.Numerics; using NUnit.Framework; using Robust.Shared.Containers; @@ -83,12 +82,14 @@ public void OnAnchored_WorldPosition_TileCenter() private sealed partial class AnchorOnInitComponent : Component; [Reflect(false)] - private sealed class AnchorOnInitTestSystem : EntitySystem + private sealed partial class AnchorOnInitTestSystem : EntitySystem { + [Dependency] private SharedTransformSystem _transform = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent((e, _, _) => Transform(e).Anchored = true); + SubscribeLocalEvent((e, _, _) => _transform.AnchorEntity(e)); } } @@ -263,8 +264,10 @@ public void Anchored_SetPosition_Nop() sim.System().FailOnMove = true; // Act +#pragma warning disable CS0618 // Checking property setters. sim.Transform(ent1).WorldPosition = new Vector2(99, 99); sim.Transform(ent1).LocalPosition = new Vector2(99, 99); +#pragma warning restore CS0618 Assert.That(xformSys.GetMapCoordinates(ent1), Is.EqualTo(coordinates)); sim.System().FailOnMove = false; diff --git a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs index 01f0559e625..4a95e37a6a7 100644 --- a/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs +++ b/Robust.Shared.IntegrationTests/GameObjects/TransformComponent_Tests.cs @@ -1,4 +1,3 @@ -using System; using System.Numerics; using NUnit.Framework; using Robust.Server.GameObjects; @@ -32,7 +31,7 @@ public void TestGetWorldMatches() xform.SetParent(ent2, ent1); - xform1.LocalRotation = MathF.PI; + xform.SetLocalRotationNoLerp(ent1, MathF.PI, xform1); var (worldPos, worldRot, worldMatrix) = xform.GetWorldPositionRotationMatrix(xform2); diff --git a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs index c3ca74f10e5..4cefd9fcb75 100644 --- a/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs +++ b/Robust.Shared.IntegrationTests/Map/GridRotation_Tests.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; using System.Numerics; -using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Map; @@ -65,6 +62,7 @@ public async Task TestChunkRotations() var entMan = server.ResolveDependency(); var mapSystem = entMan.System(); + var xformSystem = entMan.System(); await server.WaitAssertion(() => { @@ -93,19 +91,20 @@ await server.WaitAssertion(() => // With all cardinal directions these should align. Assert.That(aabb, Is.EqualTo(bounds)); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(Math.PI); + var gridXform = entMan.GetComponent(gridEnt); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(Math.PI), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(-2, -10), new Vector2(0, 0)); Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}"); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(-Math.PI / 2); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(-Math.PI / 2), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(0, -2), new Vector2(10, 0)); Assert.That(aabb.EqualsApprox(bounds), $"Expected bounds of {aabb} and got {bounds}"); - entMan.GetComponent(gridEnt).LocalRotation = new Angle(-Math.PI / 4); + xformSystem.SetLocalRotationNoLerp(gridEnt, new Angle(-Math.PI / 4), gridXform); aabb = mapSystem.CalcWorldAABB(gridEnt, grid, chunk); bounds = new Box2(new Vector2(0, -1.4142135f), new Vector2(8.485281f, 7.071068f)); diff --git a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs index 9871a6a1f71..4bd2f0f84d3 100644 --- a/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs +++ b/Robust.Shared.IntegrationTests/Prototypes/PrototypeHasCompTest.cs @@ -31,6 +31,8 @@ internal sealed class PrototypeHasCompTest : OurRobustUnitTest const string TestEntity = "TestEntity"; // expected to have TestCompNameComponent const string TestInception = "TestInception"; + // A bogus component name. + const string TestFail = "TestFail"; [OneTimeSetUp] public void Setup() @@ -107,7 +109,7 @@ public void TestCompNameSerialization() Assert.That(_factory.HasRegistration(name)); // gibberish component should prevent it from loading - Assert.That(!_proto.HasIndex("TestFail")); + Assert.That(!_proto.HasIndex(TestFail)); } const string TestPrototypes = $@" diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index 206ca55fb53..2b23fb359fd 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Numerics; using JetBrains.Annotations; using Robust.Shared.Animations; @@ -8,7 +7,6 @@ using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Physics; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -133,6 +131,7 @@ public Matrix3x2 InvLocalMatrix public bool NoLocalRotation { get => _noLocalRotation; + [Obsolete("Use SharedTransformSystem.SetNoLocalRotation() instead")] set { if (value) @@ -151,7 +150,7 @@ public bool NoLocalRotation public Angle LocalRotation { get => _localRotation; - [Obsolete("Use SharedTransformSystem.SetLocalRotation")] + [Obsolete("Use SharedTransformSystem.SetLocalRotation() instead")] set { if(_noLocalRotation) @@ -179,7 +178,7 @@ public Angle LocalRotation /// Current world rotation of the entity. /// [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.Get/SetWorldRotation() instead")] public Angle WorldRotation { get @@ -220,7 +219,7 @@ public Angle WorldRotation /// /// Matrix for transforming points from local to world space. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldMatrix() instead")] public Matrix3x2 WorldMatrix { get @@ -246,7 +245,7 @@ public Matrix3x2 WorldMatrix /// /// Matrix for transforming points from world to local space. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetInvWorldMatrix() instead")] public Matrix3x2 InvWorldMatrix { get @@ -275,7 +274,7 @@ public Matrix3x2 InvWorldMatrix /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.Get/SetWorldPosition() instead")] public Vector2 WorldPosition { get @@ -316,7 +315,7 @@ public EntityCoordinates Coordinates var valid = _parent.IsValid(); return new EntityCoordinates(valid ? _parent : Owner, valid ? LocalPosition : Vector2.Zero); } - [Obsolete("Use the system's setter method instead.")] + [Obsolete("Use SharedTransformSystem.SetCoordinates() instead")] set => _entMan.EntitySysManager.GetEntitySystem().SetCoordinates(Owner, this, value); } @@ -325,7 +324,7 @@ public EntityCoordinates Coordinates /// This is effectively a more complete version of /// [ViewVariables(VVAccess.ReadWrite)] - [Obsolete("Use TransformSystem.GetMapCoordinates")] + [Obsolete("Use SharedTransformSystem.GetMapCoordinates() instead")] public MapCoordinates MapPosition => new(WorldPosition, MapID); /// @@ -337,7 +336,7 @@ public EntityCoordinates Coordinates public Vector2 LocalPosition { get => _localPosition; - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.SetLocalPosition() instead")] set { if(Anchored) @@ -400,7 +399,7 @@ public bool Anchored /// /// Detaches this entity from its parent. /// - [Obsolete("Use the system's method instead.")] + [Obsolete("Use SharedTransformSystem.AttachToGridOrMap() instead")] public void AttachToGridOrMap() { _entMan.EntitySysManager.GetEntitySystem().AttachToGridOrMap(Owner, this); @@ -415,7 +414,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition and WorldRotation of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotation() instead")] public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation() { // Worldmatrix needs calculating anyway for worldpos so we'll just drop it. @@ -426,7 +425,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix) GetWorldPositionRotationMatrix(EntityQuery xforms) { var parent = _parent; @@ -452,7 +451,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix) GetWorldPositionRotationMatrix() { var xforms = _entMan.GetEntityQuery(); @@ -462,7 +461,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.WorldPositionRotationInvMatrix() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationInvMatrix(EntityQuery xformQuery) { var (worldPos, worldRot, _, invWorldMatrix) = GetWorldPositionRotationMatrixWithInv(xformQuery); @@ -472,7 +471,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrixWithInv() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv() { var xformQuery = _entMan.GetEntityQuery(); @@ -482,7 +481,7 @@ public void AttachParent(EntityUid parent) /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// - [Obsolete("Use the system method instead")] + [Obsolete("Use SharedTransformSystem.GetWorldPositionRotationMatrixWithInv() instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3x2 WorldMatrix, Matrix3x2 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv(EntityQuery xformQuery) { var parent = _parent; @@ -526,6 +525,7 @@ public void RebuildMatrices() _invLocalMatrix = Matrix3Helpers.CreateInverseTransform(_localPosition, _localRotation); } + [Obsolete("Use SharedTransformSystem.GetDebugString() instead")] public string GetDebugString() { return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}"; diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 6ae5ab0257b..de734da9fce 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -370,7 +370,9 @@ public virtual EntityUid CreateEntityUninitialized(string? prototypeName, MapCoo if (coordinates.MapId == MapId.Nullspace) { transform._parent = EntityUid.Invalid; +#pragma warning disable CS0618 // AnchorEntity/Unanchor only work on initialized entities transform.Anchored = false; +#pragma warning restore CS0618 return newEntity; } diff --git a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs index 4463d86db51..d36822a54fc 100644 --- a/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs +++ b/Robust.Shared/GameObjects/Systems/PrototypeReloadSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Robust.Shared.IoC; using Robust.Shared.Prototypes; namespace Robust.Shared.GameObjects; @@ -10,6 +11,8 @@ namespace Robust.Shared.GameObjects; /// internal sealed partial class PrototypeReloadSystem : EntitySystem { + [Dependency] private MetaDataSystem _meta = default!; + public override void Initialize() { base.Initialize(); @@ -76,6 +79,6 @@ private void UpdateEntity(EntityUid entity, MetaDataComponent metaData, EntityPr } // Update entity metadata - metaData.EntityPrototype = newPrototype; + _meta.SetEntityPrototype(entity, newPrototype, metaData); } } diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index 0e8b2ddcfa1..ed74453afb6 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -428,7 +428,7 @@ public void SetLocalPositionNoLerp(EntityUid uid, Vector2 value, TransformCompon if (!XformQuery.Resolve(uid, ref xform)) return; -#pragma warning disable CS0618 +#pragma warning disable CS0618 // TODO: move LocalRotation/Position manipulation into TransformSystem (don't piggyback off TransformComponent) xform.LocalPosition = value; #pragma warning restore CS0618 } @@ -447,7 +447,9 @@ public void SetLocalRotationNoLerp(EntityUid uid, Angle value, TransformComponen if (!XformQuery.Resolve(uid, ref xform)) return; +#pragma warning disable CS0618 // TODO: move LocalRotation/Position manipulation into TransformSystem (don't piggyback off TransformComponent) xform.LocalRotation = value; +#pragma warning restore CS0618 } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -887,7 +889,9 @@ internal void OnHandleState(EntityUid uid, TransformComponent xform, ref Compone } else { +#pragma warning disable CS0618 // AnchorEntity/Unanchored can't be used from uninitialized states. xform.Anchored = newState.Anchored; +#pragma warning restore CS0618 } if (oldAnchored != newState.Anchored && xform.Initialized) diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs index 7d25327359c..5d107b56aa7 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Coordinates.cs @@ -239,4 +239,15 @@ public bool InRange(Entity entA, Entity + /// Returns a readable string with the entity's local and world position and rotation. Useful for debugging. + /// + public string GetDebugString(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + return "invalid"; + + return $"pos/rot/wpos/wrot: {ent.Comp.Coordinates}/{ent.Comp.LocalRotation}/{GetWorldPosition(ent.Comp)}/{GetWorldRotation(ent.Comp)}"; + } } diff --git a/Robust.Shared/Map/TileDefinitionManager.cs b/Robust.Shared/Map/TileDefinitionManager.cs index 3b14440fca1..41989358adf 100644 --- a/Robust.Shared/Map/TileDefinitionManager.cs +++ b/Robust.Shared/Map/TileDefinitionManager.cs @@ -47,6 +47,7 @@ public Tile GetVariantTile(string name, IRobustRandom random) return GetVariantTile(tileDef, random); } + [Obsolete("Use method with IRobustRandom parameter")] public Tile GetVariantTile(string name, System.Random random) { var tileDef = this[name]; @@ -58,6 +59,7 @@ public Tile GetVariantTile(ITileDefinition tileDef, IRobustRandom random) return new Tile(tileDef.TileId, variant: random.NextByte(tileDef.Variants)); } + [Obsolete("Use method with IRobustRandom parameter")] public Tile GetVariantTile(ITileDefinition tileDef, System.Random random) { return new Tile(tileDef.TileId, variant: random.NextByte(tileDef.Variants)); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs index 6bf52122f80..d8dfdd9f20c 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Island.cs @@ -332,7 +332,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) // when contact broke so if you want to try that then GOOD LUCK. if (seed.Island) continue; - var seedUid = seed.Owner; + var seedUid = ent.Owner; var mapUid = xform.MapUid; // TODO: Handle this on client. @@ -377,7 +377,7 @@ private void Solve(float frameTime, float dtRatio, float invDt, bool prediction) if (body.BodyType == BodyType.Static) continue; // As static bodies can never be awake (unlike Farseer) we'll set this after the check. - SetAwake(bodyUid, body, true, updateSleepTime: false); + SetAwake(bodyEnt, true, updateSleepTime: false); var node = body.Contacts.First; @@ -1139,7 +1139,7 @@ private void SleepBodies(in IslandData island, bool[] sleepStatus) var body = island.Bodies[i]; - SetAwake(body.Owner, body, false); + SetAwake(body, false); } } } diff --git a/Robust.Shared/Prototypes/EntProtoId.cs b/Robust.Shared/Prototypes/EntProtoId.cs index 673da72980d..8cfd69ba5c1 100644 --- a/Robust.Shared/Prototypes/EntProtoId.cs +++ b/Robust.Shared/Prototypes/EntProtoId.cs @@ -97,7 +97,7 @@ public T Get(IPrototypeManager? prototypes, IComponentFactory compFactory) { prototypes ??= IoCManager.Resolve(); var proto = prototypes.Index(this); - if (!proto.TryGetComponent(out T? comp, compFactory)) + if (!proto.TryComp(out T? comp, compFactory)) { throw new ArgumentException($"{nameof(EntityPrototype)} {proto.ID} has no {nameof(T)}"); } @@ -110,6 +110,6 @@ public bool TryGet([NotNullWhen(true)] out T? comp, IPrototypeManager? prototype comp = default; prototypes ??= IoCManager.Resolve(); return prototypes.TryIndex(this, out var proto) && - proto.TryGetComponent(out comp, compFactory); + proto.TryComp(out comp, compFactory); } } diff --git a/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs b/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs index 62801514590..0b9dc8d5268 100644 --- a/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs +++ b/Robust.Shared/Prototypes/PrototypeManager.ValidateFields.cs @@ -177,7 +177,9 @@ private IEnumerable GetEnumerableIds(IEnumerable ids) private bool TryGetFieldPrototype(FieldInfo field, [NotNullWhen(true)] out Type? proto) { // Validate anything with the attribute +#pragma warning disable CS0618 // This is supporting the attribute, not using it var attrib = field.GetCustomAttribute(typeof(ValidatePrototypeIdAttribute<>), false); +#pragma warning restore CS0618 if (attrib != null) { proto = attrib.GetType().GetGenericArguments().First(); From 0e72d557d121c84d33aa23f54de1fe431bce76d2 Mon Sep 17 00:00:00 2001 From: Stanislav <72458459+JokeCam@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:51:31 +0300 Subject: [PATCH 173/178] Add an AudioParams.AddVariation() function akin to AddVolume() (#5804) Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Robust.Shared/Audio/AudioParams.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Robust.Shared/Audio/AudioParams.cs b/Robust.Shared/Audio/AudioParams.cs index 426691abcc0..876899bce83 100644 --- a/Robust.Shared/Audio/AudioParams.cs +++ b/Robust.Shared/Audio/AudioParams.cs @@ -164,6 +164,20 @@ public readonly AudioParams WithVariation(float? variation) return me; } + /// + /// Returns a copy of this instance with a modified variation set, for easy chaining. + /// + [Pure] + public readonly AudioParams AddVariation(float? variation) + { + var me = this; + if (variation == null) + return me; + + me.Variation = (me.Variation ?? 0f) + variation; + return me; + } + /// /// Returns a copy of this instance with a new pitch scale set, for easy chaining. /// From dfb397647983be5ec2b176ea1f8e479721787166 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:52:07 +1000 Subject: [PATCH 174/178] Omit .Collect() for sourcegen (#6803) --- Robust.Serialization.Generator/Generator.cs | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs index 5348dcca353..bdf83318013 100644 --- a/Robust.Serialization.Generator/Generator.cs +++ b/Robust.Serialization.Generator/Generator.cs @@ -62,22 +62,12 @@ public void Initialize(IncrementalGeneratorInitializationContext initContext) .Where(static type => type != null); initContext.RegisterSourceOutput( - dataDefinitions.Collect(), - static (sourceContext, sources) => + dataDefinitions, + static (sourceContext, source) => { - var done = new HashSet(); - - foreach (var source in sources) - { - var (name, code) = source!.Value; - - if (!done.Add(name)) - continue; - - sourceContext.AddSource(name, SourceText.From(code, Encoding.UTF8)); - } - } - ); + var (name, code) = source!.Value; + sourceContext.AddSource(name, SourceText.From(code, Encoding.UTF8)); + }); } private static bool IsCandidateTypeDeclaration(SyntaxNode node) From faabd84348947249495003285be41266931736f9 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:36:48 +1000 Subject: [PATCH 175/178] Fix componentnetworkgenerator not copying getstate on client (#6748) --- .../ComponentNetworkGenerator.cs | 324 ++++++++++++++---- .../GameState/AutoNetworkingTest.cs | 1 + .../GameState/NoSharedReferencesTest.cs | 156 ++++++++- 3 files changed, 392 insertions(+), 89 deletions(-) diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs index 52780ea5875..faf9d5f1a27 100644 --- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs +++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs @@ -142,6 +142,7 @@ public class ComponentNetworkGenerator : ISourceGenerator // Name = component.Name, // Count = component.Count, var getStateInit = new StringBuilder(); + var clientGetStateInit = new StringBuilder(); // eg: // component.Name = state.Name; @@ -149,13 +150,12 @@ public class ComponentNetworkGenerator : ISourceGenerator var handleStateSetters = new StringBuilder(); // Builds the string for duplicating a full component state, in preparation for applying a delta state state - // without modifying the original. Note that this will not do a proper clone of any collections, under the - // assumption that nothing should ever try to modify them. Applying the delta state should just override the - // referenced collection, not modify it. + // without modifying the original. var shallowClone = new StringBuilder(); // Delta field states var deltaGetFields = new StringBuilder(); + var clientDeltaGetFields = new StringBuilder(); var deltaHandleFields = new StringBuilder(); @@ -168,6 +168,43 @@ public class ComponentNetworkGenerator : ISourceGenerator var fieldStates = new StringBuilder(); var networkedTypes = new List(); + var usesClientCollectionCopy = false; + + void AppendShallowClone(string fieldName) + { + shallowClone.Append($@" + {fieldName} = this.{fieldName},"); + } + + void AppendCollectionClone(string fieldName, bool nullable) + { + var value = nullable + ? $"this.{fieldName} == null ? null! : new(this.{fieldName})" + : $"new(this.{fieldName})"; + shallowClone.Append($@" + {fieldName} = {value},"); + } + + string GetClientCollectionField(string fieldName, bool nullable) + { + usesClientCollectionCopy = true; + return nullable + ? $"component.{fieldName} == null ? null! : new(component.{fieldName})" + : $"new(component.{fieldName})"; + } + + string GetCollectionRefill(ITypeSymbol type, string target, string source, string indentation) + { + var named = (INamedTypeSymbol) type; + return named.ConstructedFrom.ToDisplayString(FullyQualifiedFormat) switch + { + GlobalDictionaryName => $@"foreach (var (key, value) in {source}) +{indentation} {target}.Add(key, value);", + GlobalHashSetName => $"{target}.UnionWith({source});", + GlobalListName => $"{target}.AddRange({source});", + _ => throw new InvalidOperationException($"Unsupported collection type {type}") + }; + } foreach (var (type, name) in fields) { @@ -192,6 +229,7 @@ public class ComponentNetworkGenerator : ISourceGenerator string networkedType; string getField; + string? clientGetField = null; string? cast; // TODO: Uhh I just need casts or something. var castString = typeDisplayStr.Substring(8); @@ -202,6 +240,12 @@ public class ComponentNetworkGenerator : ISourceGenerator {{ "); + clientDeltaGetFields.Append(@$" + case {Math.Pow(2, index)}: + args.State = new {deltaStateName}() + {{ + "); + deltaHandleFields.Append(@$" case {deltaStateName} {deltaStateName}_State: {{"); @@ -226,8 +270,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = EnsureEntity<{componentName}>({cast} {fieldHandleValue}, uid);"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name};"); @@ -248,8 +291,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = EnsureCoordinates<{componentName}>({cast} {fieldHandleValue}, uid);"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($@"fullState.{name} = {name};"); @@ -269,8 +311,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntitySet<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -279,7 +320,7 @@ public class ComponentNetworkGenerator : ISourceGenerator networkedType = $"{GlobalNetEntityUidListName}"; stateFields.Append($@" - public {networkedType} {name} = default!;"); + public {networkedType} {name} = default!;"); getField = $"GetNetEntityList(component.{name})"; cast = $"({GlobalNetEntityUidListName})"; @@ -290,8 +331,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntityList<{componentName}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -345,8 +385,7 @@ public class ComponentNetworkGenerator : ISourceGenerator EnsureEntityDictionary<{ensureGeneric}>({cast} {fieldHandleValue}, uid, component.{name});"); } - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -370,8 +409,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" EnsureEntityDictionary<{componentName}, {key}>({cast} {fieldHandleValue}, uid, component.{name});"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendCollectionClone(name, nullable); deltaApply.Add($@"fullState.{name} = {name};"); @@ -401,8 +439,7 @@ public class ComponentNetworkGenerator : ISourceGenerator component.{name} = null!; else component.{name} = ({nullCast})({name}Value.Clone());"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name} == null ? null! : {name}.Clone();"); } else @@ -411,32 +448,68 @@ public class ComponentNetworkGenerator : ISourceGenerator component.{name} = state.{name}.Clone();"); deltaHandleFields.Append($@" component.{name} = {cast}({fieldHandleValue}.Clone());"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name}.Clone();"); } } else if (IsCloneType(type)) { getField = $"component.{name}"; + clientGetField = GetClientCollectionField(name, nullable); cast = $"({castString})"; var nullCast = nullable ? castString.Substring(0, castString.Length - 1) : castString; + var handleRefill = GetCollectionRefill(type, $"component.{name}", $"state.{name}", " "); + var deltaRefill = GetCollectionRefill(type, $"component.{name}", $"{name}Value", " "); - handleStateSetters.Append($@" - component.{name} = state.{name} == null ? null! : new(state.{name});"); + if (nullable) + { + handleStateSetters.Append($@" + if (state.{name} == null) + component.{name} = null!; + else if (component.{name} == null) + component.{name} = new(state.{name}); + else if (!ReferenceEquals(component.{name}, state.{name})) + {{ + component.{name}.Clear(); + {handleRefill} + }}"); - deltaHandleFields.Append($@" + deltaHandleFields.Append($@" var {name}Value = {cast} {fieldHandleValue}; if ({name}Value == null) component.{name} = null!; - else - component.{name} = new {nullCast}({name}Value);"); + else if (component.{name} == null) + component.{name} = new {nullCast}({name}Value); + else if (!ReferenceEquals(component.{name}, {name}Value)) + {{ + component.{name}.Clear(); + {deltaRefill} + }}"); + + deltaApply.Add($"fullState.{name} = {name} == null ? null! : new({name});"); + } + else + { + handleStateSetters.Append($@" + if (!ReferenceEquals(component.{name}, state.{name})) + {{ + component.{name}.Clear(); + {handleRefill} + }}"); - shallowClone.Append($@" - {name} = this.{name},"); + deltaHandleFields.Append($@" + var {name}Value = {cast} {fieldHandleValue}; + if (!ReferenceEquals(component.{name}, {name}Value)) + {{ + component.{name}.Clear(); + {deltaRefill} + }}"); + + deltaApply.Add($"fullState.{name} = new({name});"); + } - deltaApply.Add($"fullState.{name} = {name} == null ? null! : new({name});"); + AppendCollectionClone(name, nullable); } else { @@ -449,8 +522,7 @@ public class ComponentNetworkGenerator : ISourceGenerator deltaHandleFields.Append($@" component.{name} = {cast} {fieldHandleValue};"); - shallowClone.Append($@" - {name} = this.{name},"); + AppendShallowClone(name); deltaApply.Add($"fullState.{name} = {name};"); } @@ -463,14 +535,22 @@ public class ComponentNetworkGenerator : ISourceGenerator */ networkedTypes.Add(networkedType); + clientGetField ??= getField; getStateInit.Append($@" {name} = {getField},"); + clientGetStateInit.Append($@" + {name} = {clientGetField},"); + deltaGetFields.Append(@$" {name} = {getField} }}; return;"); + clientDeltaGetFields.Append(@$" {name} = {clientGetField} + }}; + return;"); + deltaHandleFields.Append(@" break; } @@ -478,6 +558,7 @@ public class ComponentNetworkGenerator : ISourceGenerator } var deltaGetState = ""; + var clientDeltaGetState = ""; var deltaInterface = ""; var deltaCompFields = ""; var deltaNetRegister = ""; @@ -540,6 +621,21 @@ public void ApplyToFullState({stateName} fullState) }} }}"; + clientDeltaGetState = @$"// Delta state + if (component is IComponentDelta delta && args.FromTick > component.CreationTick) + {{ + var aspects = EntityManager.GetModifiedAspects(component, args.FromTick); + + // Try and get a matching delta state for the relevant dirty fields, otherwise fall back to full state. + switch (aspects) + {{ + case >= DeltaAspect.Unclassified: + break;{clientDeltaGetFields} + default: + break; + }} + }}"; + deltaInterface = " : IComponentDelta"; deltaCompFields = @$"/// @@ -552,6 +648,7 @@ public void ApplyToFullState({stateName} fullState) if (!fieldDeltas) { var eventRaise = ""; + var stateSetters = TrimNewLines(handleStateSetters); if (raiseAfterAutoHandle) { eventRaise = @" @@ -563,12 +660,13 @@ public void ApplyToFullState({stateName} fullState) handleState = $@" if (args.Current is not {stateName} state) return; -{handleStateSetters}{eventRaise}"; + +{stateSetters}{eventRaise}"; } else { // Re-indent handleStateSetters so it aligns with the switch block - var stateSetters = handleStateSetters.ToString(); + var stateSetters = TrimNewLines(handleStateSetters); stateSetters = stateSetters.Replace(" ", " "); @@ -598,6 +696,24 @@ public void ApplyToFullState({stateName} fullState) } var outSb = new StringBuilder(); + var stateFieldsText = TrimNewLines(stateFields); + var getStateInitText = TrimNewLines(getStateInit); + var clientGetStateInitText = TrimNewLines(clientGetStateInit); + var cloneMethodText = TrimNewLines(cloneMethod); + var deltaGetStateText = TrimNewLines(deltaGetState); + var clientDeltaGetStateText = TrimNewLines(clientDeltaGetState); + var deltaCompFieldsText = TrimNewLines(deltaCompFields); + var fieldStatesText = TrimNewLines(fieldStates); + + var netManagerDependency = usesClientCollectionCopy + ? "[global::Robust.Shared.IoC.Dependency] private global::Robust.Shared.Network.INetManager _net = default!;" + : string.Empty; + var getStateSubscription = usesClientCollectionCopy + ? $@" if (_net.IsClient) + SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetStateClient); + else + SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);" + : $@" SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);"; outSb.Append(""" // @@ -617,57 +733,125 @@ public void ApplyToFullState({stateName} fullState) partialInfo.WriteHeader(outSb); - outSb.Append($$""" - {{deltaInterface}} - { - {{deltaCompFields}} + outSb.AppendLine(deltaInterface); + outSb.AppendLine("{"); - [System.Serializable, NetSerializable] - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - [RobustAutoGenerated] - public sealed class {{stateName}} : IComponentState - { - {{stateFields}} - {{cloneMethod}} - } + if (deltaCompFieldsText.Length != 0) + { + outSb.AppendLine(deltaCompFieldsText); + outSb.AppendLine(); + } - [RobustAutoGenerated] - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - public sealed class {{componentName}}_AutoNetworkSystem : EntitySystem - { - public override void Initialize() - { - {{deltaNetRegister}} - SubscribeLocalEvent<{{componentName}}, ComponentGetState>(OnGetState); - SubscribeLocalEvent<{{componentName}}, ComponentHandleState>(OnHandleState); - } + outSb.AppendLine(" [System.Serializable, NetSerializable]"); + outSb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + outSb.AppendLine(" [RobustAutoGenerated]"); + outSb.AppendLine($" public sealed class {stateName} : IComponentState"); + outSb.AppendLine(" {"); + outSb.AppendLine(stateFieldsText); - private void OnGetState(EntityUid uid, {{componentName}} component, ref ComponentGetState args) - { - {{deltaGetState}} + if (cloneMethodText.Length != 0) + { + outSb.AppendLine(); + outSb.AppendLine(cloneMethodText); + } - // Get full state - args.State = new {{stateName}} - { - {{getStateInit}} - }; - } + outSb.AppendLine(" }"); + outSb.AppendLine(); + outSb.AppendLine(" [RobustAutoGenerated]"); + outSb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]"); + outSb.AppendLine($" public sealed class {componentName}_AutoNetworkSystem : EntitySystem"); + outSb.AppendLine(" {"); - private void OnHandleState(EntityUid uid, {{componentName}} component, ref ComponentHandleState args) - { - {{handleState}} - } - } + if (netManagerDependency.Length != 0) + { + outSb.AppendLine($" {netManagerDependency}"); + outSb.AppendLine(); + } + + outSb.AppendLine(" public override void Initialize()"); + outSb.AppendLine(" {"); + + if (deltaNetRegister.Length != 0) + outSb.AppendLine($" {deltaNetRegister}"); + + outSb.AppendLine(getStateSubscription); + outSb.AppendLine($" SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState);"); + outSb.AppendLine(" }"); + outSb.AppendLine(); + outSb.AppendLine($" private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args)"); + outSb.AppendLine(" {"); - {{fieldStates}} + if (deltaGetStateText.Length != 0) + { + outSb.AppendLine(IndentFirstLine(deltaGetStateText, 12)); + outSb.AppendLine(); + } + + outSb.AppendLine(" // Get full state"); + outSb.AppendLine($" args.State = new {stateName}"); + outSb.AppendLine(" {"); + outSb.AppendLine(getStateInitText); + outSb.AppendLine(" };"); + outSb.AppendLine(" }"); + + if (usesClientCollectionCopy) + { + outSb.AppendLine(); + outSb.AppendLine($" private void OnGetStateClient(EntityUid uid, {componentName} component, ref ComponentGetState args)"); + outSb.AppendLine(" {"); + + if (clientDeltaGetStateText.Length != 0) + { + outSb.AppendLine(IndentFirstLine(clientDeltaGetStateText, 12)); + outSb.AppendLine(); } - """); + + outSb.AppendLine(" // Get full state"); + outSb.AppendLine($" args.State = new {stateName}"); + outSb.AppendLine(" {"); + outSb.AppendLine(clientGetStateInitText); + outSb.AppendLine(" };"); + outSb.AppendLine(" }"); + } + + outSb.AppendLine(); + outSb.AppendLine($" private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)"); + outSb.AppendLine(" {"); + outSb.AppendLine(TrimNewLines(handleState)); + outSb.AppendLine(" }"); + outSb.AppendLine(" }"); + + if (fieldStatesText.Length != 0) + { + outSb.AppendLine(); + outSb.AppendLine(fieldStatesText); + } + + outSb.AppendLine("}"); partialInfo.WriteFooter(outSb); return outSb.ToString(); } + private static string TrimNewLines(StringBuilder source) + { + return source.ToString().Trim('\r', '\n'); + } + + private static string TrimNewLines(string source) + { + return source.Trim('\r', '\n'); + } + + private static string IndentFirstLine(string source, int spaces) + { + if (source.Length == 0) + return source; + + return new string(' ', spaces) + source; + } + public void Execute(GeneratorExecutionContext context) { var comp = (CSharpCompilation) context.Compilation; diff --git a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs index e0a7a5fd075..43622177381 100644 --- a/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/AutoNetworkingTest.cs @@ -310,6 +310,7 @@ await server.WaitPost(() => Assert.That(state?.GetType().Name, Is.EqualTo("Field3_FieldComponentState")); } + } [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] diff --git a/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs index 9313bede30e..5c17b888c14 100644 --- a/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs +++ b/Robust.Shared.IntegrationTests/GameState/NoSharedReferencesTest.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using NUnit.Framework; using Robust.Client.GameStates; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; -using Robust.Shared.Network; using Robust.Shared.Serialization; +using Robust.Shared.Timing; using System.Linq; using System.Threading.Tasks; using Robust.Shared.Analyzers; @@ -27,16 +28,11 @@ public async Task ReferencesAreNotShared() { var serverOpts = new ServerIntegrationOptions { Pool = false }; var clientOpts = new ClientIntegrationOptions { Pool = false }; - var server = StartServer(serverOpts); - var client = StartClient(clientOpts); - - await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()); - var netMan = client.ResolveDependency(); + await using var pair = await StartConnectedPair(serverOpts, clientOpts); + var server = pair.Server; + var client = pair.Client; var clientGameStateManager = client.ResolveDependency(); - Assert.DoesNotThrow(() => client.SetConnectTarget(server)); - client.Post(() => netMan.ClientConnect(null!, 0, null!)); - // Set up map. server.Post(() => { @@ -92,18 +88,123 @@ await server.WaitPost(() => // wait for errors. await RunTicks(); - async Task RunTicks() + Task RunTicks() => RunTicksSync(server, client, 10); + } + + [Test] + public async Task DeltaStateCollectionReferencesAreNotShared() + { + var serverOpts = new ServerIntegrationOptions { Pool = false }; + var clientOpts = new ClientIntegrationOptions { Pool = false }; + await using var pair = await StartConnectedPair(serverOpts, clientOpts); + var server = pair.Server; + var client = pair.Client; + var clientGameStateManager = client.ResolveDependency(); + + server.Post(() => + { + server.System().CreateMap(); + }); + + await RunTicks(); + + EntityUid sPlayer = default; + EntityUid cPlayer = default; + List clientValues = default!; + DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState oldState = default!; + var sEntMan = server.EntMan; + + await server.WaitPost(() => + { + sPlayer = sEntMan.Spawn(); + sEntMan.AddComponent(sPlayer, new DeltaCollectionAutogeneratedComponent()); + sEntMan.AddComponent(sPlayer, new SingleDeltaCollectionAutogeneratedComponent()); + + var session = server.PlayerMan.Sessions.First(); + server.PlayerMan.SetAttachedEntity(session, sPlayer); + server.PlayerMan.JoinGame(session); + }); + + await server.WaitPost(() => + { + var comp = sEntMan.GetComponent(sPlayer); + var state = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) sEntMan + .GetComponentState(sEntMan.EventBus, comp, null, GameTick.Zero)!; + + Assert.That(state.Values, Is.EqualTo(comp.Values)); + Assert.That(ReferenceEquals(state.Values, comp.Values), Is.True); + }); + + await RunTicks(); + + await client.WaitPost(() => + { + var cEntMan = client.EntMan; + cPlayer = cEntMan.GetEntity(server.EntMan.GetNetEntity(sPlayer)); + Assert.That(cEntMan.TryGetComponent(cPlayer, out DeltaCollectionAutogeneratedComponent? comp)); + + var componentStates = clientGameStateManager.GetFullRep()[cEntMan.GetNetEntity(cPlayer)]; + oldState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) componentStates + .First(x => x.Value is DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) + .Value!; + + Assert.That(comp!.Values, Is.EqualTo(oldState.Values)); + Assert.That(ReferenceEquals(comp.Values, oldState.Values), Is.False); + clientValues = comp.Values; + + var localState = (DeltaCollectionAutogeneratedComponent.DeltaCollectionAutogeneratedComponent_AutoState) cEntMan + .GetComponentState(cEntMan.EventBus, comp, null, GameTick.Zero)!; + + Assert.That(localState.Values, Is.EqualTo(comp.Values)); + Assert.That(ReferenceEquals(localState.Values, comp.Values), Is.False); + + Assert.That(cEntMan.TryGetComponent(cPlayer, out SingleDeltaCollectionAutogeneratedComponent? singleComp)); + var singleState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) componentStates + .First(x => x.Value is SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) + .Value!; + + Assert.That(singleComp!.Values, Is.EqualTo(singleState.Values)); + Assert.That(ReferenceEquals(singleComp.Values, singleState.Values), Is.False); + + var singleLocalState = (SingleDeltaCollectionAutogeneratedComponent.SingleDeltaCollectionAutogeneratedComponent_AutoState) cEntMan + .GetComponentState(cEntMan.EventBus, singleComp, null, GameTick.Zero)!; + + Assert.That(singleLocalState.Values, Is.EqualTo(singleComp.Values)); + Assert.That(ReferenceEquals(singleLocalState.Values, singleComp.Values), Is.False); + }); + + await client.WaitPost(() => { - for (int i = 0; i < 10; i++) + var deltaState = new DeltaCollectionAutogeneratedComponent.Counter_FieldComponentState { - await server.WaitRunTicks(1); - await client.WaitRunTicks(1); - } - } - - await client.WaitPost(() => netMan.ClientDisconnect("")); - await server.WaitRunTicks(5); - await client.WaitRunTicks(5); + Counter = 1 + }; + var newState = deltaState.CreateNewFullState(oldState); + + Assert.That(newState, Is.Not.SameAs(oldState)); + Assert.That(newState.Counter, Is.EqualTo(1)); + Assert.That(newState.Values, Is.EqualTo(oldState.Values)); + Assert.That(ReferenceEquals(newState.Values, oldState.Values), Is.False); + }); + + await server.WaitPost(() => + { + var comp = sEntMan.GetComponent(sPlayer); + comp.Values.Clear(); + comp.Values.Add(4); + sEntMan.Dirty(sPlayer, comp); + }); + + await RunTicks(); + + await client.WaitPost(() => + { + var comp = client.EntMan.GetComponent(cPlayer); + Assert.That(comp.Values, Is.SameAs(clientValues)); + Assert.That(comp.Values, Is.EqualTo(new[] { 4 })); + }); + + Task RunTicks() => RunTicksSync(server, client, 10); } } @@ -116,3 +217,20 @@ public sealed partial class ExampleAutogeneratedComponent : Component [Serializable, NetSerializable] public sealed record ExampleObject(int Value); } + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class DeltaCollectionAutogeneratedComponent : Component +{ + [AutoNetworkedField] + public List Values = new() { 1, 2, 3 }; + + [AutoNetworkedField] + public int Counter; +} + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class SingleDeltaCollectionAutogeneratedComponent : Component +{ + [AutoNetworkedField] + public List Values = new() { 1, 2, 3 }; +} From 86464ca989146fa2fd0932427cb8bbef7e80769d Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 16 Jul 2026 06:38:23 -0400 Subject: [PATCH 176/178] Test building content master against engine in release configuration (#6557) --- .github/workflows/test-content.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-content.yml b/.github/workflows/test-content.yml index 2c751ffd986..2bdd0c06291 100644 --- a/.github/workflows/test-content.yml +++ b/.github/workflows/test-content.yml @@ -40,7 +40,9 @@ jobs: run: cp RobustToolbox/global.json . - name: Install dependencies run: dotnet restore - - name: Build + - name: Build (Release) + run: dotnet build --configuration Release --no-restore /m + - name: Build (Debug) run: dotnet build --configuration DebugOpt --no-restore /m - name: Content.Tests shell: pwsh From 7bfa10ec04bfc8f00956419609bd6ec370f9bbac Mon Sep 17 00:00:00 2001 From: metalgearsloth Date: Thu, 16 Jul 2026 21:03:06 +1000 Subject: [PATCH 177/178] Version: 283.0.0 --- MSBuild/Robust.Engine.Version.props | 2 +- RELEASE-NOTES.md | 40 +++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index 699ce228c59..befe6da3636 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 282.0.0 + 283.0.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 670b33eecea..5eb382e8722 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -39,11 +39,11 @@ END TEMPLATE--> ### New features -* Added a NetMessage.EstimateBufferSize() to provide an estimate of the initialCapacity required for NetMessages. This will make NetMessage take an existing adequately sized pooled buffer for your message. This is opt-in and will default to the old 4-byte size if not specified. +*None yet* ### Bugfixes -* Static + StaticSundries will now also be considered for grid-traversal when a grid moves over these entities. +*None yet* ### Other @@ -54,6 +54,42 @@ END TEMPLATE--> *None yet* +## 283.0.0 + +### Breaking changes + +* The CompNetworkGenerator now .Clears and .Adds for collections where possible on the client. +* Defer UI state application until the Update loop. +* Expose the file name for selected file in dialog. +* Removed unused GridEventHandler. + +### New features + +* Added a NetMessage.EstimateBufferSize() to provide an estimate of the initialCapacity required for NetMessages. This will make NetMessage take an existing adequately sized pooled buffer for your message. This is opt-in and will default to the old 4-byte size if not specified. +* Add AudioParams.AddVariation. +* Made AudioAuxiliaryComponent public. +* Added API for OwnedTexture to load a texture without caching it. +* Make TableContainer virtual and public. +* Try to make connection failure message more useful by not defaulting to the first message (on IPV4 or IPV6). +* Add methods for generating Color palettes. +* Expose FovRenderTarget for the viewport. +* Add TryComp(EntityUid, Type, IComponent?) proxy methods to EntitySystem. + +### Bugfixes + +* Static + StaticSundries will now also be considered for grid-traversal when a grid moves over these entities. +* Fix field deltas not supporting 1-field states. +* Fix interface cast in serialization generator. +* Dispose PVS session states on disconnect. +* Fix BaseWindow jittering on resize. + +### Internal + +* Improve clean and incremental build times around serialization generators. +* Cleanup engine warnings around transform handling. +* Made several internal performance improvements on debug and release. + + ## 282.0.0 ### Breaking changes From 9388f362d11d507f908abf1a35bdde8adccc9782 Mon Sep 17 00:00:00 2001 From: DrSmugleaf Date: Fri, 31 Jul 2026 01:16:33 -0700 Subject: [PATCH 178/178] Change Lidgren.Network back to space-wizards --- .gitmodules | 2 +- Lidgren.Network/Lidgren.Network | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index ced7b604ef7..4f7f4889a01 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/space-wizards/netserializer [submodule "Lidgren.Network"] path = Lidgren.Network/Lidgren.Network - url = https://github.com/ss14Starlight/SpaceWizards.Lidgren.Network.git + url = https://github.com/space-wizards/SpaceWizards.Lidgren.Network.git [submodule "XamlX"] path = XamlX url = https://github.com/space-wizards/XamlX diff --git a/Lidgren.Network/Lidgren.Network b/Lidgren.Network/Lidgren.Network index b060f75c6bd..68a5b883d5c 160000 --- a/Lidgren.Network/Lidgren.Network +++ b/Lidgren.Network/Lidgren.Network @@ -1 +1 @@ -Subproject commit b060f75c6bde4a0bd0623da15b7eecf9f4eca5cb +Subproject commit 68a5b883d5c3f5d4eabda8f04a2f4fd1afce12f4