diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1e6b2698c..7d6451d1c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -51,11 +51,14 @@
+
+
+
diff --git a/Robust.Analyzers.Tests/Aliases.cs b/Robust.Analyzers.Tests/Aliases.cs
new file mode 100644
index 000000000..238d418ec
--- /dev/null
+++ b/Robust.Analyzers.Tests/Aliases.cs
@@ -0,0 +1,8 @@
+// OH BOY. TURNS OUT IT GETS EVEN MORE CURSED.
+//
+// So because we're compiling a copy of Robust.Roslyn.Shared into every analyzer project,
+// the test project sees multiple copies of it. This would make it impossible to use.
+// UNLESS you use this obscure C# feature called "extern alias"
+// that I guarantee you you've never heard of before, and are now concerned about.
+
+extern alias SerializationGenerator;
diff --git a/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs b/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs
new file mode 100644
index 000000000..79a5c70c7
--- /dev/null
+++ b/Robust.Analyzers.Tests/ComponentPauseGeneratorTest.cs
@@ -0,0 +1,340 @@
+extern alias SerializationGenerator;
+using System.Linq;
+using System.Reflection;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+using NUnit.Framework;
+using SerializationGenerator::Robust.Roslyn.Shared;
+using SerializationGenerator::Robust.Serialization.Generator;
+
+namespace Robust.Analyzers.Tests;
+
+[TestFixture]
+[TestOf(typeof(ComponentPauseGenerator))]
+[Parallelizable(ParallelScope.All)]
+public sealed class ComponentPauseGeneratorTest
+{
+ private const string TypesCode = """
+ global using System;
+ global using Robust.Shared.Analyzers;
+ global using Robust.Shared.GameObjects;
+
+ namespace Robust.Shared.Analyzers
+ {
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class AutoGenerateComponentPauseAttribute : Attribute
+ {
+ public bool Dirty = false;
+ }
+
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ public sealed class AutoPausedFieldAttribute : Attribute;
+
+ [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+ public sealed class AutoNetworkedFieldAttribute : Attribute
+ {
+ }
+ }
+
+ namespace Robust.Shared.GameObjects
+ {
+ public interface IComponent;
+ }
+ """;
+
+ [Test]
+ public void TestBasic()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField]
+ public TimeSpan Foo;
+ }
+ """);
+
+ ExpectNoDiagnostics(result);
+ ExpectSource(
+ result,
+ """
+ //
+
+ using Robust.Shared.GameObjects;
+
+ public partial class FooComponent
+ {
+ [RobustAutoGenerated]
+ public sealed class FooComponent_AutoPauseSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnEntityUnpaused);
+ }
+
+ private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
+ {
+ component.Foo += args.PausedTime;
+ }
+ }
+ }
+
+ """);
+ }
+
+ [Test]
+ public void TestNullable()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField]
+ public TimeSpan? Foo;
+ }
+ """);
+
+ ExpectNoDiagnostics(result);
+ ExpectSource(
+ result,
+ """
+ //
+
+ using Robust.Shared.GameObjects;
+
+ public partial class FooComponent
+ {
+ [RobustAutoGenerated]
+ public sealed class FooComponent_AutoPauseSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnEntityUnpaused);
+ }
+
+ private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
+ {
+ if (component.Foo.HasValue)
+ component.Foo = component.Foo.Value + args.PausedTime;
+ }
+ }
+ }
+
+ """);
+ }
+
+ [Test]
+ public void TestAutoState()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField, AutoNetworkedField]
+ public TimeSpan Foo;
+ }
+ """);
+
+ ExpectNoDiagnostics(result);
+ ExpectSource(
+ result,
+ """
+ //
+
+ using Robust.Shared.GameObjects;
+
+ public partial class FooComponent
+ {
+ [RobustAutoGenerated]
+ public sealed class FooComponent_AutoPauseSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnEntityUnpaused);
+ }
+
+ private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
+ {
+ component.Foo += args.PausedTime;
+ Dirty(uid, component);
+ }
+ }
+ }
+
+ """);
+ }
+
+ [Test]
+ public void TestExplicitDirty()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause(Dirty = true)]
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField]
+ public TimeSpan Foo;
+ }
+ """);
+
+ ExpectNoDiagnostics(result);
+ ExpectSource(
+ result,
+ """
+ //
+
+ using Robust.Shared.GameObjects;
+
+ public partial class FooComponent
+ {
+ [RobustAutoGenerated]
+ public sealed class FooComponent_AutoPauseSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnEntityUnpaused);
+ }
+
+ private void OnEntityUnpaused(EntityUid uid, FooComponent component, ref EntityUnpausedEvent args)
+ {
+ component.Foo += args.PausedTime;
+ Dirty(uid, component);
+ }
+ }
+ }
+
+ """);
+ }
+
+ [Test]
+ public void TestDiagnosticNotIComponent()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent
+ {
+ [AutoPausedField]
+ public TimeSpan Foo;
+ }
+ """);
+
+ ExpectNoSource(result);
+ ExpectDiagnostics(result, [
+ (Diagnostics.IdComponentPauseNotComponent, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
+ ]);
+ }
+
+ [Test]
+ public void TestDiagnosticNoFields()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent : IComponent
+ {
+ public TimeSpan Foo;
+ }
+ """);
+
+ ExpectNoSource(result);
+ ExpectDiagnostics(result, [
+ (Diagnostics.IdComponentPauseNoFields, new LinePositionSpan(new LinePosition(1, 28), new LinePosition(1, 40)))
+ ]);
+ }
+
+ [Test]
+ public void TestDiagnosticNoParentAttribute()
+ {
+ var result = RunGenerator("""
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField]
+ public TimeSpan Foo, Fooz;
+
+ [AutoPausedField]
+ public TimeSpan Bar { get; set; }
+ }
+ """);
+
+ ExpectNoSource(result);
+ ExpectDiagnostics(result, [
+ (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 20), new LinePosition(3, 23))),
+ (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(3, 25), new LinePosition(3, 29))),
+ (Diagnostics.IdComponentPauseNoParentAttribute, new LinePositionSpan(new LinePosition(6, 20), new LinePosition(6, 23)))
+ ]);
+ }
+
+ [Test]
+ public void TestDiagnosticWrongType()
+ {
+ var result = RunGenerator("""
+ [AutoGenerateComponentPause]
+ public sealed partial class FooComponent : IComponent
+ {
+ [AutoPausedField]
+ public int Foo, Fooz;
+
+ [AutoPausedField]
+ public int Bar { get; set; }
+ }
+ """);
+
+ ExpectNoSource(result);
+ ExpectDiagnostics(result, [
+ (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 15), new LinePosition(4, 18))),
+ (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(4, 20), new LinePosition(4, 24))),
+ (Diagnostics.IdComponentPauseWrongTypeAttribute, new LinePositionSpan(new LinePosition(7, 15), new LinePosition(7, 18)))
+ ]);
+ }
+
+ private static void ExpectSource(GeneratorRunResult result, string expected)
+ {
+ Assert.That(result.GeneratedSources, Has.Length.EqualTo(1));
+
+ var source = result.GeneratedSources[0];
+
+ Assert.That(source.SourceText.ToString(), Is.EqualTo(expected));
+ }
+
+ private static void ExpectNoSource(GeneratorRunResult result)
+ {
+ Assert.That(result.GeneratedSources, Is.Empty);
+ }
+
+ private static void ExpectNoDiagnostics(GeneratorRunResult result)
+ {
+ Assert.That(result.Diagnostics, Is.Empty);
+ }
+
+ private static void ExpectDiagnostics(GeneratorRunResult result, (string code, LinePositionSpan span)[] diagnostics)
+ {
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.Diagnostics, Has.Length.EqualTo(diagnostics.Length));
+ foreach (var (code, span) in diagnostics)
+ {
+ Assert.That(
+ result.Diagnostics.Any(x => x.Id == code && x.Location.GetLineSpan().Span == span),
+ $"Expected diagnostic with code {code} and location {span}");
+ }
+ });
+ }
+
+ private static GeneratorRunResult RunGenerator(string source)
+ {
+ var compilation = (Compilation)CSharpCompilation.Create("compilation",
+ new[]
+ {
+ CSharpSyntaxTree.ParseText(source, path: "Source.cs"),
+ CSharpSyntaxTree.ParseText(TypesCode, path: "Types.cs")
+ },
+ new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+
+ var generator = new ComponentPauseGenerator();
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out _);
+ var result = driver.GetRunResult();
+
+ return result.Results[0];
+ }
+}
diff --git a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
index 431fe38fa..954204dcb 100644
--- a/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
+++ b/Robust.Analyzers.Tests/Robust.Analyzers.Tests.csproj
@@ -1,4 +1,8 @@
+
+ true
+
+
@@ -23,5 +27,6 @@
+
diff --git a/Robust.Analyzers/AccessAnalyzer.cs b/Robust.Analyzers/AccessAnalyzer.cs
index adb5880af..3068d806d 100644
--- a/Robust.Analyzers/AccessAnalyzer.cs
+++ b/Robust.Analyzers/AccessAnalyzer.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
using Robust.Shared.Analyzers.Implementation;
namespace Robust.Analyzers
diff --git a/Robust.Analyzers/ByRefEventAnalyzer.cs b/Robust.Analyzers/ByRefEventAnalyzer.cs
index 0609c84e1..62236bc12 100644
--- a/Robust.Analyzers/ByRefEventAnalyzer.cs
+++ b/Robust.Analyzers/ByRefEventAnalyzer.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
using static Microsoft.CodeAnalysis.SymbolEqualityComparer;
namespace Robust.Analyzers;
@@ -16,7 +17,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByRefEventSubscribedByValueRule = new(
Diagnostics.IdByRefEventSubscribedByValue,
"By-ref event subscribed to by value",
- "Tried to subscribe to a by-ref event '{0}' by value.",
+ "Tried to subscribe to a by-ref event '{0}' by value",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -26,7 +27,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByRefEventRaisedByValueRule = new(
Diagnostics.IdByRefEventRaisedByValue,
"By-ref event raised by value",
- "Tried to raise a by-ref event '{0}' by value.",
+ "Tried to raise a by-ref event '{0}' by value",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -36,7 +37,7 @@ public sealed class ByRefEventAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor ByValueEventRaisedByRefRule = new(
Diagnostics.IdValueEventRaisedByRef,
"Value event raised by-ref",
- "Tried to raise a value event '{0}' by-ref.",
+ "Tried to raise a value event '{0}' by-ref",
"Usage",
DiagnosticSeverity.Error,
true,
diff --git a/Robust.Analyzers/DataDefinitionAnalyzer.cs b/Robust.Analyzers/DataDefinitionAnalyzer.cs
index 5953ee2c7..14910c10a 100644
--- a/Robust.Analyzers/DataDefinitionAnalyzer.cs
+++ b/Robust.Analyzers/DataDefinitionAnalyzer.cs
@@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
@@ -18,7 +19,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataDefinitionPartialRule = new(
Diagnostics.IdDataDefinitionPartial,
"Type must be partial",
- "Type {0} is a DataDefinition but is not partial.",
+ "Type {0} is a DataDefinition but is not partial",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -28,7 +29,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor NestedDataDefinitionPartialRule = new(
Diagnostics.IdNestedDataDefinitionPartial,
"Type must be partial",
- "Type {0} contains nested data definition {1} but is not partial.",
+ "Type {0} contains nested data definition {1} but is not partial",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -38,7 +39,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataFieldWritableRule = new(
Diagnostics.IdDataFieldWritable,
"Data field must not be readonly",
- "Data field {0} in data definition {1} is readonly.",
+ "Data field {0} in data definition {1} is readonly",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -48,7 +49,7 @@ public sealed class DataDefinitionAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor DataFieldPropertyWritableRule = new(
Diagnostics.IdDataFieldPropertyWritable,
"Data field property must have a setter",
- "Data field property {0} in data definition {1} does not have a setter.",
+ "Data field property {0} in data definition {1} does not have a setter",
"Usage",
DiagnosticSeverity.Error,
true,
diff --git a/Robust.Analyzers/DataDefinitionFixer.cs b/Robust.Analyzers/DataDefinitionFixer.cs
index f30ca830d..80ba22a96 100644
--- a/Robust.Analyzers/DataDefinitionFixer.cs
+++ b/Robust.Analyzers/DataDefinitionFixer.cs
@@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxKind;
-using static Robust.Analyzers.Diagnostics;
+using static Robust.Roslyn.Shared.Diagnostics;
namespace Robust.Analyzers;
diff --git a/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs b/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs
index 020d5bd56..a689e0711 100644
--- a/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs
+++ b/Robust.Analyzers/ExplicitInterfaceAnalyzer.cs
@@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Robust.Roslyn.Shared;
using Document = Microsoft.CodeAnalysis.Document;
namespace Robust.Analyzers
diff --git a/Robust.Analyzers/ExplicitVirtualAnalyzer.cs b/Robust.Analyzers/ExplicitVirtualAnalyzer.cs
index 74a34e00c..1b4a2a986 100644
--- a/Robust.Analyzers/ExplicitVirtualAnalyzer.cs
+++ b/Robust.Analyzers/ExplicitVirtualAnalyzer.cs
@@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
diff --git a/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs
index c4a2039f8..cadee94d4 100644
--- a/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs
+++ b/Robust.Analyzers/MeansImplicitAssigmentSuppressor.cs
@@ -2,6 +2,7 @@ using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers
{
diff --git a/Robust.Analyzers/NotNullableFlagAnalyzer.cs b/Robust.Analyzers/NotNullableFlagAnalyzer.cs
index 648767140..f50e8d15c 100644
--- a/Robust.Analyzers/NotNullableFlagAnalyzer.cs
+++ b/Robust.Analyzers/NotNullableFlagAnalyzer.cs
@@ -3,6 +3,7 @@ using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
@@ -31,7 +32,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor InvalidNotNullableImplementationRule = new (
Diagnostics.IdInvalidNotNullableFlagImplementation,
- "Invalid NotNullable flag implementation.",
+ "Invalid NotNullable flag implementation",
"NotNullable flag is either not typed as bool, or does not have a default value equaling false",
"Usage",
DiagnosticSeverity.Error,
@@ -41,7 +42,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor InvalidNotNullableTypeRule = new (
Diagnostics.IdInvalidNotNullableFlagType,
"Failed to resolve type parameter",
- "Failed to resolve type parameter \"{0}\".",
+ "Failed to resolve type parameter \"{0}\"",
"Usage",
DiagnosticSeverity.Error,
true,
@@ -49,7 +50,7 @@ public sealed class NotNullableFlagAnalyzer : DiagnosticAnalyzer
private static readonly DiagnosticDescriptor NotNullableFlagValueTypeRule = new (
Diagnostics.IdNotNullableFlagValueType,
- "NotNullable flag not supported for value types.",
+ "NotNullable flag not supported for value types",
"Value types as generic arguments are not supported for NotNullable flags",
"Usage",
DiagnosticSeverity.Error,
diff --git a/Robust.Analyzers/PreferGenericVariantAnalyzer.cs b/Robust.Analyzers/PreferGenericVariantAnalyzer.cs
index 9edf020e4..57e0c5ef1 100644
--- a/Robust.Analyzers/PreferGenericVariantAnalyzer.cs
+++ b/Robust.Analyzers/PreferGenericVariantAnalyzer.cs
@@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
diff --git a/Robust.Analyzers/Robust.Analyzers.csproj b/Robust.Analyzers/Robust.Analyzers.csproj
index bcba84910..c115cd001 100644
--- a/Robust.Analyzers/Robust.Analyzers.csproj
+++ b/Robust.Analyzers/Robust.Analyzers.csproj
@@ -1,17 +1,5 @@
-
- netstandard2.0
- 10
- true
-
-
-
-
-
-
-
-
@@ -28,4 +16,10 @@
+
+
+
+ disable
+
+
diff --git a/Robust.Analyzers/SerializableAnalyzer.cs b/Robust.Analyzers/SerializableAnalyzer.cs
index 96ae34404..c9423879e 100644
--- a/Robust.Analyzers/SerializableAnalyzer.cs
+++ b/Robust.Analyzers/SerializableAnalyzer.cs
@@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers
{
diff --git a/Robust.Analyzers/TaskResultAnalyzer.cs b/Robust.Analyzers/TaskResultAnalyzer.cs
index 9eeaddf9e..96f16375b 100644
--- a/Robust.Analyzers/TaskResultAnalyzer.cs
+++ b/Robust.Analyzers/TaskResultAnalyzer.cs
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
+using Robust.Roslyn.Shared;
namespace Robust.Analyzers;
diff --git a/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj
index 51ff69c86..f7fa741d8 100644
--- a/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj
+++ b/Robust.Client.NameGenerator/Robust.Client.NameGenerator.csproj
@@ -1,19 +1,17 @@
-
- netstandard2.0
- true
-
-
-
-
-
-
-
+
+
+
+
+
+ disable
+
+
diff --git a/Robust.Roslyn.Shared/AttributeHelper.cs b/Robust.Roslyn.Shared/AttributeHelper.cs
new file mode 100644
index 000000000..7a8174b1f
--- /dev/null
+++ b/Robust.Roslyn.Shared/AttributeHelper.cs
@@ -0,0 +1,46 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+
+namespace Robust.Roslyn.Shared;
+
+#nullable enable
+
+public static class AttributeHelper
+{
+ public static bool HasAttribute(ISymbol symbol, string attributeMetadataName, [NotNullWhen(true)] out AttributeData? matchedAttribute)
+ {
+ foreach (var attribute in symbol.GetAttributes())
+ {
+ if (attribute.AttributeClass == null)
+ continue;
+
+ if (TypeSymbolHelper.ShittyTypeMatch(attribute.AttributeClass, attributeMetadataName))
+ {
+ matchedAttribute = attribute;
+ return true;
+ }
+ }
+
+ matchedAttribute = null;
+ return false;
+ }
+
+ public static bool GetNamedArgumentBool(AttributeData data, string name, bool defaultValue)
+ {
+ foreach (var kv in data.NamedArguments)
+ {
+ if (kv.Key != name)
+ continue;
+
+ if (kv.Value.Kind != TypedConstantKind.Primitive)
+ continue;
+
+ if (kv.Value.Value is not bool value)
+ continue;
+
+ return value;
+ }
+
+ return defaultValue;
+ }
+}
diff --git a/Robust.Analyzers/Diagnostics.cs b/Robust.Roslyn.Shared/Diagnostics.cs
similarity index 82%
rename from Robust.Analyzers/Diagnostics.cs
rename to Robust.Roslyn.Shared/Diagnostics.cs
index 38873dbc1..b829a2f4c 100644
--- a/Robust.Analyzers/Diagnostics.cs
+++ b/Robust.Roslyn.Shared/Diagnostics.cs
@@ -1,6 +1,6 @@
using Microsoft.CodeAnalysis;
-namespace Robust.Analyzers;
+namespace Robust.Roslyn.Shared;
public static class Diagnostics
{
@@ -24,6 +24,10 @@ public static class Diagnostics
public const string IdNestedDataDefinitionPartial = "RA0018";
public const string IdDataFieldWritable = "RA0019";
public const string IdDataFieldPropertyWritable = "RA0020";
+ public const string IdComponentPauseNotComponent = "RA0021";
+ public const string IdComponentPauseNoFields = "RA0022";
+ public const string IdComponentPauseNoParentAttribute = "RA0023";
+ public const string IdComponentPauseWrongTypeAttribute = "RA0024";
public static SuppressionDescriptor MeansImplicitAssignment =>
new SuppressionDescriptor("RADC1000", "CS0649", "Marked as implicitly assigned.");
diff --git a/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs b/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs
new file mode 100644
index 000000000..f141f2744
--- /dev/null
+++ b/Robust.Roslyn.Shared/Helpers/EquatableArray{T}.cs
@@ -0,0 +1,201 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// Taken from https://github.com/CommunityToolkit/dotnet/blob/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/EquatableArray%7BT%7D.cs
+
+using System.Collections;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Robust.Roslyn.Shared.Helpers;
+
+#nullable enable
+
+///
+/// Extensions for .
+///
+public static class EquatableArray
+{
+ ///
+ /// Creates an instance from a given .
+ ///
+ /// The type of items in the input array.
+ /// The input instance.
+ /// An instance from a given .
+ public static EquatableArray AsEquatableArray(this ImmutableArray array)
+ where T : IEquatable
+ {
+ return new(array);
+ }
+}
+
+///
+/// An immutable, equatable array. This is equivalent to but with value equality support.
+///
+/// The type of values in the array.
+public readonly struct EquatableArray : IEquatable>, IEnumerable
+ where T : IEquatable
+{
+ ///
+ /// The underlying array.
+ ///
+ private readonly T[]? array;
+
+ ///
+ /// Creates a new instance.
+ ///
+ /// The input to wrap.
+ public EquatableArray(ImmutableArray array)
+ {
+ this.array = Unsafe.As, T[]?>(ref array);
+ }
+
+ ///
+ /// Gets a reference to an item at a specified position within the array.
+ ///
+ /// The index of the item to retrieve a reference to.
+ /// A reference to an item at a specified position within the array.
+ public ref readonly T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref AsImmutableArray().ItemRef(index);
+ }
+
+ ///
+ /// Gets a value indicating whether the current array is empty.
+ ///
+ public bool IsEmpty
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => AsImmutableArray().IsEmpty;
+ }
+
+ ///
+ public bool Equals(EquatableArray array)
+ {
+ return AsSpan().SequenceEqual(array.AsSpan());
+ }
+
+ ///
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ {
+ return obj is EquatableArray array && Equals(this, array);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ if (this.array is not T[] array)
+ {
+ return 0;
+ }
+
+ HashCode hashCode = default;
+
+ foreach (T item in array)
+ {
+ hashCode.Add(item);
+ }
+
+ return hashCode.ToHashCode();
+ }
+
+ ///
+ /// Gets an instance from the current .
+ ///
+ /// The from the current .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ImmutableArray AsImmutableArray()
+ {
+ return Unsafe.As>(ref Unsafe.AsRef(in this.array));
+ }
+
+ ///
+ /// Creates an instance from a given .
+ ///
+ /// The input instance.
+ /// An instance from a given .
+ public static EquatableArray FromImmutableArray(ImmutableArray array)
+ {
+ return new(array);
+ }
+
+ ///
+ /// Returns a wrapping the current items.
+ ///
+ /// A wrapping the current items.
+ public ReadOnlySpan AsSpan()
+ {
+ return AsImmutableArray().AsSpan();
+ }
+
+ ///
+ /// Copies the contents of this instance to a mutable array.
+ ///
+ /// The newly instantiated array.
+ public T[] ToArray()
+ {
+ return AsImmutableArray().ToArray();
+ }
+
+ ///
+ /// Gets an value to traverse items in the current array.
+ ///
+ /// An value to traverse items in the current array.
+ public ImmutableArray.Enumerator GetEnumerator()
+ {
+ return AsImmutableArray().GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)AsImmutableArray()).GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((IEnumerable)AsImmutableArray()).GetEnumerator();
+ }
+
+ ///
+ /// Implicitly converts an to .
+ ///
+ /// An instance from a given .
+ public static implicit operator EquatableArray(ImmutableArray array)
+ {
+ return FromImmutableArray(array);
+ }
+
+ ///
+ /// Implicitly converts an to .
+ ///
+ /// An instance from a given .
+ public static implicit operator ImmutableArray(EquatableArray array)
+ {
+ return array.AsImmutableArray();
+ }
+
+ ///
+ /// Checks whether two values are the same.
+ ///
+ /// The first value.
+ /// The second value.
+ /// Whether and are equal.
+ public static bool operator ==(EquatableArray left, EquatableArray right)
+ {
+ return left.Equals(right);
+ }
+
+ ///
+ /// Checks whether two values are not the same.
+ ///
+ /// The first value.
+ /// The second value.
+ /// Whether and are not equal.
+ public static bool operator !=(EquatableArray left, EquatableArray right)
+ {
+ return !left.Equals(right);
+ }
+}
diff --git a/Robust.Roslyn.Shared/Helpers/HashCode.cs b/Robust.Roslyn.Shared/Helpers/HashCode.cs
new file mode 100644
index 000000000..38445d2d8
--- /dev/null
+++ b/Robust.Roslyn.Shared/Helpers/HashCode.cs
@@ -0,0 +1,190 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// Taken from https://raw.githubusercontent.com/CommunityToolkit/dotnet/ecd1711b740f4f88d2bb943ce292ae4fc90df1bc/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/HashCode.cs
+
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+
+#pragma warning disable CS0809
+
+namespace System;
+
+#nullable enable
+
+///
+/// A polyfill type that mirrors some methods from on .NET 6.
+///
+public struct HashCode
+{
+ private const uint Prime1 = 2654435761U;
+ private const uint Prime2 = 2246822519U;
+ private const uint Prime3 = 3266489917U;
+ private const uint Prime4 = 668265263U;
+ private const uint Prime5 = 374761393U;
+
+ private static readonly uint seed = GenerateGlobalSeed();
+
+ private uint v1, v2, v3, v4;
+ private uint queue1, queue2, queue3;
+ private uint length;
+
+ ///
+ /// Initializes the default seed.
+ ///
+ /// A random seed.
+ private static unsafe uint GenerateGlobalSeed()
+ {
+ byte[] bytes = new byte[4];
+
+ using (RandomNumberGenerator generator = RandomNumberGenerator.Create())
+ {
+ generator.GetBytes(bytes);
+ }
+
+ return BitConverter.ToUInt32(bytes, 0);
+ }
+
+ ///
+ /// Adds a single value to the current hash.
+ ///
+ /// The type of the value to add into the hash code.
+ /// The value to add into the hash code.
+ public void Add(T value)
+ {
+ Add(value?.GetHashCode() ?? 0);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
+ {
+ v1 = seed + Prime1 + Prime2;
+ v2 = seed + Prime2;
+ v3 = seed;
+ v4 = seed - Prime1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint Round(uint hash, uint input)
+ {
+ return RotateLeft(hash + input * Prime2, 13) * Prime1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint QueueRound(uint hash, uint queuedValue)
+ {
+ return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixState(uint v1, uint v2, uint v3, uint v4)
+ {
+ return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixEmptyState()
+ {
+ return seed + Prime5;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint MixFinal(uint hash)
+ {
+ hash ^= hash >> 15;
+ hash *= Prime2;
+ hash ^= hash >> 13;
+ hash *= Prime3;
+ hash ^= hash >> 16;
+
+ return hash;
+ }
+
+ private void Add(int value)
+ {
+ uint val = (uint)value;
+ uint previousLength = this.length++;
+ uint position = previousLength % 4;
+
+ if (position == 0)
+ {
+ this.queue1 = val;
+ }
+ else if (position == 1)
+ {
+ this.queue2 = val;
+ }
+ else if (position == 2)
+ {
+ this.queue3 = val;
+ }
+ else
+ {
+ if (previousLength == 3)
+ {
+ Initialize(out this.v1, out this.v2, out this.v3, out this.v4);
+ }
+
+ this.v1 = Round(this.v1, this.queue1);
+ this.v2 = Round(this.v2, this.queue2);
+ this.v3 = Round(this.v3, this.queue3);
+ this.v4 = Round(this.v4, val);
+ }
+ }
+
+ ///
+ /// Gets the resulting hashcode from the current instance.
+ ///
+ /// The resulting hashcode from the current instance.
+ public int ToHashCode()
+ {
+ uint length = this.length;
+ uint position = length % 4;
+ uint hash = length < 4 ? MixEmptyState() : MixState(this.v1, this.v2, this.v3, this.v4);
+
+ hash += length * 4;
+
+ if (position > 0)
+ {
+ hash = QueueRound(hash, this.queue1);
+
+ if (position > 1)
+ {
+ hash = QueueRound(hash, this.queue2);
+
+ if (position > 2)
+ {
+ hash = QueueRound(hash, this.queue3);
+ }
+ }
+ }
+
+ hash = MixFinal(hash);
+
+ return (int)hash;
+ }
+
+ ///
+ [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override int GetHashCode() => throw new NotSupportedException();
+
+ ///
+ [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public override bool Equals(object? obj) => throw new NotSupportedException();
+
+ ///
+ /// Rotates the specified value left by the specified number of bits.
+ /// Similar in behavior to the x86 instruction ROL.
+ ///
+ /// The value to rotate.
+ /// The number of bits to rotate by.
+ /// Any value outside the range [0..31] is treated as congruent mod 32.
+ /// The rotated value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static uint RotateLeft(uint value, int offset)
+ {
+ return (value << offset) | (value >> (32 - offset));
+ }
+}
diff --git a/Robust.Roslyn.Shared/PartialTypeHelper.cs b/Robust.Roslyn.Shared/PartialTypeHelper.cs
new file mode 100644
index 000000000..2044b0985
--- /dev/null
+++ b/Robust.Roslyn.Shared/PartialTypeHelper.cs
@@ -0,0 +1,121 @@
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Robust.Roslyn.Shared.Helpers;
+
+namespace Robust.Roslyn.Shared;
+
+#nullable enable
+
+///
+/// All the information to make a partial type alternative for a type.
+///
+public sealed record PartialTypeInfo(
+ string? Namespace,
+ string Name,
+ string DisplayName,
+ EquatableArray TypeParameterNames,
+ bool IsValid,
+ Location SyntaxLocation,
+ Accessibility Accessibility,
+ TypeKind Kind,
+ bool IsRecord,
+ bool IsAbstract)
+{
+ public static PartialTypeInfo FromSymbol(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
+ {
+ var typeParameters = ImmutableArray.Empty;
+ if (symbol.TypeParameters.Length > 0)
+ {
+ var builder = ImmutableArray.CreateBuilder(symbol.TypeParameters.Length);
+ foreach (var typeParameter in symbol.TypeParameters)
+ {
+ builder.Add(typeParameter.Name);
+ }
+
+ typeParameters = builder.MoveToImmutable();
+ }
+
+ return new PartialTypeInfo(
+ symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(),
+ symbol.Name,
+ symbol.ToDisplayString(),
+ typeParameters,
+ syntax.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword)),
+ syntax.Keyword.GetLocation(),
+ symbol.DeclaredAccessibility,
+ symbol.TypeKind,
+ symbol.IsRecord,
+ symbol.IsAbstract);
+ }
+
+ public bool CheckPartialDiagnostic(SourceProductionContext context, DiagnosticDescriptor diagnostic)
+ {
+ if (!IsValid)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(diagnostic, SyntaxLocation, DisplayName));
+ return true;
+ }
+
+ return false;
+ }
+
+ public string GetGeneratedFileName()
+ {
+ var name = Namespace == null ? Name : $"{Namespace}.{Name}";
+ if (TypeParameterNames.AsImmutableArray().Length > 0)
+ name += $"`{TypeParameterNames.AsImmutableArray().Length}";
+
+ name += ".g.cs";
+
+ return name;
+ }
+
+ public void WriteHeader(StringBuilder builder)
+ {
+ if (Namespace != null)
+ builder.AppendLine($"namespace {Namespace};\n");
+
+ // TODO: Nested classes
+
+ var access = Accessibility switch
+ {
+ Accessibility.Private => "private",
+ Accessibility.ProtectedAndInternal => "private protected",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ Accessibility.Protected => "protected",
+ Accessibility.Internal => "internal",
+ _ => "public"
+ };
+
+ string keyword;
+ if (Kind == TypeKind.Interface)
+ {
+ keyword = "interface";
+ }
+ else
+ {
+ if (IsRecord)
+ {
+ keyword = Kind == TypeKind.Struct ? "record struct" : "record";
+ }
+ else
+ {
+ keyword = Kind == TypeKind.Struct ? "struct" : "class";
+ }
+ }
+
+ builder.Append($"{access} {(IsAbstract ? "abstract " : "")}partial {keyword} {Name}");
+ if (TypeParameterNames.AsSpan().Length > 0)
+ {
+ builder.Append($"<{string.Join(", ", TypeParameterNames.AsImmutableArray())}>");
+ }
+ }
+
+ public void WriteFooter(StringBuilder builder)
+ {
+ // TODO: Nested classes
+ }
+}
diff --git a/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props b/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props
new file mode 100644
index 000000000..aefce3e02
--- /dev/null
+++ b/Robust.Roslyn.Shared/Robust.Roslyn.Shared.props
@@ -0,0 +1,38 @@
+
+
+
+ netstandard2.0
+ 12
+ enable
+ true
+ true
+ true
+ enable
+ System.Index;System.Diagnostics.CodeAnalysis.NotNullWhenAttribute;System.Runtime.CompilerServices.IsExternalInit;System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute
+ RS2008
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+ Robust.Roslyn.Shared\%(RecursiveDir)%(Filename)%(Extension)
+
+
+
+
diff --git a/Robust.Roslyn.Shared/TypeSymbolHelper.cs b/Robust.Roslyn.Shared/TypeSymbolHelper.cs
new file mode 100644
index 000000000..cb969139c
--- /dev/null
+++ b/Robust.Roslyn.Shared/TypeSymbolHelper.cs
@@ -0,0 +1,28 @@
+using Microsoft.CodeAnalysis;
+
+namespace Robust.Roslyn.Shared;
+
+#nullable enable
+
+public static class TypeSymbolHelper
+{
+ public static bool ShittyTypeMatch(INamedTypeSymbol type, string attributeMetadataName)
+ {
+ // Doing it like this only allocates when the type actually matches, which is good enough for me right now.
+ if (!attributeMetadataName.EndsWith(type.Name))
+ return false;
+
+ return type.ToDisplayString() == attributeMetadataName;
+ }
+
+ public static bool ImplementsInterface(INamedTypeSymbol type, string interfaceTypeName)
+ {
+ foreach (var interfaceType in type.AllInterfaces)
+ {
+ if (ShittyTypeMatch(interfaceType, interfaceTypeName))
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Robust.Serialization.Generator/ComponentPauseGenerator.cs b/Robust.Serialization.Generator/ComponentPauseGenerator.cs
new file mode 100644
index 000000000..a1ee87c1f
--- /dev/null
+++ b/Robust.Serialization.Generator/ComponentPauseGenerator.cs
@@ -0,0 +1,252 @@
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Robust.Roslyn.Shared;
+using Robust.Roslyn.Shared.Helpers;
+
+namespace Robust.Serialization.Generator;
+
+///
+/// Automatically generates implementations for handling timer unpausing.
+///
+[Generator(LanguageNames.CSharp)]
+public sealed class ComponentPauseGenerator : IIncrementalGenerator
+{
+ private const string AutoGenerateComponentPauseAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentPauseAttribute";
+ private const string AutoPausedFieldAttributeName = "Robust.Shared.Analyzers.AutoPausedFieldAttribute";
+ private const string AutoNetworkFieldAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute";
+ // ReSharper disable once InconsistentNaming
+ private const string IComponentTypeName = "Robust.Shared.GameObjects.IComponent";
+
+ private static readonly DiagnosticDescriptor NotComponentDiagnostic = new(
+ Diagnostics.IdComponentPauseNotComponent,
+ "Class must be an IComponent to use AutoGenerateComponentPause",
+ "Class '{0}' must implement IComponent to be used with [AutoGenerateComponentPause]",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ private static readonly DiagnosticDescriptor NoFieldsDiagnostic = new(
+ Diagnostics.IdComponentPauseNoFields,
+ "AutoGenerateComponentPause has no fields",
+ "Class '{0}' has [AutoGenerateComponentPause] but has no fields or properties with [AutoPausedField]",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ true);
+
+ private static readonly DiagnosticDescriptor NoParentAttributeDiagnostic = new(
+ Diagnostics.IdComponentPauseNoParentAttribute,
+ "AutoPausedField on type of field without AutoGenerateComponentPause",
+ "Field '{0}' has [AutoPausedField] but its containing type does not have [AutoGenerateComponentPause]",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ private static readonly DiagnosticDescriptor WrongTypeAttributeDiagnostic = new(
+ Diagnostics.IdComponentPauseWrongTypeAttribute,
+ "AutoPausedField has wrong type",
+ "Field '{0}' has [AutoPausedField] but is not of type TimeSpan",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var componentInfos = context.SyntaxProvider.ForAttributeWithMetadataName(
+ AutoGenerateComponentPauseAttributeName,
+ (syntaxNode, _) => syntaxNode is TypeDeclarationSyntax,
+ (syntaxContext, _) =>
+ {
+ var symbol = (INamedTypeSymbol)syntaxContext.TargetSymbol;
+
+ var typeDeclarationSyntax = (TypeDeclarationSyntax) syntaxContext.TargetNode;
+ var partialTypeInfo = PartialTypeInfo.FromSymbol(
+ symbol,
+ typeDeclarationSyntax);
+
+ var dirty = AttributeHelper.GetNamedArgumentBool(syntaxContext.Attributes[0], "Dirty", false);
+
+ var fieldBuilder = ImmutableArray.CreateBuilder();
+ foreach (var member in symbol.GetMembers())
+ {
+ if (!AttributeHelper.HasAttribute(member, AutoPausedFieldAttributeName, out var _))
+ continue;
+
+ var type = member switch
+ {
+ IPropertySymbol property => property.Type,
+ IFieldSymbol field => field.Type,
+ _ => null
+ };
+
+ if (type is not INamedTypeSymbol namedType)
+ continue;
+
+ var invalid = false;
+ var nullable = false;
+ if (namedType.Name != "TimeSpan")
+ {
+ if (namedType is { Name: "Nullable", TypeArguments: [{Name: "TimeSpan"}] })
+ {
+ nullable = true;
+ }
+ else
+ {
+ invalid = true;
+ }
+ }
+
+ // If any pause field has [AutoNetworkedField], automatically mark it to dirty on unpause.
+ if (AttributeHelper.HasAttribute(member, AutoNetworkFieldAttributeName, out var _))
+ dirty = true;
+
+ fieldBuilder.Add(new FieldInfo(member.Name, nullable, invalid, member.Locations[0]));
+ }
+
+ return new ComponentInfo(
+ partialTypeInfo,
+ EquatableArray.FromImmutableArray(fieldBuilder.ToImmutable()),
+ dirty,
+ !TypeSymbolHelper.ImplementsInterface(symbol, IComponentTypeName),
+ typeDeclarationSyntax.Identifier.GetLocation());
+ });
+
+ context.RegisterImplementationSourceOutput(componentInfos, static (productionContext, info) =>
+ {
+ if (info.NotComponent)
+ {
+ productionContext.ReportDiagnostic(Diagnostic.Create(
+ NotComponentDiagnostic,
+ info.Location,
+ info.PartialTypeInfo.Name));
+ return;
+ }
+
+ // Component always have to be partial anyways due to the serialization generator.
+ // So I can't be arsed to define a diagnostic for this.
+ if (!info.PartialTypeInfo.IsValid)
+ return;
+
+ if (info.Fields.AsImmutableArray().Length == 0)
+ {
+ productionContext.ReportDiagnostic(Diagnostic.Create(
+ NoFieldsDiagnostic,
+ info.Location,
+ info.PartialTypeInfo.Name));
+ return;
+ }
+
+ var builder = new StringBuilder();
+
+ builder.AppendLine("""
+ //
+
+ using Robust.Shared.GameObjects;
+
+ """);
+
+ info.PartialTypeInfo.WriteHeader(builder);
+
+ builder.AppendLine();
+ builder.AppendLine("{");
+
+ builder.AppendLine($$"""
+ [RobustAutoGenerated]
+ public sealed class {{info.PartialTypeInfo.Name}}_AutoPauseSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<{{info.PartialTypeInfo.Name}}, EntityUnpausedEvent>(OnEntityUnpaused);
+ }
+
+ private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} component, ref EntityUnpausedEvent args)
+ {
+ """);
+
+ var anyValidField = false;
+ foreach (var field in info.Fields)
+ {
+ if (field.Invalid)
+ {
+ productionContext.ReportDiagnostic(Diagnostic.Create(WrongTypeAttributeDiagnostic, field.Location));
+ continue;
+ }
+
+ if (field.Nullable)
+ {
+ builder.AppendLine($"""
+ if (component.{field.Name}.HasValue)
+ component.{field.Name} = component.{field.Name}.Value + args.PausedTime;
+ """);
+ }
+ else
+ {
+ builder.AppendLine($" component.{field.Name} += args.PausedTime;");
+ }
+
+ anyValidField = true;
+ }
+
+ if (!anyValidField)
+ return;
+
+ if (info.Dirty)
+ builder.AppendLine(" Dirty(uid, component);");
+
+ builder.AppendLine("""
+ }
+ }
+ """);
+
+ builder.AppendLine("}");
+
+ info.PartialTypeInfo.WriteFooter(builder);
+
+ productionContext.AddSource(info.PartialTypeInfo.GetGeneratedFileName(), builder.ToString());
+ });
+
+ // Code to report diagnostic for fields that have it but don't have the attribute on the parent.
+ var allFields = context.SyntaxProvider.ForAttributeWithMetadataName(
+ AutoPausedFieldAttributeName,
+ (syntaxNode, _) => syntaxNode is VariableDeclaratorSyntax or PropertyDeclarationSyntax,
+ (syntaxContext, _) =>
+ {
+ var errorTarget = syntaxContext.TargetNode is PropertyDeclarationSyntax prop
+ ? prop.Identifier.GetLocation()
+ : syntaxContext.TargetNode.GetLocation();
+ return new AllFieldInfo(
+ syntaxContext.TargetSymbol.Name,
+ syntaxContext.TargetSymbol.ContainingType.ToDisplayString(),
+ errorTarget);
+ });
+
+ var allComponentsTogether = componentInfos.Collect();
+ var allFieldsTogether = allFields.Collect();
+ var componentFieldJoin = allFieldsTogether.Combine(allComponentsTogether);
+
+ context.RegisterImplementationSourceOutput(componentFieldJoin, (productionContext, info) =>
+ {
+ var componentsByName = new HashSet(info.Right.Select(x => x.PartialTypeInfo.DisplayName));
+ foreach (var field in info.Left)
+ {
+ if (!componentsByName.Contains(field.ParentDisplayName))
+ {
+ productionContext.ReportDiagnostic(
+ Diagnostic.Create(NoParentAttributeDiagnostic, field.Location, field.Name));
+ }
+ }
+ });
+ }
+
+ public sealed record ComponentInfo(
+ PartialTypeInfo PartialTypeInfo,
+ EquatableArray Fields,
+ bool Dirty,
+ bool NotComponent,
+ Location Location);
+
+ public sealed record FieldInfo(string Name, bool Nullable, bool Invalid, Location Location);
+
+ public sealed record AllFieldInfo(string Name, string ParentDisplayName, Location Location);
+}
diff --git a/Robust.Serialization.Generator/Properties/launchSettings.json b/Robust.Serialization.Generator/Properties/launchSettings.json
new file mode 100644
index 000000000..9e8e5b146
--- /dev/null
+++ b/Robust.Serialization.Generator/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Generators": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "../../Content.Shared/Content.Shared.csproj"
+ }
+ }
+}
diff --git a/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj b/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj
index 8b7a626e1..0ecebf949 100644
--- a/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj
+++ b/Robust.Serialization.Generator/Robust.Serialization.Generator.csproj
@@ -1,16 +1,5 @@
-
- netstandard2.0
- 11
- enable
- true
-
-
-
-
-
-
-
+
diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
index cd1fe3952..b94c9e7d7 100644
--- a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
+++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
@@ -32,7 +32,7 @@ namespace Robust.Shared.CompNetworkGenerator
private const string GlobalHashSetName = "global::System.Collections.Generic.HashSet";
private const string GlobalListName = "global::System.Collections.Generic.List";
- private static string GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
+ private static string? GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
{
var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
var componentName = classSymbol.Name;
@@ -280,10 +280,10 @@ public partial class {componentName}
{
var attr = type.Attribute;
var raiseEv = false;
- if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value != null)
+ if (attr.ConstructorArguments is [{Value: bool raise}])
{
// Get the afterautohandle bool, which is first constructor arg
- raiseEv = (bool) attr.ConstructorArguments[0].Value;
+ raiseEv = raise;
}
var source = GenerateSource(context, type.Type, comp, raiseEv);
@@ -325,11 +325,11 @@ public partial class {componentName}
attr.AttributeClass != null &&
attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
+ if (typeSymbol == null)
+ continue;
+
if (relevantAttribute == null)
{
- if (typeSymbol == null)
- continue;
-
foreach (var mem in typeSymbol.GetMembers())
{
var attribute = mem.GetAttributes().FirstOrDefault(a =>
diff --git a/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
index c12253963..076e7a492 100644
--- a/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
+++ b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
@@ -1,13 +1,5 @@
-
- netstandard2.0
- 9
- true
-
+
-
-
-
-
diff --git a/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs b/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs
new file mode 100644
index 000000000..55ddfc5f2
--- /dev/null
+++ b/Robust.Shared/Analyzers/ComponentPauseGeneratorAttributes.cs
@@ -0,0 +1,29 @@
+using System;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Robust.Shared.Analyzers;
+
+///
+/// Indicate that a should automatically handle unpausing of timer fields.
+///
+///
+/// When this attribute is set on a , an will automatically be
+/// generated that increments any fields tagged with when the entity is unpaused
+/// ().
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+[BaseTypeRequired(typeof(IComponent))]
+public sealed class AutoGenerateComponentPauseAttribute : Attribute
+{
+ public bool Dirty = false;
+}
+
+///
+/// Mark a field or property to automatically handle unpausing with .
+///
+///
+/// The type of the field or prototype must be (potentially nullable).
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class AutoPausedFieldAttribute : Attribute;