diff --git a/MSBuild/Robust.CompNetworkGenerator.targets b/MSBuild/Robust.CompNetworkGenerator.targets
new file mode 100644
index 000000000..944d6f0f0
--- /dev/null
+++ b/MSBuild/Robust.CompNetworkGenerator.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Robust.Analyzers/AccessAnalyzer.cs b/Robust.Analyzers/AccessAnalyzer.cs
index 03ab376e4..adb5880af 100644
--- a/Robust.Analyzers/AccessAnalyzer.cs
+++ b/Robust.Analyzers/AccessAnalyzer.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
@@ -12,6 +13,7 @@ namespace Robust.Analyzers
public class AccessAnalyzer : DiagnosticAnalyzer
{
private const string AccessAttributeType = "Robust.Shared.Analyzers.AccessAttribute";
+ private const string RobustAutoGeneratedAttributeType = "Robust.Shared.Analyzers.RobustAutoGeneratedAttribute";
private const string PureAttributeType = "System.Diagnostics.Contracts.PureAttribute";
[SuppressMessage("ReSharper", "RS2008")]
@@ -73,11 +75,20 @@ namespace Robust.Analyzers
// Get the attributes
var friendAttribute = context.Compilation.GetTypeByMetadataName(AccessAttributeType);
+ var autoGenAttribute = context.Compilation.GetTypeByMetadataName(RobustAutoGeneratedAttributeType);
// Get the type that is containing this expression, or, the type where this is happening.
if (context.ContainingSymbol?.ContainingType is not {} accessingType)
return;
+ // Should we ignore the access attempt due to the accessing type being auto-generated?
+ if (accessingType.GetAttributes().FirstOrDefault(a =>
+ a.AttributeClass != null &&
+ a.AttributeClass.Equals(autoGenAttribute, SymbolEqualityComparer.Default)) is { } attr)
+ {
+ return;
+ }
+
// Determine which type of access is happening here... Read, write or execute?
var accessAttempt = DetermineAccess(context, targetAccess, operation);
diff --git a/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
new file mode 100644
index 000000000..dce955c70
--- /dev/null
+++ b/Robust.Shared.CompNetworkGenerator/ComponentNetworkGenerator.cs
@@ -0,0 +1,295 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Robust.Shared.CompNetworkGenerator
+{
+ [Generator]
+ public class ComponentNetworkGenerator : ISourceGenerator
+ {
+ private const string ClassAttributeName = "Robust.Shared.Analyzers.AutoGenerateComponentStateAttribute";
+ private const string MemberAttributeName = "Robust.Shared.Analyzers.AutoNetworkedFieldAttribute";
+
+ private static string GenerateSource(in GeneratorExecutionContext context, INamedTypeSymbol classSymbol, CSharpCompilation comp, bool raiseAfterAutoHandle)
+ {
+ var nameSpace = classSymbol.ContainingNamespace.ToDisplayString();
+ var componentName = classSymbol.Name;
+ var stateName = $"{componentName}_AutoState";
+
+ var members = classSymbol.GetMembers();
+ var fields = new List<(ITypeSymbol Type, string FieldName, AttributeData Attribute)>();
+ var fieldAttr = comp.GetTypeByMetadataName(MemberAttributeName);
+
+ foreach (var mem in members)
+ {
+ var attribute = mem.GetAttributes().FirstOrDefault(a =>
+ a.AttributeClass != null &&
+ a.AttributeClass.Equals(fieldAttr, SymbolEqualityComparer.Default));
+
+ if (attribute == null)
+ {
+ continue;
+ }
+
+ switch (mem)
+ {
+ case IFieldSymbol field:
+ fields.Add((field.Type, field.Name, attribute));
+ break;
+ case IPropertySymbol prop:
+ {
+ if (prop.SetMethod == null || prop.SetMethod.DeclaredAccessibility != Accessibility.Public)
+ {
+ var msg = "Property is marked with [AutoNetworkField], but has no accessible setter method.";
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "RXN0008",
+ msg,
+ msg,
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ classSymbol.Locations[0]));
+ continue;
+ }
+
+ if (prop.GetMethod == null || prop.GetMethod.DeclaredAccessibility != Accessibility.Public)
+ {
+ var msg = "Property is marked with [AutoNetworkField], but has no accessible getter method.";
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "RXN0008",
+ msg,
+ msg,
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ classSymbol.Locations[0]));
+ continue;
+ }
+
+ fields.Add((prop.Type, prop.Name, attribute));
+ break;
+ }
+ }
+ }
+
+ if (fields.Count == 0)
+ {
+ var msg = "Component is marked with [AutoGenerateComponentState], but has no valid members marked with [AutoNetworkField].";
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "RXN0007",
+ msg,
+ msg,
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ classSymbol.Locations[0]));
+
+ return null;
+ }
+
+ // eg:
+ // public string Name = default!;
+ // public int Count = default!;
+ var stateFields = new StringBuilder();
+
+ // eg:
+ // Name = component.Name,
+ // Count = component.Count,
+ var getStateInit = new StringBuilder();
+
+ // eg:
+ // component.Name = state.Name;
+ // component.Count = state.Count;
+ var handleStateSetters = new StringBuilder();
+
+ foreach (var (type, name, attribute) in fields)
+ {
+ stateFields.Append($@"
+ public {type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} {name} = default!;");
+
+ // get first ctor arg of the field attribute, which determines whether the field should be cloned
+ // (like if its a dict or list)
+ if (attribute.ConstructorArguments[0].Value is bool val && val)
+ {
+ getStateInit.Append($@"
+ {name} = component.{name},");
+
+ handleStateSetters.Append($@"
+ if (state.{name} != null)
+ component.{name} = new(state.{name});");
+ }
+ else
+ {
+ getStateInit.Append($@"
+ {name} = component.{name},");
+
+ handleStateSetters.Append($@"
+ component.{name} = state.{name};");
+ }
+ }
+
+ var eventRaise = "";
+ if (raiseAfterAutoHandle)
+ {
+ eventRaise = @"
+ var ev = new AfterAutoHandleStateEvent(args.Current);
+ EntityManager.EventBus.RaiseComponentEvent(component, ref ev);";
+ }
+
+ return $@"//
+using Robust.Shared.GameStates;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Serialization;
+
+namespace {nameSpace};
+
+public partial class {componentName}
+{{
+ [Serializable, NetSerializable]
+ public class {stateName} : ComponentState
+ {{{stateFields}
+ }}
+
+ [RobustAutoGenerated]
+ public class {componentName}_AutoNetworkSystem : EntitySystem
+ {{
+ public override void Initialize()
+ {{
+ SubscribeLocalEvent<{componentName}, ComponentGetState>(OnGetState);
+ SubscribeLocalEvent<{componentName}, ComponentHandleState>(OnHandleState);
+ }}
+
+ private void OnGetState(EntityUid uid, {componentName} component, ref ComponentGetState args)
+ {{
+ args.State = new {stateName}
+ {{{getStateInit}
+ }};
+ }}
+
+ private void OnHandleState(EntityUid uid, {componentName} component, ref ComponentHandleState args)
+ {{
+ if (args.Current is not {stateName} state)
+ return;
+{handleStateSetters}{eventRaise}
+ }}
+ }}
+}}
+";
+ }
+
+ public void Execute(GeneratorExecutionContext context)
+ {
+ var comp = (CSharpCompilation) context.Compilation;
+
+ if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver))
+ {
+ return;
+ }
+
+ var symbols = GetAnnotatedTypes(context, comp, receiver);
+
+ // Generate component sources and add
+ foreach (var type in symbols)
+ {
+ try
+ {
+ var attr = type.Attribute;
+ var raiseEv = false;
+ if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value != null)
+ {
+ // Get the afterautohandle bool, which is first constructor arg
+ raiseEv = (bool) attr.ConstructorArguments[0].Value;
+ }
+
+ var source = GenerateSource(context, type.Type, comp, raiseEv);
+ // can be null if no members marked with network field, which already has a diagnostic, so
+ // just continue
+ if (source == null)
+ continue;
+
+ context.AddSource($"{type.Type.Name}_CompNetwork.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+ catch (Exception e)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "RXN0003",
+ "Unhandled exception occured while generating automatic component state handling.",
+ $"Unhandled exception occured while generating automatic component state handling: {e}",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ type.Type.Locations[0]));
+ }
+ }
+ }
+
+ private IReadOnlyList<(INamedTypeSymbol Type, AttributeData Attribute)> GetAnnotatedTypes(in GeneratorExecutionContext context,
+ CSharpCompilation comp, NameReferenceSyntaxReceiver receiver)
+ {
+ var symbols = new List<(INamedTypeSymbol, AttributeData)>();
+ var attributeSymbol = comp.GetTypeByMetadataName(ClassAttributeName);
+ foreach (var candidateClass in receiver.CandidateClasses)
+ {
+ var model = comp.GetSemanticModel(candidateClass.SyntaxTree);
+ var typeSymbol = model.GetDeclaredSymbol(candidateClass);
+ var relevantAttribute = typeSymbol?.GetAttributes().FirstOrDefault(attr =>
+ attr.AttributeClass != null &&
+ attr.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
+
+ if (relevantAttribute == null)
+ {
+ continue;
+ }
+
+ var isPartial = candidateClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
+
+ if (isPartial)
+ {
+ symbols.Add((typeSymbol, relevantAttribute));
+ }
+ else
+ {
+ var missingPartialKeywordMessage =
+ $"The type {typeSymbol.Name} should be declared with the 'partial' keyword " +
+ "as it is annotated with the [AutoGenerateComponentState] attribute.";
+
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ new DiagnosticDescriptor(
+ "RXN0006",
+ missingPartialKeywordMessage,
+ missingPartialKeywordMessage,
+ "Usage",
+ DiagnosticSeverity.Error,
+ true),
+ Location.None));
+ }
+ }
+
+ return symbols;
+ }
+
+ public void Initialize(GeneratorInitializationContext context)
+ {
+ if (!Debugger.IsAttached)
+ {
+ //Debugger.Launch();
+ }
+ context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());
+ }
+ }
+}
diff --git a/Robust.Shared.CompNetworkGenerator/NameReferenceSyntaxReceiver.cs b/Robust.Shared.CompNetworkGenerator/NameReferenceSyntaxReceiver.cs
new file mode 100644
index 000000000..e449b0d71
--- /dev/null
+++ b/Robust.Shared.CompNetworkGenerator/NameReferenceSyntaxReceiver.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Robust.Shared.CompNetworkGenerator
+{
+ public sealed class NameReferenceSyntaxReceiver : ISyntaxReceiver
+ {
+ public List CandidateClasses { get; } = new List();
+
+ public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+ {
+ if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
+ classDeclarationSyntax.AttributeLists.Count > 0)
+ CandidateClasses.Add(classDeclarationSyntax);
+ }
+ }
+}
diff --git a/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
new file mode 100644
index 000000000..878ba53d5
--- /dev/null
+++ b/Robust.Shared.CompNetworkGenerator/Robust.Shared.CompNetworkGenerator.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
diff --git a/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs b/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs
new file mode 100644
index 000000000..4018bdd8c
--- /dev/null
+++ b/Robust.Shared/Analyzers/ComponentNetworkGeneratorAuxiliary.cs
@@ -0,0 +1,57 @@
+using System;
+using JetBrains.Annotations;
+using Robust.Shared.GameObjects;
+
+namespace Robust.Shared.Analyzers;
+
+///
+/// When a component is marked with this attribute, any members it has marked with
+/// will automatically be replicated using component states to clients. Systems which need to have more intelligent
+/// component state replication beyond just directly setting variables should not use this attribute.
+///
+[AttributeUsage(AttributeTargets.Class, Inherited = false)]
+[BaseTypeRequired(typeof(Component))]
+public sealed class AutoGenerateComponentStateAttribute : Attribute
+{
+ ///
+ /// If this is true, the autogenerated code will raise a component event
+ /// so that user-defined systems can have effects after handling state without redefining all replication.
+ ///
+ public bool RaiseAfterAutoHandleState;
+
+ public AutoGenerateComponentStateAttribute(bool raiseAfterAutoHandleState = false)
+ {
+ RaiseAfterAutoHandleState = raiseAfterAutoHandleState;
+ }
+}
+
+///
+/// Used to mark component members which should be automatically replicated, assuming the component is marked with
+/// .
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class AutoNetworkedFieldAttribute : Attribute
+{
+ ///
+ /// Determines whether the data should be wrapped in a new() when setting in get/handlestate
+ /// e.g. for cloning collections like dictionaries or hashsets which is sometimes necessary.
+ ///
+ ///
+ /// This should only be true if the type actually has a constructor that takes in itself.
+ ///
+ [UsedImplicitly]
+ public bool CloneData;
+
+ public AutoNetworkedFieldAttribute(bool cloneData=false)
+ {
+ CloneData = cloneData;
+ }
+}
+
+///
+/// Raised as a component event after auto handling state is done, if
+/// is true, so that other systems
+/// can have effects after handling state without having to redefine all replication.
+///
+[ByRefEvent]
+public record struct AfterAutoHandleStateEvent(ComponentState State);
diff --git a/Robust.Shared/Analyzers/RobustAutoGeneratedAttribute.cs b/Robust.Shared/Analyzers/RobustAutoGeneratedAttribute.cs
new file mode 100644
index 000000000..6c48e5445
--- /dev/null
+++ b/Robust.Shared/Analyzers/RobustAutoGeneratedAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Robust.Shared.Analyzers;
+
+///
+/// Placed on auto-generated classes to mark to certain robust analyzers that they are auto-generated
+/// and may need to be ignored (e.g. the access analyzer)
+///
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class RobustAutoGeneratedAttribute : Attribute
+{
+}
diff --git a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
index 296fe186e..685410d04 100644
--- a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
+++ b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs
@@ -57,12 +57,13 @@ namespace Robust.Shared.GameObjects
///
///
/// This has a very specific purpose, and has massive potential to be abused.
- /// DO NOT EXPOSE THIS TO CONTENT.
+ /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
+ /// is because of the component network source generator.
///
/// Event to dispatch.
/// Component receiving the event.
/// Event arguments for the event.
- internal void RaiseComponentEvent(IComponent component, TEvent args)
+ public void RaiseComponentEvent(IComponent component, TEvent args)
where TEvent : notnull;
///
@@ -70,13 +71,14 @@ namespace Robust.Shared.GameObjects
///
///
/// This has a very specific purpose, and has massive potential to be abused.
- /// DO NOT EXPOSE THIS TO CONTENT.
+ /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
+ /// is because of the component network source generator.
///
/// Event to dispatch.
/// Component receiving the event.
/// Type of the component, for faster lookups.
/// Event arguments for the event.
- internal void RaiseComponentEvent(IComponent component, CompIdx idx, TEvent args)
+ public void RaiseComponentEvent(IComponent component, CompIdx idx, TEvent args)
where TEvent : notnull;
///
@@ -84,12 +86,13 @@ namespace Robust.Shared.GameObjects
///
///
/// This has a very specific purpose, and has massive potential to be abused.
- /// DO NOT EXPOSE THIS TO CONTENT.
+ /// DO NOT USE THIS IN CONTENT UNLESS YOU KNOW WHAT YOU'RE DOING, the only reason it's not internal
+ /// is because of the component network source generator.
///
/// Event to dispatch.
/// Component receiving the event.
/// Event arguments for the event.
- internal void RaiseComponentEvent(IComponent component, ref TEvent args)
+ public void RaiseComponentEvent(IComponent component, ref TEvent args)
where TEvent : notnull;
public void OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare();
diff --git a/Robust.Shared/Robust.Shared.csproj b/Robust.Shared/Robust.Shared.csproj
index b88aa016a..f4d422be2 100644
--- a/Robust.Shared/Robust.Shared.csproj
+++ b/Robust.Shared/Robust.Shared.csproj
@@ -50,4 +50,5 @@
+