From 63bb3ad3429f5b05478550684d38ff549e5e0831 Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Sun, 2 Aug 2026 14:17:07 +1000
Subject: [PATCH 01/50] Cache texture UVs (#6894)
* Cache texture UVs
O(0) beats doing basic maths operations. Followup work to make some hot drawing paths (fonts + RSIs) use the AtlasTexture methods instead.
* 2026-08-02: Support headless atlas textures
---
.../Graphics/AtlasTextureUvBenchmark.cs | 94 +++++++++++++++++++
Robust.Benchmarks/Robust.Benchmarks.csproj | 1 +
.../Graphics/AtlasTextureTest.cs | 27 ++++++
Robust.Client/Graphics/AtlasTexture.cs | 21 +++++
.../Graphics/Clyde/Clyde.RenderHandle.cs | 66 +++++++++++--
5 files changed, 202 insertions(+), 7 deletions(-)
create mode 100644 Robust.Benchmarks/Graphics/AtlasTextureUvBenchmark.cs
create mode 100644 Robust.Client.Tests/Graphics/AtlasTextureTest.cs
diff --git a/Robust.Benchmarks/Graphics/AtlasTextureUvBenchmark.cs b/Robust.Benchmarks/Graphics/AtlasTextureUvBenchmark.cs
new file mode 100644
index 000000000..194d5654b
--- /dev/null
+++ b/Robust.Benchmarks/Graphics/AtlasTextureUvBenchmark.cs
@@ -0,0 +1,94 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using Robust.Client.Graphics;
+using Robust.Shared.Graphics;
+using Robust.Shared.Maths;
+using ClydeRenderer = Robust.Client.Graphics.Clyde.Clyde;
+
+namespace Robust.Benchmarks.Graphics;
+
+[MemoryDiagnoser]
+public class AtlasTextureUvBenchmark
+{
+ private AtlasTexture _atlas = default!;
+ private Texture _texture = default!;
+ private UIBox2? _subRegion;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var sourceTexture = new ClydeRenderer.ClydeTexture((ClydeHandle) 42, (256, 256), false, null!);
+ GC.SuppressFinalize(sourceTexture);
+ _atlas = new AtlasTexture(sourceTexture, UIBox2.FromDimensions(48, 80, 16, 16));
+ _texture = _atlas;
+ _subRegion = null;
+ }
+
+ [Benchmark(Baseline = true)]
+ public DrawCall LegacyAtlasPath()
+ {
+ var sourceTexture = ExtractTexture(_texture, in _subRegion, out var region);
+ return new DrawCall((long) sourceTexture.TextureId, CalculateUvs(sourceTexture, region));
+ }
+
+ [Benchmark]
+ public DrawCall TextureCallerToAtlasOverload()
+ {
+ return DrawCached(_texture, in _subRegion);
+ }
+
+ [Benchmark]
+ public DrawCall StaticallyTypedAtlasCaller()
+ {
+ return DrawCached(_atlas);
+ }
+
+ private static DrawCall DrawCached(Texture texture, in UIBox2? subRegion)
+ {
+ if (subRegion == null && texture is AtlasTexture atlas)
+ return DrawCached(atlas);
+
+ var fallbackTexture = ExtractTexture(texture, in subRegion, out var region);
+ return new DrawCall((long) fallbackTexture.TextureId, CalculateUvs(fallbackTexture, region));
+ }
+
+ private static DrawCall DrawCached(AtlasTexture texture)
+ {
+ return new DrawCall((long) texture.ClydeTexture!.TextureId, texture.NormalizedSubRegion);
+ }
+
+ private static ClydeRenderer.ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegion, out UIBox2 region)
+ {
+ if (texture is AtlasTexture atlas)
+ {
+ texture = atlas.SourceTexture;
+ if (subRegion.HasValue)
+ {
+ var offset = atlas.SubRegion.TopLeft;
+ region = new UIBox2(subRegion.Value.TopLeft + offset, subRegion.Value.BottomRight + offset);
+ }
+ else
+ {
+ region = atlas.SubRegion;
+ }
+ }
+ else
+ {
+ region = subRegion ?? new UIBox2(0, 0, texture.Width, texture.Height);
+ }
+
+ return (ClydeRenderer.ClydeTexture) texture;
+ }
+
+ private static Box2 CalculateUvs(Texture texture, UIBox2 region)
+ {
+ var (width, height) = texture.Size;
+ return new Box2(
+ region.Left / width,
+ (height - region.Bottom) / height,
+ region.Right / width,
+ (height - region.Top) / height);
+ }
+
+ public readonly record struct DrawCall(long TextureId, Box2 TexCoords);
+}
diff --git a/Robust.Benchmarks/Robust.Benchmarks.csproj b/Robust.Benchmarks/Robust.Benchmarks.csproj
index f47c475bc..129d5a238 100644
--- a/Robust.Benchmarks/Robust.Benchmarks.csproj
+++ b/Robust.Benchmarks/Robust.Benchmarks.csproj
@@ -9,6 +9,7 @@
false
+
diff --git a/Robust.Client.Tests/Graphics/AtlasTextureTest.cs b/Robust.Client.Tests/Graphics/AtlasTextureTest.cs
new file mode 100644
index 000000000..dd35848a8
--- /dev/null
+++ b/Robust.Client.Tests/Graphics/AtlasTextureTest.cs
@@ -0,0 +1,27 @@
+using NUnit.Framework;
+using Robust.Client.Graphics;
+using Robust.Shared.Maths;
+
+namespace Robust.Client.Tests.Graphics;
+
+[TestFixture]
+public sealed class AtlasTextureTest
+{
+ [Test]
+ public void AllowsNonClydeSourceTextures()
+ {
+ var source = new TestTexture((64, 64));
+ var atlas = new AtlasTexture(source, UIBox2.FromDimensions(8, 16, 32, 32));
+
+ Assert.That(atlas.SourceTexture, Is.SameAs(source));
+ Assert.That(atlas.ClydeTexture, Is.Null);
+ }
+
+ private sealed class TestTexture(Vector2i size) : Texture(size)
+ {
+ public override Color GetPixel(int x, int y)
+ {
+ return Color.Black;
+ }
+ }
+}
diff --git a/Robust.Client/Graphics/AtlasTexture.cs b/Robust.Client/Graphics/AtlasTexture.cs
index 6a983f8b0..3000189b0 100644
--- a/Robust.Client/Graphics/AtlasTexture.cs
+++ b/Robust.Client/Graphics/AtlasTexture.cs
@@ -2,6 +2,7 @@
using Robust.Shared.Graphics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
+using ClydeTextureImpl = Robust.Client.Graphics.Clyde.Clyde.ClydeTexture;
namespace Robust.Client.Graphics
{
@@ -21,6 +22,14 @@ public AtlasTexture(Texture texture, UIBox2 subRegion) : base((Vector2i) subRegi
SubRegion = subRegion;
SourceTexture = texture;
+ ClydeTexture = texture as ClydeTextureImpl;
+
+ var (width, height) = texture.Size;
+ NormalizedSubRegion = new Box2(
+ subRegion.Left / width,
+ (height - subRegion.Bottom) / height,
+ subRegion.Right / width,
+ (height - subRegion.Top) / height);
}
///
@@ -28,11 +37,23 @@ public AtlasTexture(Texture texture, UIBox2 subRegion) : base((Vector2i) subRegi
///
public Texture SourceTexture { get; }
+ ///
+ /// The Clyde texture backing this atlas texture.
+ ///
+ // Headless Clyde uses dummy textures. They are never drawn through the regular renderer,
+ // but atlas creation must still work for resources loaded by headless tests.
+ internal ClydeTextureImpl? ClydeTexture { get; }
+
///
/// Our sub region within our source, in pixel coordinates.
///
public UIBox2 SubRegion { get; }
+ ///
+ /// Our sub region within the source texture, normalized for rendering.
+ ///
+ internal Box2 NormalizedSubRegion { get; }
+
public override Color GetPixel(int x, int y)
{
DebugTools.Assert(x < SubRegion.Right);
diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
index f392de2ac..ac094d864 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
@@ -68,6 +68,12 @@ public void SetProjView(in Matrix3x2 proj, in Matrix3x2 view)
public void DrawTextureScreen(Texture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
in Color modulate, in UIBox2? subRegion)
{
+ if (subRegion == null && texture is AtlasTexture atlas)
+ {
+ DrawTextureScreen(atlas, bl, br, tl, tr, in modulate);
+ return;
+ }
+
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
var (w, h) = clydeTexture.Size;
@@ -76,6 +82,14 @@ public void DrawTextureScreen(Texture texture, Vector2 bl, Vector2 br, Vector2 t
_clyde.DrawTexture(clydeTexture.TextureId, bl, br, tl, tr, in modulate, in sr);
}
+ public void DrawTextureScreen(AtlasTexture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
+ in Color modulate)
+ {
+ var texCoords = texture.NormalizedSubRegion;
+ _clyde.DrawTexture(texture.ClydeTexture!.TextureId, bl, br, tl, tr, in modulate,
+ in texCoords);
+ }
+
///
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set
@@ -91,6 +105,12 @@ public void DrawTextureScreen(Texture texture, Vector2 bl, Vector2 br, Vector2 t
public void DrawTextureWorld(Texture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
Color modulate, in UIBox2? subRegion)
{
+ if (subRegion == null && texture is AtlasTexture atlas)
+ {
+ DrawTextureWorld(atlas, bl, br, tl, tr, modulate);
+ return;
+ }
+
var clydeTexture = ExtractTexture(texture, in subRegion, out var csr);
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
@@ -98,20 +118,55 @@ 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 DrawTextureWorld(AtlasTexture texture, Vector2 bl, Vector2 br, Vector2 tl, Vector2 tr,
+ Color modulate)
+ {
+ var texCoords = texture.NormalizedSubRegion;
+ _clyde.DrawTexture(texture.ClydeTexture!.TextureId, bl, br, tl, tr, in modulate,
+ in texCoords);
+ }
+
public void DrawTextureWorldBatch(Texture texture, ReadOnlySpan rects, Color modulate)
{
+ if (texture is AtlasTexture atlas)
+ {
+ DrawTextureWorldBatch(atlas, rects, modulate);
+ return;
+ }
+
var clydeTexture = ExtractTexture(texture, null, out var csr);
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
_clyde.DrawTextureBatch(clydeTexture.TextureId, rects, modulate, in sr);
}
+ public void DrawTextureWorldBatch(AtlasTexture texture, ReadOnlySpan rects,
+ Color modulate)
+ {
+ var texCoords = texture.NormalizedSubRegion;
+ _clyde.DrawTextureBatch(texture.ClydeTexture!.TextureId, rects, modulate,
+ in texCoords);
+ }
+
public void DrawTextureWorldBatchUnmodulated(Texture texture, ReadOnlySpan rects)
{
+ if (texture is AtlasTexture atlas)
+ {
+ DrawTextureWorldBatchUnmodulated(atlas, rects);
+ return;
+ }
+
var clydeTexture = ExtractTexture(texture, null, out var csr);
var sr = WorldTextureBoundsToUV(clydeTexture, csr);
_clyde.DrawTextureBatchUnmodulated(clydeTexture.TextureId, rects, in sr);
}
+ public void DrawTextureWorldBatchUnmodulated(AtlasTexture texture, ReadOnlySpan rects)
+ {
+ var texCoords = texture.NormalizedSubRegion;
+ _clyde.DrawTextureBatchUnmodulated(texture.ClydeTexture!.TextureId, rects,
+ in texCoords);
+ }
+
public void DrawRectWorldBatch(ReadOnlySpan rects, Color modulate)
{
_clyde.DrawRectBatch(_whiteClydeTexture.TextureId, rects, modulate, in _whiteUv);
@@ -135,7 +190,6 @@ internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegio
{
if (texture is AtlasTexture atlas)
{
- texture = atlas.SourceTexture;
if (subRegion.HasValue)
{
var offset = atlas.SubRegion.TopLeft;
@@ -147,14 +201,12 @@ internal static ClydeTexture ExtractTexture(Texture texture, in UIBox2? subRegio
{
sr = atlas.SubRegion;
}
- }
- else
- {
- sr = subRegion ?? new UIBox2(0, 0, texture.Width, texture.Height);
+
+ return atlas.ClydeTexture!;
}
- var clydeTexture = (ClydeTexture) texture;
- return clydeTexture;
+ sr = subRegion ?? new UIBox2(0, 0, texture.Width, texture.Height);
+ return (ClydeTexture) texture;
}
public void RenderInRenderTarget(IRenderTarget target, Action a, Color? clearColor)
From b19ec9b72b026520ca09c909933544399f6faf51 Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Mon, 3 Aug 2026 21:09:42 +1000
Subject: [PATCH 02/50] Remove redundant MsgEntity data (#6906)
---
Robust.Shared/Network/Messages/MsgEntity.cs | 2 --
1 file changed, 2 deletions(-)
diff --git a/Robust.Shared/Network/Messages/MsgEntity.cs b/Robust.Shared/Network/Messages/MsgEntity.cs
index add8278d2..c614d7365 100644
--- a/Robust.Shared/Network/Messages/MsgEntity.cs
+++ b/Robust.Shared/Network/Messages/MsgEntity.cs
@@ -18,8 +18,6 @@ public sealed class MsgEntity : NetMessage
public EntityMessageType Type { get; set; }
public EntityEventArgs SystemMessage { get; set; }
- public EntityUid EntityUid { get; set; }
- public uint NetId { get; set; }
public uint Sequence { get; set; }
public GameTick SourceTick { get; set; }
From 05fec545a07452d2c8c64e20d9c2a346099586b6 Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Tue, 4 Aug 2026 19:46:29 -0700
Subject: [PATCH 03/50] Make SerializationManager.TryGetTypeNodeSerializer 65
times faster (#6915)
---
.../Manager/SerializationManager.Copying.cs | 14 +-
.../Manager/SerializationManager.Reading.cs | 27 +-
...SerializationManager.SerializerProvider.cs | 409 ++++++++++++------
.../Manager/SerializationManager.cs | 12 +-
4 files changed, 311 insertions(+), 151 deletions(-)
diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs
index 4302934d2..b991c33e8 100644
--- a/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs
+++ b/Robust.Shared/Serialization/Manager/SerializationManager.Copying.cs
@@ -372,7 +372,7 @@ private T CreateCopyInternal(T source, SerializationHookContext hookCtx, ISer
var generated = Unsafe.As>(source);
var target = generated.Instantiate();
generated.Copy(ref target, this, hookCtx, context);
- RunAfterHook(target, hookCtx);
+ TryRunAfterHook(target, hookCtx);
return target;
}
else
@@ -474,7 +474,7 @@ public void CopyTo(
var generated = Unsafe.As>(source);
target ??= generated.Instantiate();
generated.Copy(ref target, this, hookCtx, context);
- RunAfterHook(target, hookCtx);
+ TryRunAfterHook(target, hookCtx);
return;
}
@@ -489,7 +489,7 @@ public void CopyTo(
target = CreateCopy(source, hookCtx, context);
}
- RunAfterHook(target, hookCtx);
+ TryRunAfterHook(target, hookCtx);
}
public void CopyTo(ITypeCopier copier, T source, ref T target, ISerializationContext? context = null,
@@ -534,7 +534,7 @@ public void CopyTo(
}
copier.CopyTo(this, source, ref target, DependencyCollection, hookCtx, context);
- RunAfterHook(target, hookCtx);
+ TryRunAfterHook(target, hookCtx);
}
public void CopyTo(T source, ref T target, ISerializationContext? context = null, bool skipHook = false, bool notNullableOverride = false)
@@ -607,13 +607,13 @@ public T CreateCopy(
var generated = Unsafe.As>(source);
var target = generated.Instantiate();
generated.Copy(ref target, this, hookCtx, context);
- RunAfterHook(target, hookCtx);
+ TryRunAfterHook(target, hookCtx);
return target;
}
var res = GetOrCreateCreateCopyGenericDelegate()(source, hookCtx, context);
- RunAfterHook(res, hookCtx);
+ TryRunAfterHook(res, hookCtx);
return res;
}
@@ -643,7 +643,7 @@ public T CreateCopy(
}
var res = copyCreator.CreateCopy(this, source, DependencyCollection, hookCtx, context);
- RunAfterHook(res, hookCtx);
+ TryRunAfterHook(res, hookCtx);
return res;
}
diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs
index c187ff7e4..fb8bfdfa9 100644
--- a/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs
+++ b/Robust.Shared/Serialization/Manager/SerializationManager.Reading.cs
@@ -193,6 +193,7 @@ public T ReadStructDefinition(
{
var baseType = typeof(T);
var nullable = baseType.IsNullable();
+ var isArray = ReadTypeMetadata.IsArray;
T val = default!;
@@ -321,7 +322,7 @@ void RegularRead()
if (!hasSerializer)
{
- if (baseType.IsArray)
+ if (isArray)
{
val = node switch
{
@@ -381,7 +382,7 @@ void RegularRead()
throw new ArgumentException($"No mapping or value node provided for type {baseType}.");
}
- RunAfterHook(val, hookCtx);
+ TryRunAfterHook(val, hookCtx);
}
}
}
@@ -581,7 +582,8 @@ public T Read(
}
var baseType = typeof(T);
- if (baseType.IsEnum || baseType.IsArray ||
+ if (baseType.IsEnum ||
+ ReadTypeMetadata.IsArray ||
(baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(Nullable<>)))
{
return ((ReadGenericDelegate)_readGenericDelegates.GetOrAdd((typeof(T), node.GetType()!, notNullableOverride),
@@ -771,7 +773,7 @@ void RegularRead()
throw new ArgumentException($"No mapping or value node provided for type {baseType}.");
}
- RunAfterHook(val, hookCtx);
+ TryRunAfterHook(val, hookCtx);
}
}
}
@@ -992,7 +994,7 @@ void RegularRead()
throw new ArgumentException($"No mapping or value node provided for type {baseType}.");
}
- RunAfterHook(val, hookCtx);
+ TryRunAfterHook(val, hookCtx);
}
}
}
@@ -1427,7 +1429,7 @@ private TValue ReadGenericValue(
throw new ArgumentException($"No mapping node provided for type {type} at line: {node.Start.Line}");
}
- RunAfterHook(instance, hookCtx);
+ TryRunAfterHook(instance, hookCtx);
return instance;
}
@@ -1447,7 +1449,7 @@ private TValue ReadGenericMapping(
definition.Populate(ref instance, node, this, hookCtx, context);
- RunAfterHook(instance, hookCtx);
+ TryRunAfterHook(instance, hookCtx);
return instance;
}
@@ -1456,5 +1458,16 @@ 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");
}
+
+ private static class ReadTypeMetadata
+ {
+ // ReSharper disable once StaticMemberInGenericType
+ public static bool IsArray;
+
+ static ReadTypeMetadata()
+ {
+ IsArray = typeof(T).IsArray;
+ }
+ }
}
}
diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs
index e5a0d8cbf..f297ac255 100644
--- a/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs
+++ b/Robust.Shared/Serialization/Manager/SerializationManager.SerializerProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
@@ -10,6 +11,9 @@
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.Serialization.TypeSerializers.Interfaces;
using Robust.Shared.Utility;
@@ -31,6 +35,28 @@ public sealed partial class SerializationManager
typeof(ITypeWriter<>)
}.ToImmutableArray();
+ private static readonly ImmutableArray Nodes = new[]
+ {
+ typeof(MappingDataNode),
+ typeof(SequenceDataNode),
+ typeof(ValueDataNode),
+ }.ToImmutableArray();
+
+ ///
+ ///
+ ///
+ private const int ReaderIndex = 0;
+
+ ///
+ ///
+ ///
+ private const int InheritanceHandlerIndex = 1;
+
+ ///
+ ///
+ ///
+ private const int ValidatorIndex = 2;
+
///
///
///
@@ -41,12 +67,42 @@ public sealed partial class SerializationManager
///
private const int CopierIndex = 4;
+ ///
+ ///
+ ///
+ private const int WriterIndex = 5;
+
+ ///
+ ///
+ ///
+ private const int MappingIndex = 0;
+
+ ///
+ ///
+ ///
+ private const int SequenceIndex = 1;
+
+ ///
+ ///
+ ///
+ private const int ValueIndex = 2;
+
private SerializerProvider _regularSerializerProvider = default!;
private ISawmill _serializerSawmill = default!;
private void InitializeTypeSerializers(IEnumerable typeSerializers)
{
+ DebugTools.AssertEqual(ReaderIndex, SerializerInterfaces.IndexOf(typeof(ITypeReader<,>)));
+ DebugTools.AssertEqual(InheritanceHandlerIndex, SerializerInterfaces.IndexOf(typeof(ITypeInheritanceHandler<,>)));
+ DebugTools.AssertEqual(ValidatorIndex, SerializerInterfaces.IndexOf(typeof(ITypeValidator<,>)));
+ DebugTools.AssertEqual(CopyCreatorIndex, SerializerInterfaces.IndexOf(typeof(ITypeCopyCreator<>)));
+ DebugTools.AssertEqual(CopierIndex, SerializerInterfaces.IndexOf(typeof(ITypeCopier<>)));
+
+ DebugTools.AssertEqual(MappingIndex, Nodes.IndexOf(typeof(MappingDataNode)));
+ DebugTools.AssertEqual(SequenceIndex, Nodes.IndexOf(typeof(SequenceDataNode)));
+ DebugTools.AssertEqual(ValueIndex, Nodes.IndexOf(typeof(ValueDataNode)));
+
_regularSerializerProvider = new(this, typeSerializers);
}
@@ -62,6 +118,10 @@ private object CreateSerializer(Type type)
ser.SerMan = this;
ser.Log = _serializerSawmill;
}
+
+ if (result is IPostInjectInit postInject)
+ postInject.PostInject();
+
return result;
}
@@ -102,7 +162,7 @@ public bool TryCustomCopy(T source, ref T target, SerializationHookContext ho
public sealed class SerializerProvider
{
- private SerializationManager _ser;
+ private readonly SerializationManager _ser;
public SerializerProvider(ISerializationManager ser, IEnumerable typeSerializers) : this(ser)
{
@@ -122,8 +182,9 @@ public SerializerProvider(ISerializationManager ser)
}
}
- private Dictionary> _typeNodeSerializers = new();
- private Dictionary> _typeSerializers = new();
+ private (object? Regular, object? Generic, bool Init)[] _typeNodeSerializersArray = [];
+ private readonly ConcurrentDictionary> _typeNodeSerializers = new();
+ private readonly ConcurrentDictionary> _typeSerializers = new();
// TODO make this a 1d array containing the 6 interfaces
///
@@ -131,15 +192,15 @@ public SerializerProvider(ISerializationManager ser)
/// that they serialize.
/// for the first index.
///
- private (object? Regular, object? Generic)[]?[] _typeSerializersArray = new (object? Regular, object? Generic)[]?[] { };
+ private (object? Regular, object? Generic)[]?[] _typeSerializersArray = [];
- private Dictionary> _genericTypeNodeSerializers = new();
- private Dictionary> _genericTypeSerializers = new();
+ private readonly ConcurrentDictionary> _genericTypeNodeSerializers = new();
+ private readonly ConcurrentDictionary> _genericTypeSerializers = new();
- private List _typeNodeInterfaces = new();
- private List _typeInterfaces = new();
+ private readonly List _typeNodeInterfaces = new();
+ private readonly List _typeInterfaces = new();
- private readonly object _lock = new();
+ private readonly Lock _lock = new();
#region GetSerializerMethods
@@ -148,11 +209,51 @@ public bool TryGetTypeNodeSerializer([NotNullWhen(true
where TNode : DataNode
{
serializer = default;
- if (!TryGetTypeNodeSerializer(typeof(TInterface).GetGenericTypeDefinition(), typeof(TType), typeof(TNode), out var rawSerializer))
+ object? rawSerializer;
+ var index = TypeSerializerType.Index;
+ if (index < _typeNodeSerializersArray.Length)
+ {
+ ref var serializers = ref _typeNodeSerializersArray[index];
+ if (serializers.Init)
+ {
+ if (serializers.Regular != null)
+ {
+ serializer = (TInterface) serializers.Regular;
+ return true;
+ }
+
+ if (serializers.Generic != null)
+ {
+ serializer = (TInterface) serializers.Generic;
+ return true;
+ }
+
+ return false;
+ }
+
+ if (TryGetTypeNodeSerializer(typeof(TInterface).GetGenericTypeDefinition(),
+ typeof(TType),
+ typeof(TNode),
+ out rawSerializer))
+ {
+ serializer = (TInterface) rawSerializer;
+ return true;
+ }
+
+ serializers.Init = true;
return false;
+ }
- serializer = (TInterface)rawSerializer;
- return true;
+ if (TryGetTypeNodeSerializer(typeof(TInterface).GetGenericTypeDefinition(),
+ typeof(TType),
+ typeof(TNode),
+ out rawSerializer))
+ {
+ serializer = (TInterface) rawSerializer;
+ return true;
+ }
+
+ return false;
}
internal bool TryGetTypeNodeSerializerArray([NotNullWhen(true)] out TInterface? serializer)
@@ -169,30 +270,28 @@ internal bool TryGetTypeNodeSerializerArray([NotNullWh
public bool TryGetTypeNodeSerializer(Type interfaceType, Type objectType, Type nodeType, [NotNullWhen(true)] out object? serializer)
{
- lock (_lock)
- {
- if (_typeNodeSerializers.TryGetValue(interfaceType, out var typeNodeSerializers) &&
- typeNodeSerializers.TryGetValue((objectType, nodeType), out serializer))
- return true;
+ if (_typeNodeSerializers.TryGetValue(interfaceType, out var typeNodeSerializers) &&
+ typeNodeSerializers.TryGetValue((objectType, nodeType), out serializer))
+ return true;
- if (_genericTypeNodeSerializers.TryGetValue(interfaceType, out var genericTypeNodeSerializers) &&
- objectType.IsGenericType)
+ if (_genericTypeNodeSerializers.TryGetValue(interfaceType, out var genericTypeNodeSerializers) &&
+ objectType.IsGenericType)
+ {
+ var typeDef = objectType.GetGenericTypeDefinition();
+ foreach (var (key, val) in genericTypeNodeSerializers)
{
- var typeDef = objectType.GetGenericTypeDefinition();
- foreach (var (key, val) in genericTypeNodeSerializers)
- {
- if (typeDef.HasSameMetadataDefinitionAs(key.ObjectType) && nodeType == key.NodeType)
- {
- var serializerType = val.MakeGenericType(objectType.GetGenericArguments());
- serializer = RegisterSerializer(serializerType)!;
- return true;
- }
- }
- }
+ if (!typeDef.HasSameMetadataDefinitionAs(key.ObjectType) || nodeType != key.NodeType)
+ continue;
- serializer = null;
- return false;
+ var serializerType = val.MakeGenericType(objectType.GetGenericArguments());
+ serializer = RegisterSerializer(serializerType)!;
+ RegisterIndexedNodeSerializer(interfaceType, objectType, key.NodeType, serializer, false);
+ return true;
+ }
}
+
+ serializer = null;
+ return false;
}
public TInterface GetTypeNodeSerializer()
@@ -226,31 +325,34 @@ public bool TryGetTypeSerializer([NotNullWhen(true)] out TInt
public bool TryGetTypeSerializer(Type interfaceType, Type objectType, [NotNullWhen(true)] out object? serializer)
{
- lock (_lock)
- {
- if (_typeSerializers.TryGetValue(interfaceType, out var typeSerializers) &&
- typeSerializers.TryGetValue(objectType, out serializer))
- return true;
+ if (_typeSerializers.TryGetValue(interfaceType, out var typeSerializers) &&
+ typeSerializers.TryGetValue(objectType, out serializer))
+ return true;
- if (_genericTypeSerializers.TryGetValue(interfaceType, out var genericTypeSerializers) &&
- objectType.IsGenericType)
+ if (_genericTypeSerializers.TryGetValue(interfaceType, out var genericTypeSerializers) &&
+ objectType.IsGenericType)
+ {
+ var typeDef = objectType.GetGenericTypeDefinition();
+ foreach (var (key, val) in genericTypeSerializers)
{
- var typeDef = objectType.GetGenericTypeDefinition();
- foreach (var (key, val) in genericTypeSerializers)
- {
- if (typeDef.HasSameMetadataDefinitionAs(key))
- {
- var serializerType = val.MakeGenericType(objectType.GetGenericArguments());
- serializer = RegisterSerializer(serializerType)!;
- RegisterIndexedSerializer(objectType, SerializerInterfaces.IndexOf(interfaceType), serializer, false);
- return true;
- }
- }
- }
+ if (!typeDef.HasSameMetadataDefinitionAs(key))
+ continue;
+
+ var serializerType = val.MakeGenericType(objectType.GetGenericArguments());
+ serializer = RegisterSerializer(serializerType)!;
+ RegisterIndexedSerializer(
+ objectType,
+ SerializerInterfaces.IndexOf(interfaceType),
+ serializer,
+ false
+ );
- serializer = null;
- return false;
+ return true;
+ }
}
+
+ serializer = null;
+ return false;
}
internal bool TryGetCopierOrCreator(out ITypeCopier? copier, out ITypeCopyCreator? copyCreator)
@@ -311,95 +413,100 @@ public object GetTypeSerializer(Type interfaceType, Type objectType)
private object RegisterSerializer(Type type, object obj)
{
- lock (_lock)
+ foreach (var @interface in type.GetInterfaces())
{
- foreach (var @interface in type.GetInterfaces())
- {
- if (!@interface.IsGenericType) continue;
+ if (!@interface.IsGenericType) continue;
- for (var i = 0; i < _typeInterfaces.Count; i++)
- {
- var typeInterface = _typeInterfaces[i];
- if (@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
- {
- var arguments = @interface.GetGenericArguments();
- if (arguments.Length != 1)
- throw new InvalidGenericParameterCountException();
- _typeSerializers.GetOrNew(typeInterface).Add(arguments[0], obj);
- RegisterIndexedSerializer(arguments[0], SerializerInterfaces.IndexOf(typeInterface), obj, true);
- }
- }
-
- foreach (var typeInterface in _typeNodeInterfaces)
- {
- if (@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
- {
- var arguments = @interface.GetGenericArguments();
- if (arguments.Length != 2)
- throw new InvalidGenericParameterCountException();
- _typeNodeSerializers.GetOrNew(typeInterface).Add((arguments[0], arguments[1]), obj);
- }
- }
+ foreach (var typeInterface in _typeInterfaces)
+ {
+ if (!@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
+ continue;
+
+ var arguments = @interface.GetGenericArguments();
+ if (arguments.Length != 1)
+ throw new InvalidGenericParameterCountException();
+
+ _typeSerializers.GetOrNew(typeInterface).TryAdd(arguments[0], obj);
+ RegisterIndexedSerializer(
+ arguments[0],
+ SerializerInterfaces.IndexOf(typeInterface),
+ obj,
+ true
+ );
}
- return obj;
+ foreach (var typeInterface in _typeNodeInterfaces)
+ {
+ if (!@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
+ continue;
+
+ var arguments = @interface.GetGenericArguments();
+ if (arguments.Length != 2)
+ throw new InvalidGenericParameterCountException();
+
+ _typeNodeSerializers.GetOrAdd(typeInterface, _ => new())
+ .TryAdd((arguments[0], arguments[1]), obj);
+ RegisterIndexedNodeSerializer(
+ typeInterface,
+ arguments[0],
+ arguments[1],
+ obj,
+ true
+ );
+ }
}
+
+ return obj;
}
public T? RegisterSerializer() => (T?)RegisterSerializer(typeof(T));
public object? RegisterSerializer(Type type)
{
- lock (_lock)
+ if (!type.IsGenericTypeDefinition)
+ return RegisterSerializer(type, _ser.CreateSerializer(type));
+
+ var typeArguments = type.GetGenericArguments();
+ foreach (var @interface in type.GetInterfaces())
{
- if (type.IsGenericTypeDefinition)
+ foreach (var typeInterface in _typeInterfaces)
{
- var typeArguments = type.GetGenericArguments();
- foreach (var @interface in type.GetInterfaces())
+ if (!@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
+ continue;
+
+ var arguments = @interface.GetGenericArguments();
+ if (arguments.Length != 1)
+ throw new InvalidGenericParameterCountException();
+ var objArguments = arguments[0].GetGenericArguments();
+ for (var i = 0; i < typeArguments.Length; i++)
{
- foreach (var typeInterface in _typeInterfaces)
- {
- if (@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
- {
- var arguments = @interface.GetGenericArguments();
- if (arguments.Length != 1)
- throw new InvalidGenericParameterCountException();
- var objArguments = arguments[0].GetGenericArguments();
- for (int i = 0; i < typeArguments.Length; i++)
- {
- if (typeArguments[i] != objArguments[i])
- throw new GenericParameterMismatchException();
- }
-
- _genericTypeSerializers.GetOrNew(typeInterface).Add(arguments[0], type);
- }
- }
-
- foreach (var typeInterface in _typeNodeInterfaces)
- {
- if (@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
- {
- var arguments = @interface.GetGenericArguments();
- if (arguments.Length != 2)
- throw new InvalidGenericParameterCountException();
- var objArguments = arguments[0].GetGenericArguments();
- for (int i = 0; i < typeArguments.Length; i++)
- {
- if (typeArguments[i] != objArguments[i])
- throw new GenericParameterMismatchException();
- }
-
- _genericTypeNodeSerializers.GetOrNew(typeInterface)
- .Add((arguments[0], arguments[1]), type);
- }
- }
+ if (typeArguments[i] != objArguments[i])
+ throw new GenericParameterMismatchException();
}
- return null;
+ _genericTypeSerializers.GetOrNew(typeInterface).TryAdd(arguments[0], type);
}
- return RegisterSerializer(type, _ser.CreateSerializer(type));
+ foreach (var typeInterface in _typeNodeInterfaces)
+ {
+ if (!@interface.GetGenericTypeDefinition().HasSameMetadataDefinitionAs(typeInterface))
+ continue;
+
+ var arguments = @interface.GetGenericArguments();
+ if (arguments.Length != 2)
+ throw new InvalidGenericParameterCountException();
+ var objArguments = arguments[0].GetGenericArguments();
+ for (var i = 0; i < typeArguments.Length; i++)
+ {
+ if (typeArguments[i] != objArguments[i])
+ throw new GenericParameterMismatchException();
+ }
+
+ _genericTypeNodeSerializers.GetOrNew(typeInterface).TryAdd((arguments[0], arguments[1]), type);
+ }
}
+
+ return null;
}
//todo paul serv3 is there a better way than comparing names here?
@@ -420,7 +527,7 @@ private void RegisterSerializerInterface(Type type)
if (genericInterface.HasSameMetadataDefinitionAs(genericTypeNode))
{
var genericInterfaceParams = genericInterface.GetGenericArguments();
- for (int i = 0; i < genericParams.Length; i++)
+ for (var i = 0; i < genericParams.Length; i++)
{
if (genericParams[i].Name != genericInterfaceParams[i].Name)
throw new GenericParameterMismatchException();
@@ -431,7 +538,7 @@ private void RegisterSerializerInterface(Type type)
else if (genericInterface.HasSameMetadataDefinitionAs(genericType))
{
var genericInterfaceParams = genericInterface.GetGenericArguments();
- for (int i = 0; i < genericParams.Length; i++)
+ for (var i = 0; i < genericParams.Length; i++)
{
if (genericParams[i].Name != genericInterfaceParams[i].Name)
throw new GenericParameterMismatchException();
@@ -447,9 +554,7 @@ private void RegisterIndexedSerializer(Type elementType, int interfaceIndex, obj
{
var id = SerializedType.GetId(elementType);
if (id >= _typeSerializersArray.Length)
- {
Array.Resize(ref _typeSerializersArray, (id + 1) * 2);
- }
var array = _typeSerializersArray[id];
if (array == null)
@@ -459,12 +564,26 @@ private void RegisterIndexedSerializer(Type elementType, int interfaceIndex, obj
}
if (regular)
- {
array[interfaceIndex].Regular = serializer;
- }
else
- {
array[interfaceIndex].Generic = serializer;
+ }
+
+ private void RegisterIndexedNodeSerializer(Type interfaceIndex, Type elementType, Type nodeType, object serializer, bool regular)
+ {
+ lock (_lock)
+ {
+ var id = TypeSerializerType.GetId(interfaceIndex, elementType, nodeType);
+ if (id >= _typeNodeSerializersArray.Length)
+ Array.Resize(ref _typeNodeSerializersArray, (id + 1) * 2);
+
+ ref var tuple = ref _typeNodeSerializersArray[id];
+ if (regular)
+ tuple.Regular = serializer;
+ else
+ tuple.Generic = serializer;
+
+ tuple.Init = true;
}
}
@@ -474,7 +593,7 @@ private void RegisterIndexedSerializer(Type elementType, int interfaceIndex, obj
private static class SerializedType
{
internal static int Id;
- private static readonly object Lock = new();
+ private static readonly Lock Lock = new();
internal static int GetId(Type type)
{
@@ -515,4 +634,32 @@ public TypeInformation(int id, bool returnSource, bool serializationGenerated)
SerializationGenerated = serializationGenerated;
}
}
+
+ internal static class TypeSerializerType
+ {
+ internal static int GetId(Type typeInterface, Type type, Type typeNode)
+ {
+ var interfaceIndex = SerializerInterfaces.IndexOf(typeInterface.GetGenericTypeDefinition());
+ if (interfaceIndex == -1)
+ throw new ArgumentException($"Invalid type interface: {typeInterface}");
+
+ var nodeIndex = Nodes.IndexOf(typeNode);
+ if (nodeIndex == -1)
+ throw new ArgumentException($"Invalid node type: {typeInterface}");
+
+ return SerializedType.GetId(type) *
+ (SerializerInterfaces.Length + Nodes.Length) +
+ interfaceIndex +
+ nodeIndex;
+ }
+ }
+
+ internal static class TypeSerializerType
+ {
+ // ReSharper disable once StaticMemberInGenericType
+ internal static readonly int Index = SerializedType.Information.Id *
+ (SerializerInterfaces.Length + Nodes.Length) +
+ SerializerInterfaces.IndexOf(typeof(TInterface).GetGenericTypeDefinition()) +
+ Nodes.IndexOf(typeof(TNode));
+ }
}
diff --git a/Robust.Shared/Serialization/Manager/SerializationManager.cs b/Robust.Shared/Serialization/Manager/SerializationManager.cs
index a09bbd5e4..0a6b84623 100644
--- a/Robust.Shared/Serialization/Manager/SerializationManager.cs
+++ b/Robust.Shared/Serialization/Manager/SerializationManager.cs
@@ -371,17 +371,17 @@ private Type ResolveConcreteType(Type baseType, string typeName)
}
#pragma warning disable CS0618
- private static void RunAfterHook(TValue instance, SerializationHookContext ctx)
+ internal static void TryRunAfterHook(TValue instance, SerializationHookContext ctx)
{
+ if (ctx.SkipHooks)
+ return;
+
if (instance is ISerializationHooks hooks)
- RunAfterHookGenerated(hooks, ctx);
+ ForceRunAfterHookGenerated(hooks, ctx);
}
- private static void RunAfterHookGenerated(TValue instance, SerializationHookContext ctx) where TValue : ISerializationHooks
+ private static void ForceRunAfterHookGenerated(TValue instance, SerializationHookContext ctx) where TValue : ISerializationHooks
{
- if (ctx.SkipHooks)
- return;
-
DebugTools.Assert(!typeof(TValue).IsValueType, "ISerializationHooks must only be used on reference types");
if (ctx.DeferQueue != null)
From 93dcc2134502f0250366473c04e4790ce2fbb856 Mon Sep 17 00:00:00 2001
From: eoineoineoin
Date: Wed, 5 Aug 2026 03:47:11 +0100
Subject: [PATCH 04/50] Fix WordWrap bugs (#6908)
---
.../UserInterface/WordWrapTest.cs | 89 +++++++++++++++++++
Robust.Client/UserInterface/WordWrap.cs | 21 +----
2 files changed, 93 insertions(+), 17 deletions(-)
create mode 100644 Robust.Client.Tests/UserInterface/WordWrapTest.cs
diff --git a/Robust.Client.Tests/UserInterface/WordWrapTest.cs b/Robust.Client.Tests/UserInterface/WordWrapTest.cs
new file mode 100644
index 000000000..ed3657558
--- /dev/null
+++ b/Robust.Client.Tests/UserInterface/WordWrapTest.cs
@@ -0,0 +1,89 @@
+using NUnit.Framework;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using System.Text;
+
+namespace Robust.Client.Tests.UserInterface;
+
+[Parallelizable(ParallelScope.All)]
+public sealed class WordWrapTest
+{
+ private List GenerateBreaks(string s, int maxWidth)
+ {
+ var breaksOut = new List();
+ var wrapper = new WordWrap(maxSizeX: maxWidth);
+
+ // For simplicity, assume every character has the same width, except for some special ones
+ var charMetrics = new CharMetrics (bearingX: 0, bearingY: 0, advance: 10, width: 10, height: 10);
+ var wideMetrics = new CharMetrics (bearingX: 0, bearingY: 0, advance: 25, width: 25, height: 10);
+ var narrowMetrics = new CharMetrics (bearingX: 0, bearingY: 0, advance: 6, width: 6, height: 10);
+
+ foreach (var r in s.EnumerateRunes())
+ {
+ wrapper.NextRune(r, out var breakLine, out var breakNewLine, out var skip);
+ if (breakLine != null)
+ {
+ breaksOut.Add(breakLine.Value);
+ }
+ if (breakNewLine != null)
+ {
+ breaksOut.Add(breakNewLine.Value);
+ }
+ if (skip)
+ {
+ continue;
+ }
+
+ var metrics = charMetrics;
+ if (r == new Rune('W'))
+ {
+ metrics = wideMetrics;
+ }
+ else if (r == new Rune('|'))
+ {
+ metrics = narrowMetrics;
+ }
+
+ wrapper.NextMetrics(metrics, out breakLine, out var abort);
+
+ if (breakLine != null)
+ {
+ breaksOut.Add(breakLine.Value);
+ }
+ if (abort)
+ {
+ return breaksOut;
+ }
+ }
+
+ return breaksOut;
+ }
+
+ [Test]
+ // Basic wrapping. First two words fit on one line, need a break to fit the third
+ //Breaks at: v
+ [TestCase("1 3 123", 50, new int[]{4})]
+ // Basic wrapping, over more lines:
+ //Breaks at: v v
+ [TestCase("1 3 123 5 1234", 50, new int[]{4, 10})]
+ // Word doesn't fit on one line, need to break mid-word
+ //Breaks at: v
+ [TestCase("12345123", 50, new int[]{5})]
+ // Word doesn't fit on *two* lines, needs two breaks mid-word
+ //Breaks at: v v
+ [TestCase("1234512345123", 50, new int[]{5, 10})]
+ // Same, but with some words at the start
+ //Breaks at: v v v
+ [TestCase("1 3 12345123451", 50, new int[]{4, 9, 14})]
+ // Can fit first two words on one line, need a break for the third word and needs splitting mid-word
+ //Breaks at: v v
+ [TestCase("1 3 12345123", 50, new int[]{4, 9})]
+ // Check for a debug assert in WordWrap. Second word needs an extra split on the last character
+ //Breaks at: v v
+ [TestCase("123 1|34W ", 50, new int[]{4, 8})]
+ public void TestSimpleWrapping(string s, int maxWidth, int[] expectedBreaks)
+ {
+ var breaks = GenerateBreaks(s, maxWidth);
+ Assert.That(breaks, Is.EqualTo(expectedBreaks));
+ }
+}
diff --git a/Robust.Client/UserInterface/WordWrap.cs b/Robust.Client/UserInterface/WordWrap.cs
index d28ca605c..0483d26f6 100644
--- a/Robust.Client/UserInterface/WordWrap.cs
+++ b/Robust.Client/UserInterface/WordWrap.cs
@@ -25,9 +25,6 @@ internal struct WordWrap
// The horizontal position of the text cursor.
public int PosX;
public Rune LastRune;
- // If a word is larger than maxSizeX, we split it.
- // We need to keep track of some data to split it into two words.
- public (int breakIndex, int wordSizePixels)? ForceSplitData = null;
public WordWrap(float maxSizeX)
{
@@ -75,7 +72,6 @@ public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out b
//wordSize = 0;
WordSizePixels = 0;
WordStartBreakIndex = (BreakIndexCounter, PosX);
- ForceSplitData = null;
// Just manually handle newlines.
if (rune == new Rune('\n'))
@@ -110,21 +106,15 @@ public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abo
// Break the "word" at the last word index
if (WordStartBreakIndex.HasValue && oldWordSizePixels != 0)
{
- breakLine = WordStartBreakIndex!.Value.index;
+ breakLine = WordStartBreakIndex.Value.index;
MaxUsedWidth = Math.Max(MaxUsedWidth, WordStartBreakIndex.Value.lineSize);
PosX = WordSizePixels;
}
- if (!ForceSplitData.HasValue)
- {
- ForceSplitData = (BreakIndexCounter, oldWordSizePixels);
- }
-
// Oh hey we get to break a word that doesn't fit on a single line.
if (WordSizePixels > _maxSizeX)
{
- var (breakIndex, splitWordSize) = ForceSplitData.Value;
- if (splitWordSize == 0)
+ if (oldWordSizePixels == 0)
{
// Happens if there's literally not enough space for a single character so uh...
// Yeah just don't.
@@ -132,10 +122,8 @@ public void NextMetrics(in CharMetrics metrics, out int? breakLine, out bool abo
return;
}
- // Reset forceSplitData so that we can split again if necessary.
- ForceSplitData = null;
- breakLine = breakIndex;
- WordSizePixels -= splitWordSize;
+ breakLine = BreakIndexCounter;
+ WordSizePixels -= oldWordSizePixels;
WordStartBreakIndex = null;
MaxUsedWidth = Math.Max(MaxUsedWidth, _maxSizeX);
PosX = WordSizePixels;
@@ -161,7 +149,6 @@ public int FinalizeText(out int? breakLine)
Logger.Error($"wordSizePixels: {WordSizePixels}");
Logger.Error($"posX: {PosX}");
Logger.Error($"lastChar: {LastRune}");
- Logger.Error($"forceSplitData: {ForceSplitData}");
// Logger.Error($"LineBreaks: {string.Join(", ", LineBreaks)}");
throw new Exception(
From 375c11aac1ebed2fa2647147ac80c129dd7b8d57 Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Wed, 5 Aug 2026 12:51:22 +1000
Subject: [PATCH 05/50] Use fastpath for RSI state drawing (#6895)
---
.../EntitySystems/SpriteSystem.Render.cs | 17 +++++++++++++++--
.../Graphics/Clyde/Clyde.RenderHandle.cs | 9 +++++++++
.../Graphics/Drawing/DrawingHandleWorld.cs | 10 ++++++++++
Robust.Client/Graphics/RSI/RSI.State.cs | 10 ++++++++--
.../ResourceTypes/RSIResource.cs | 6 +++---
5 files changed, 45 insertions(+), 7 deletions(-)
diff --git a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
index 86c536aa7..c3ca7a240 100644
--- a/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
+++ b/Robust.Client/GameObjects/EntitySystems/SpriteSystem.Render.cs
@@ -141,7 +141,17 @@ private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matr
dir = overrideDirection.Value.Convert(state.RsiDirections);
dir = dir.OffsetRsiDir(layer.DirOffset);
- var texture = state?.GetFrame(dir, layer.AnimationFrame) ?? layer.Texture ?? GetFallbackTexture();
+ AtlasTexture? atlasTexture = null;
+ Texture texture;
+ if (state != null)
+ {
+ atlasTexture = state.GetAtlasFrame(dir, layer.AnimationFrame);
+ texture = atlasTexture;
+ }
+ else
+ {
+ texture = layer.Texture ?? GetFallbackTexture();
+ }
// TODO SPRITE
// Refactor shader-param-layers to a separate layer type after layers are split into types & collections.
@@ -174,7 +184,10 @@ private void RenderLayer(Layer layer, DrawingHandleWorld drawingHandle, ref Matr
layerColor = new(new Vector4(-1) - layerColor.RGBA);
}
- drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
+ if (atlasTexture != null)
+ drawingHandle.DrawTextureRect(atlasTexture, quad, layerColor);
+ else
+ drawingHandle.DrawTextureRectRegion(texture, quad, layerColor);
if (layer.Shader != null)
drawingHandle.UseShader(null);
diff --git a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
index ac094d864..4c712d462 100644
--- a/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
+++ b/Robust.Client/Graphics/Clyde/Clyde.RenderHandle.cs
@@ -770,6 +770,15 @@ public override void DrawTextureRectRegion(Texture texture, Box2 quad,
quad.TopLeft, quad.TopRight, color, in subRegion);
}
+ public override void DrawTextureRect(AtlasTexture texture, Box2 quad, Color? modulate = null)
+ {
+ CheckDisposed();
+
+ var color = (modulate ?? Color.White) * Modulate;
+ _renderHandle.DrawTextureWorld(texture, quad.BottomLeft, quad.BottomRight,
+ quad.TopLeft, quad.TopRight, color);
+ }
+
///
/// Draws a sprite to the world. The coordinate system is right handed.
/// Make sure to set
diff --git a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs
index 5ab73d358..14fcbc182 100644
--- a/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs
+++ b/Robust.Client/Graphics/Drawing/DrawingHandleWorld.cs
@@ -155,6 +155,16 @@ public void DrawTextureRect(Texture texture, Box2 quad, Color? modulate = null)
DrawTextureRectRegion(texture, quad, modulate);
}
+ ///
+ /// Draws an atlas texture without an additional subregion.
+ ///
+ public virtual void DrawTextureRect(AtlasTexture texture, Box2 quad, Color? modulate = null)
+ {
+ CheckDisposed();
+
+ DrawTextureRectRegion(texture, quad, modulate);
+ }
+
///
/// Draws a full texture sprite to the world. The coordinate system is right handed.
/// Make sure to set
diff --git a/Robust.Client/Graphics/RSI/RSI.State.cs b/Robust.Client/Graphics/RSI/RSI.State.cs
index 0bd9d2fb7..87b3f7db3 100644
--- a/Robust.Client/Graphics/RSI/RSI.State.cs
+++ b/Robust.Client/Graphics/RSI/RSI.State.cs
@@ -25,9 +25,10 @@ public sealed class State : IRsiStateLike
public readonly float[] Delays;
// 2D array for the texture to use for each animation frame at each direction.
- public readonly Texture[][] Icons;
+ public readonly AtlasTexture[][] Icons;
- internal State(Vector2i size, RSI rsi, StateId stateId, RsiDirectionType rsiDirection, float[] delays, Texture[][] icons)
+ internal State(Vector2i size, RSI rsi, StateId stateId, RsiDirectionType rsiDirection, float[] delays,
+ AtlasTexture[][] icons)
{
DebugTools.Assert(size.X > 0);
DebugTools.Assert(size.Y > 0);
@@ -97,6 +98,11 @@ public Texture GetFrame(RsiDirection rsiDirection, int frame)
return Icons[(int) rsiDirection][frame];
}
+ internal AtlasTexture GetAtlasFrame(RsiDirection rsiDirection, int frame)
+ {
+ return Icons[(int) rsiDirection][frame];
+ }
+
public Texture[] GetFrames(RsiDirection rsiDirection)
{
return Icons[(int) rsiDirection];
diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
index 494834e54..0d503c133 100644
--- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
+++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs
@@ -173,12 +173,12 @@ private static void LoadPreTextureCommon(
var (foldedDelays, foldedIndices) = FoldDelays(stateObject.Delays);
- var textures = new Texture[foldedIndices.Length][];
+ var textures = new AtlasTexture[foldedIndices.Length][];
var callbackOffset = new Vector2i[foldedIndices.Length][];
for (var i = 0; i < textures.Length; i++)
{
- textures[i] = new Texture[foldedIndices[0].Length];
+ textures[i] = new AtlasTexture[foldedIndices[0].Length];
callbackOffset[i] = new Vector2i[foldedIndices[0].Length];
}
@@ -420,7 +420,7 @@ internal sealed class LoadStepData
internal struct StateReg
{
- public Texture[][] Output;
+ public AtlasTexture[][] Output;
public int[][] Indices;
public Vector2i[][] Offsets;
}
From 3c77ce1710fb3acfbb8784c42b5ed69bdecce789 Mon Sep 17 00:00:00 2001
From: Winkarst-cpu <74284083+Winkarst-cpu@users.noreply.github.com>
Date: Wed, 5 Aug 2026 05:52:16 +0300
Subject: [PATCH 06/50] New Feature: `before` and `after` support for
subscriptions through attributes (#6904)
---
.../EntitySystemSubscriptionGenerator.cs | 24 ++++++++++--
...ySystemSubscriptionsGeneratorAttributes.cs | 39 +++++++++++++++++--
2 files changed, 56 insertions(+), 7 deletions(-)
diff --git a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs
index d941b42a8..8eedce813 100644
--- a/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs
+++ b/Robust.Shared.EntitySystemSubscriptionsGenerator/EntitySystemSubscriptionGenerator.cs
@@ -77,7 +77,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
productionContext.CancellationToken.ThrowIfCancellationRequested();
var subscriptionMethod = method.Type.ToSubscriptionMethod();
var typeArgs = string.Join(", ", method.TypeArgs);
- subscriptionsSyntax.AppendLine($" {subscriptionMethod}<{typeArgs}>({method.MethodName});");
+
+ var before = method.Before.HasValue ? ("[" + string.Join(", ", method.Before.Value.Select(t => $"typeof({t})")) + "]") : "null";
+ var after = method.After.HasValue ? ("[" + string.Join(", ", method.After.Value.Select(t => $"typeof({t})")) + "]") : "null";
+
+ subscriptionsSyntax.AppendLine($" {subscriptionMethod}<{typeArgs}>({method.MethodName}, {before}, {after});");
}
var builder = new StringBuilder(@"
@@ -220,11 +224,23 @@ method.Parameters[2].Type is not INamedTypeSymbol eventType ||
)
{
if (annotationName.ToSubscriptionType() is not { } subType ||
- !AttributeHelper.HasAttribute(method, annotationName, out _) ||
+ !AttributeHelper.HasAttribute(method, annotationName, out var attribute) ||
parseFunc(method) is not { } parameters)
return null;
- return new SubscriptionInfo(method.Name, subType, parameters);
+ var args = attribute.ConstructorArguments;
+ return new SubscriptionInfo(method.Name, subType, parameters, GetTypes(args[0]), GetTypes(args[1]));
+ }
+
+ ///
+ /// Gets an array of type names from the typed constant.
+ ///
+ private static ImmutableArray? GetTypes(TypedConstant constant)
+ {
+ if (constant.IsNull || constant.Kind != TypedConstantKind.Array)
+ return null;
+
+ return [.. constant.Values.Select(v => (v.Value as ITypeSymbol)!.ToDisplayString())];
}
/// Aggregates all of the s across all the given providers into a single array value
@@ -244,5 +260,5 @@ params IncrementalValuesProvider[] more
private record struct EntitySystemInfo(PartialTypeInfo Type, EquatableArray Subscriptions);
- private record struct SubscriptionInfo(string MethodName, SubscriptionType Type, EquatableArray TypeArgs);
+ private record struct SubscriptionInfo(string MethodName, SubscriptionType Type, EquatableArray TypeArgs, EquatableArray? Before, EquatableArray? After);
}
diff --git a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs
index 3a5572ec9..51810f9c2 100644
--- a/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs
+++ b/Robust.Shared/Analyzers/EntitySystemSubscriptionsGeneratorAttributes.cs
@@ -24,7 +24,18 @@ namespace Robust.Shared.Analyzers;
///
[AttributeUsage(AttributeTargets.Method)]
[MeansImplicitUse]
-public sealed class SubscribeLocalEventAttribute : Attribute;
+public sealed class SubscribeLocalEventAttribute(Type[]? before = null, Type[]? after = null) : Attribute
+{
+ ///
+ /// Systems that this event subscription should run before.
+ ///
+ public readonly Type[]? Before = before;
+
+ ///
+ /// Systems that this event subscription should run after.
+ ///
+ public readonly Type[]? After = after;
+}
///
/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with
@@ -41,7 +52,18 @@ public sealed class SubscribeLocalEventAttribute : Attribute;
///
[AttributeUsage(AttributeTargets.Method)]
[MeansImplicitUse]
-public sealed class SubscribeNetworkEventAttribute : Attribute;
+public sealed class SubscribeNetworkEventAttribute(Type[]? before = null, Type[]? after = null) : Attribute
+{
+ ///
+ /// Systems that this event subscription should run before.
+ ///
+ public readonly Type[]? Before = before;
+
+ ///
+ /// Systems that this event subscription should run after.
+ ///
+ public readonly Type[]? After = after;
+}
///
/// This attribute indicates that the annotated method is a handler for an event subscription. Methods annotated with
@@ -58,4 +80,15 @@ public sealed class SubscribeNetworkEventAttribute : Attribute;
///
[AttributeUsage(AttributeTargets.Method)]
[MeansImplicitUse]
-public sealed class EventSubscriptionAttribute : Attribute;
+public sealed class EventSubscriptionAttribute(Type[]? before = null, Type[]? after = null) : Attribute
+{
+ ///
+ /// Systems that this event subscription should run before.
+ ///
+ public readonly Type[]? Before = before;
+
+ ///
+ /// Systems that this event subscription should run after.
+ ///
+ public readonly Type[]? After = after;
+}
From 34f307414800e4428612a3cec984f9baecb1ffb4 Mon Sep 17 00:00:00 2001
From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Date: Wed, 5 Aug 2026 12:56:10 +1000
Subject: [PATCH 07/50] Fix GameStateBuffer TryAdd count check (#6917)
---
Robust.Client/GameStates/GameStateProcessor.cs | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/Robust.Client/GameStates/GameStateProcessor.cs b/Robust.Client/GameStates/GameStateProcessor.cs
index bf5a97d2d..6d17fe06c 100644
--- a/Robust.Client/GameStates/GameStateProcessor.cs
+++ b/Robust.Client/GameStates/GameStateProcessor.cs
@@ -55,7 +55,11 @@ public int MaxBufferSize
{
get => _maxBufferSize;
// We place a lower bound on the maximum size to avoid spamming servers with full game state requests.
- set => _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
+ set
+ {
+ _maxBufferSize = Math.Max(value, MinimumMaxBufferSize);
+ _stateBuffer.EnsureCapacity(value);
+ }
}
///
@@ -131,7 +135,7 @@ public bool AddNewState(GameState state)
public void TryAdd(GameState state)
{
- if (_stateBuffer.Count <= MaxBufferSize)
+ if (_stateBuffer.Count < MaxBufferSize)
{
_stateBuffer.Add(state);
return;
From 1d1ffcde243c0b5ed7e1a29004e5b207278af04d Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Tue, 4 Aug 2026 20:24:28 -0700
Subject: [PATCH 08/50] Add and use CopyNoType to MappingDataNode, don't copy
in ComponentRegistrySerializer.Read (#6912)
---
Robust.Shared/Prototypes/PrototypeManager.cs | 5 ++---
.../Markdown/Mapping/MappingDataNode.cs | 19 +++++++++++++++++++
.../ComponentRegistrySerializer.cs | 9 ++-------
3 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/Robust.Shared/Prototypes/PrototypeManager.cs b/Robust.Shared/Prototypes/PrototypeManager.cs
index 1b97bc2f7..17f7f963e 100644
--- a/Robust.Shared/Prototypes/PrototypeManager.cs
+++ b/Robust.Shared/Prototypes/PrototypeManager.cs
@@ -1032,7 +1032,7 @@ private void RegisterKind(Type kind, Dictionary kinds)
{
throw new InvalidImplementationException(kind,
typeof(IPrototype),
- $"Duplicate prototype type ID: {attribute.Type}. Current: {existing}");
+ $"Duplicate prototype type ID: {name}. Current: {existing}");
}
var foundIdAttribute = false;
@@ -1200,8 +1200,7 @@ public IReadOnlyDictionary GetPrototypeData(EntityProto
continue;
}
- var copy = componentMapping.Copy();
- copy.Remove("type");
+ var copy = componentMapping.CopyNoType();
_tempMappingData[type.Value] = copy;
}
}
diff --git a/Robust.Shared/Serialization/Markdown/Mapping/MappingDataNode.cs b/Robust.Shared/Serialization/Markdown/Mapping/MappingDataNode.cs
index ff8ddbe22..3dd1cde55 100644
--- a/Robust.Shared/Serialization/Markdown/Mapping/MappingDataNode.cs
+++ b/Robust.Shared/Serialization/Markdown/Mapping/MappingDataNode.cs
@@ -266,6 +266,25 @@ public override MappingDataNode Copy()
return newMapping;
}
+ internal MappingDataNode CopyNoType()
+ {
+ var newMapping = new MappingDataNode(_children.Count)
+ {
+ Tag = Tag,
+ Start = Start,
+ End = End
+ };
+
+ foreach (var (key, val) in _list)
+ {
+ if (key != "type")
+ newMapping.Add(key, val.Copy());
+ }
+
+ newMapping._keyNodes = _keyNodes;
+ return newMapping;
+ }
+
///
/// Variant of that doesn't clone the keys or values.
///
diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
index 32e2c32a8..6c8a474ad 100644
--- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
+++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
@@ -69,10 +69,7 @@ public ComponentRegistry Read(ISerializationManager serializationManager,
referenceTypes[refIdx++] = compIdx;
- var copy = componentMapping.Copy()!;
- copy.Remove("type");
-
- var read = (IComponent)serializationManager.Read(registration.Type, copy, hookCtx, context)!;
+ var read = (IComponent)serializationManager.Read(registration.Type, componentMapping, hookCtx, context)!;
// The full YAML mapping is already retained by PrototypeManager.
components[compType] = new ComponentRegistryEntry(read);
@@ -132,9 +129,7 @@ public ValidationNode Validate(ISerializationManager serializationManager,
referenceTypes[refIdx++] = compIdx;
- var copy = componentMapping.Copy();
- copy.Remove("type");
-
+ var copy = componentMapping.CopyNoType();
list.Add(serializationManager.ValidateNode(registration.Type, copy, context));
}
From 9cb27cea76cf8115a15251d79d8837afd8504873 Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Wed, 5 Aug 2026 00:39:50 -0700
Subject: [PATCH 09/50] Replace some usages of object reads with generic type
reads (#6914)
---
Robust.Shared/Audio/SoundSpecifierTypeSerializer.cs | 12 +++++++++++-
.../EntitySerialization/MapChunkSerializer.cs | 6 ++----
.../Custom/AbstractDictionarySerializer.cs | 4 +++-
3 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/Robust.Shared/Audio/SoundSpecifierTypeSerializer.cs b/Robust.Shared/Audio/SoundSpecifierTypeSerializer.cs
index 8d1a31857..41df81d21 100644
--- a/Robust.Shared/Audio/SoundSpecifierTypeSerializer.cs
+++ b/Robust.Shared/Audio/SoundSpecifierTypeSerializer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
@@ -27,6 +28,7 @@ private Type GetType(MappingDataNode node)
if (hasCollection)
return typeof(SoundCollectionSpecifier);
+ // See Read below if you are adding new types
return typeof(SoundPathSpecifier);
}
@@ -35,7 +37,15 @@ public SoundSpecifier Read(ISerializationManager serializationManager, MappingDa
ISerializationManager.InstantiationDelegate? instanceProvider = null)
{
var type = GetType(node);
- return (SoundSpecifier)serializationManager.Read(type, node, hookCtx, context)!;
+
+ if (type == typeof(SoundPathSpecifier))
+ return serializationManager.Read(node, hookCtx, context, notNullableOverride: true);
+
+ if (type == typeof(SoundCollectionSpecifier))
+ return serializationManager.Read(node, hookCtx, context, notNullableOverride: true);
+
+ // See GetType above if you are adding new types
+ throw new NotImplementedException();
}
public SoundSpecifier Read(ISerializationManager serializationManager, ValueDataNode node,
diff --git a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs
index c5df77dfa..79b741ceb 100644
--- a/Robust.Shared/EntitySerialization/MapChunkSerializer.cs
+++ b/Robust.Shared/EntitySerialization/MapChunkSerializer.cs
@@ -35,7 +35,7 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate? instantiationDelegate = null)
{
- var ind = (Vector2i) serializationManager.Read(typeof(Vector2i), node["ind"], hookCtx, context)!;
+ var ind = serializationManager.Read(node["ind"], hookCtx, context)!;
var tileNode = (ValueDataNode)node["tiles"];
var tileBytes = Convert.FromBase64String(tileNode.Value);
@@ -49,9 +49,7 @@ public MapChunk Read(ISerializationManager serializationManager, MappingDataNode
// TODO: This should be on the context I think?
if (node.TryGet("size", out ValueDataNode? sizeNode))
- {
- size = (ushort) serializationManager.Read(typeof(ushort), sizeNode, context)!;
- }
+ size = serializationManager.Read(sizeNode, context)!;
var chunk = instantiationDelegate != null ? instantiationDelegate() : new MapChunk(ind.X, ind.Y, size);
diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/AbstractDictionarySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/AbstractDictionarySerializer.cs
index 2a1142166..2ee7e66f0 100644
--- a/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/AbstractDictionarySerializer.cs
+++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/Custom/AbstractDictionarySerializer.cs
@@ -42,7 +42,9 @@ public Dictionary Read(ISerializationManager serializationManager,
foreach (var (key, valueNode) in node.Children)
{
var type = serializationManager.ReflectionManager.YamlTypeTagLookup(typeof(TValue), key)!;
- var value = (TValue) serializationManager.Read(type, valueNode, hookCtx, context, notNullableOverride:true)!;
+ var copy = valueNode.Copy();
+ copy.Tag = $"!type:{key}";
+ var value = serializationManager.Read(copy, hookCtx, context, notNullableOverride: true);
dict.Add(type, value);
}
From d4c6e71058003034e99f43fcabe826d1c8b564f7 Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Wed, 5 Aug 2026 01:04:27 -0700
Subject: [PATCH 10/50] Make ComponentRegistrySerializer.Read 3.7 times faster
(#6913)
---
Robust.Serialization.Generator/Generator.cs | 105 +++++++++++++-----
.../Serialization/ISerializationGenerated.cs | 16 ++-
.../ComponentRegistrySerializer.cs | 30 +++--
3 files changed, 112 insertions(+), 39 deletions(-)
diff --git a/Robust.Serialization.Generator/Generator.cs b/Robust.Serialization.Generator/Generator.cs
index c598bbd2b..77d72f873 100644
--- a/Robust.Serialization.Generator/Generator.cs
+++ b/Robust.Serialization.Generator/Generator.cs
@@ -5,6 +5,7 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
+using Robust.Roslyn.Shared;
using static Robust.Roslyn.Shared.DataDefinitionHelper;
using static Robust.Serialization.Generator.CustomSerializerType;
using static Robust.Serialization.Generator.Types;
@@ -36,6 +37,7 @@ public class Generator : IIncrementalGenerator
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";
+ private const string ComponentName = "Robust.Shared.GameObjects.Component";
public void Initialize(IncrementalGeneratorInitializationContext initContext)
{
@@ -193,7 +195,7 @@ private static (string, string)? GenerateForDataDefinition(
{{GetCopiers(definition)}}
- {{GetReader(definition)}}
+ {{GetReaders(definition)}}
{{GetWriter(definition)}}
@@ -612,6 +614,41 @@ private static string GetReadBody(DataDefinition definition, string targetPrefix
return builder.ToString();
}
+ private static string GetReadCompMethod(DataDefinition definition)
+ {
+ var inheritsComp = TypeSymbolHelper.Inherits(definition.Type, ComponentName);
+ if (!inheritsComp)
+ {
+ if (!TypeSymbolHelper.ShittyTypeMatch(definition.Type, ComponentName)) return string.Empty;
+
+ return """
+ public virtual void ReadComp(
+ ref Component target,
+ MappingDataNode mappingDataNode,
+ ISerializationManager serialization,
+ SerializationHookContext hookCtx,
+ ISerializationContext? context)
+ {
+ Component.Read(ref target, mappingDataNode, serialization, hookCtx, context);
+ }
+ """;
+ }
+
+ return $$"""
+ public override void ReadComp(
+ ref Component target,
+ MappingDataNode mappingDataNode,
+ ISerializationManager serialization,
+ SerializationHookContext hookCtx,
+ ISerializationContext? context)
+ {
+ var cast = ({{definition.GenericTypeName}}) target;
+ {{definition.GenericTypeName}}.Read(ref cast, mappingDataNode, serialization, hookCtx, context);
+ target = (Component) cast;
+ }
+ """;
+ }
+
private static string GetInstantiators(DataDefinition definition)
{
var builder = new StringBuilder();
@@ -773,32 +810,8 @@ private static void GetCopierMethod(
if (!definition.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 ";
- }
+ var modifier = GetModifier(definition, type, targetType, forceOverride, out var sameType);
builder.AppendLine($"""
public {modifier}void Copy(
@@ -876,7 +889,43 @@ private static void GetCopierMethod(
}
}
- private static string GetReader(DataDefinition definition)
+ private static object GetModifier(
+ DataDefinition definition,
+ ITypeSymbol type,
+ string targetType,
+ bool forceOverride,
+ out bool sameType)
+ {
+ sameType = definition.Type.Equals(type, SymbolEqualityComparer.Default) &&
+ targetType == definition.GenericTypeName &&
+ targetType != "object";
+ var isSealedOrStruct = definition.Type.IsSealed || definition.Type.IsValueType;
+ 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 ";
+ }
+
+ return modifier;
+ }
+
+ private static string GetReaders(DataDefinition definition)
{
string body;
if (definition.Type.IsAbstract)
@@ -947,6 +996,8 @@ public static void Read(
{
{{body}}
}
+
+ {{GetReadCompMethod(definition)}}
""";
}
diff --git a/Robust.Shared/Serialization/ISerializationGenerated.cs b/Robust.Shared/Serialization/ISerializationGenerated.cs
index ebc7dbe43..7cb0fd3d2 100644
--- a/Robust.Shared/Serialization/ISerializationGenerated.cs
+++ b/Robust.Shared/Serialization/ISerializationGenerated.cs
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using Robust.Shared.GameObjects;
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;
@@ -11,6 +11,7 @@
namespace Robust.Shared.Serialization;
+[NotContentImplementable]
public interface ISerializationGenerated : ISerializationGenerated
{
///
@@ -51,6 +52,18 @@ static virtual void Read(
throw new NotImplementedException();
}
+ ///
+ [Obsolete("Use ISerializationManager.Read instead")]
+ void ReadComp(
+ ref Component target,
+ MappingDataNode mappingDataNode,
+ ISerializationManager serialization,
+ SerializationHookContext hookCtx,
+ ISerializationContext? context)
+ {
+ throw new NotImplementedException();
+ }
+
///
[Obsolete("Use ISerializationManager.Write instead")]
static virtual void Write(
@@ -92,6 +105,7 @@ static virtual void GetFieldDefinitions(T? instance, List f
}
}
+[NotContentImplementable]
public interface ISerializationGenerated
{
///
diff --git a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
index 6c8a474ad..0c85376fa 100644
--- a/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
+++ b/Robust.Shared/Serialization/TypeSerializers/Implementations/ComponentRegistrySerializer.cs
@@ -18,10 +18,14 @@
namespace Robust.Shared.Serialization.TypeSerializers.Implementations
{
[TypeSerializer]
- public sealed partial class ComponentRegistrySerializer : BaseTypeSerializer, ITypeSerializer, ITypeInheritanceHandler, ITypeCopier
+ public sealed partial class ComponentRegistrySerializer : BaseTypeSerializer, ITypeSerializer, ITypeInheritanceHandler, ITypeCopier,
+ IPostInjectInit
{
+ [Dependency] private IDynamicTypeFactory _dynamicTypeFactory = default!;
[Dependency] private IComponentFactory _factory = default!;
+ private IDynamicTypeFactoryInternal _dynamicTypeFactoryInternal = default!;
+
public ComponentRegistry Read(ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
@@ -36,7 +40,7 @@ public ComponentRegistry Read(ISerializationManager serializationManager,
foreach (var sequenceEntry in node.Sequence)
{
var componentMapping = (MappingDataNode)sequenceEntry;
- string compType = ((ValueDataNode) componentMapping.Get("type")).Value;
+ var compType = ((ValueDataNode) componentMapping.Get("type")).Value;
// See if type exists to detect errors.
switch (_factory.GetComponentAvailability(compType))
{
@@ -51,16 +55,10 @@ public ComponentRegistry Read(ISerializationManager serializationManager,
continue;
}
- // Has this type already been added?
- if (components.ContainsKey(compType))
- {
- Log.Error($"Component of type '{compType}' defined twice in prototype!");
- continue;
- }
-
var registration = _factory.GetRegistration(compType);
var compIdx = registration.Idx;
+ // Has this type already been added?
if (referenceTypes[..refIdx].Contains(compIdx))
{
throw new InvalidOperationException(
@@ -69,10 +67,15 @@ public ComponentRegistry Read(ISerializationManager serializationManager,
referenceTypes[refIdx++] = compIdx;
- var read = (IComponent)serializationManager.Read(registration.Type, componentMapping, hookCtx, context)!;
+ var comp = (Component) _dynamicTypeFactoryInternal.CreateInstanceUnchecked(registration.Type, inject: false);
+#pragma warning disable CS0618 // Type or member is obsolete
+ comp = comp.Instantiate();
+#pragma warning restore CS0618 // Type or member is obsolete
+ comp.ReadComp(ref comp, componentMapping, serializationManager, hookCtx, context);
+ SerializationManager.TryRunAfterHook(comp, hookCtx);
// The full YAML mapping is already retained by PrototypeManager.
- components[compType] = new ComponentRegistryEntry(read);
+ components[compType] = new ComponentRegistryEntry(comp);
}
return components;
@@ -223,5 +226,10 @@ private Dictionary ToTypeIndexedDictionary(SequenceD
return dict;
}
+
+ void IPostInjectInit.PostInject()
+ {
+ _dynamicTypeFactoryInternal = (IDynamicTypeFactoryInternal) _dynamicTypeFactory;
+ }
}
}
From 5fdaa2b0040a1948523b2e216315052fa0dfb7cc Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Wed, 5 Aug 2026 01:19:01 -0700
Subject: [PATCH 11/50] Make IHasDependencies injection 6x faster and
BuildGraph faster 1.6x faster (#6916)
---
.../HasDependenciesGeneratorTest.cs | 114 +++---------------
.../Generators/HasDependenciesGenerator.cs | 83 ++-----------
.../GameObjects/EntitySystemManager.cs | 7 +-
Robust.Shared/IoC/DependencyCollection.cs | 113 +++++++++++++----
Robust.Shared/IoC/DependencyType.cs | 36 ++++++
Robust.Shared/IoC/IDependencyCollection.cs | 3 +
Robust.Shared/IoC/IHasDependencies.cs | 10 +-
7 files changed, 158 insertions(+), 208 deletions(-)
create mode 100644 Robust.Shared/IoC/DependencyType.cs
diff --git a/Robust.Analyzers.Tests/HasDependenciesGeneratorTest.cs b/Robust.Analyzers.Tests/HasDependenciesGeneratorTest.cs
index f10b06308..2c68e19ce 100644
--- a/Robust.Analyzers.Tests/HasDependenciesGeneratorTest.cs
+++ b/Robust.Analyzers.Tests/HasDependenciesGeneratorTest.cs
@@ -37,18 +37,9 @@ public sealed partial class Foobar
public partial class Foobar : global::Robust.Shared.IoC.IHasDependencies
{
[global::Robust.Shared.Analyzers.RobustAutoGenerated]
- global::System.Type[] global::Robust.Shared.IoC.IHasDependencies.GetDependencyTypes()
+ void global::Robust.Shared.IoC.IHasDependencies.Inject(global::Robust.Shared.IoC.IDependencyCollection dependencies)
{
- return new global::System.Type[]
- {
- typeof(global::string)
- };
- }
-
- [global::Robust.Shared.Analyzers.RobustAutoGenerated]
- void global::Robust.Shared.IoC.IHasDependencies.Inject(global::System.ReadOnlySpan