diff --git a/Directory.Packages.props b/Directory.Packages.props
index 724621432..a08ae7d9d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -74,7 +74,7 @@
-
+
diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props
index 4f36c6fa0..0371fd488 100644
--- a/MSBuild/Robust.Engine.Version.props
+++ b/MSBuild/Robust.Engine.Version.props
@@ -1,4 +1,4 @@
-
-
- 286.0.0
-
+
+
+ 288.0.1
+
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 07d9a1e2c..33eb101eb 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -26,7 +26,7 @@ Don't change the format without looking at the script!
### Internal
-* Pool sprite post-shader render targets in Clyde. ([#6657](https://github.com/space-wizards/RobustToolbox/pull/6657))
+*None yet*
END TEMPLATE-->
@@ -54,6 +54,80 @@ END TEMPLATE-->
*None yet*
+## 288.0.1
+
+### Bugfixes
+
+* Fix server and client getstate not being aligned for ComponentNetworkGenerator.
+* Fix components removed on entity deserializer not flagging the entity as dirty.
+
+
+## 288.0.0
+
+### Breaking changes
+
+* `ReplayData` no longer exposes the `States`/`Messages` lists; use `Count`, `GetState(index)` and `GetMessages(index)` instead. Replay history is now provided lazily through the new `IReplayDataProvider` interface, and `IReplayLoadManager.GenerateCheckpointsAsync` is no longer part of the public API.
+* GridFixtureSystem now updates the grid origin for split grids to re-centre them.
+* Batch font outline drawing with new API methods.
+* SharedMapSystem enumerators can now use struct foreach loops instead of .MoveNext calls and obsoleted the other ones.
+
+### New features
+
+* Added IAudioManager.ConvertAudioDeviceNameForDisplay helper method for decoding OpenAL device names into a more human-readable format.
+* The replay client now streams replay history from disk instead of keeping the entire deserialized replay resident in RAM, both while loading (history is streamed block-by-block through checkpoint generation) and during playback (data blocks are lazily re-read through a small LRU window, configurable via the `replay.loaded_block_window` cvar). Measured on an 861 MB, 1h38m replay this halves load time and cuts peak memory from ~22 GB to ~12 GB.
+* Added WithCompOrNull helper methods to EntitySystems.
+* Fix deletion rectangle rotation
+* Added CVar to mute on unfocus.
+
+### Bugfixes
+
+* Failed runtime prototype uploads are now dropped.
+* Fix spawn tiles window UIBox2 errors.
+* Release all keybinds when window loses focus.
+* Fix chunt pausing not aligning with the attached root pausing.
+
+### Internal
+
+* ISimulation no longer has SpawnEntity methods, resolve IEntityManager and call the spawn methods directly instead.
+* Run GenerateClient and GenerateServer in parallel.
+
+
+## 287.0.0
+
+### Breaking changes
+
+* OccluderComponent now supports convex polygons and no longer uses a bounding box. These use the same limitations as convex hulls for physics (no more than 8 points).
+* Update Yamldotnet to 18.1.0
+* EntityPrototype components are now interned and shared. Any components that have the same datafield data are now shared when stored on PrototypeManager, saving significant amounts of memory.
+* RSIStates now store AtlasTexture and not Texture, speeding up RSI rendering by directly passing it through.
+* Reverted BUI state queueing.
+
+### New features
+
+* Added a field to `ComponentNetworkGenerator` to exclude components from replays.
+* Added support for before and after subscriptions for the new `[SubscribeLocalEvent]` and related attributes.
+
+### Bugfixes
+
+* Make DefaultWindow call base.FrameUpdate to support animations.
+* Fix Discord RPC playtime resetting on join.
+* Fix some WordWrap bugs.
+
+### Other
+
+* Change `OccluderComponent` access to `ReadExecute`.
+* Components that are being removed are no longer serialized.
+* Added test workflows for the template repos.
+
+### Internal
+
+* Rewrite ReflectionManager for performance reasons.
+* Made many optimizations to GameStates, componentregistryserializer, IoC dependencies, and collection serializers.
+* Remove redundant MsgEntity properties.
+* Cached texture UVs for rendering atlas textures.
+* Pool sprite post-shader render targets in Clyde.
+
+
## 286.0.0
### Breaking changes
diff --git a/Resources/Locale/en-US/input.ftl b/Resources/Locale/en-US/input.ftl
index 4b8e1f72b..999438d06 100644
--- a/Resources/Locale/en-US/input.ftl
+++ b/Resources/Locale/en-US/input.ftl
@@ -78,4 +78,15 @@ input-key-RSystem-mac = Right ⌘
input-key-LSystem-linux = Left Meta
input-key-RSystem-linux = Right Meta
+input-key-Help = Help
+input-key-Stop = Stop
+input-key-Again = Again
+input-key-Prop = Props
+input-key-Undo = Undo
+input-key-Cut = Cut
+input-key-Copy = Copy
+input-key-Open = Open
+input-key-Paste = Paste
+input-key-Find = Find
+
input-key-unknown =
diff --git a/Resources/Locale/en-US/replays.ftl b/Resources/Locale/en-US/replays.ftl
index f7949e30c..b8efa68fc 100644
--- a/Resources/Locale/en-US/replays.ftl
+++ b/Resources/Locale/en-US/replays.ftl
@@ -33,6 +33,8 @@ cmd-replay-error-no-replay = Not currently playing a replay.
cmd-replay-error-already-loaded = A replay is already loaded.
cmd-replay-error-run-level = You cannot load a replay while connected to a server.
+cmd-replay-toggleui-desc = Toggles the replay control UI.
+
# Recording commands
cmd-replay-recording-start-desc = Starts a replay recording, optionally with some time limit.
diff --git a/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionAnalyzerTest.cs b/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionAnalyzerTest.cs
new file mode 100644
index 000000000..3c5fdb4c3
--- /dev/null
+++ b/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionAnalyzerTest.cs
@@ -0,0 +1,274 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using VerifyCS =
+ Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier;
+
+namespace Robust.Analyzers.Tests;
+
+[TestOf(typeof(EntitySystemSubscriptionConversionAnalyzer))]
+public sealed class EntitySystemSubscriptionConversionAnalyzerTest
+{
+ private static Task Verifier(string code, params DiagnosticResult[] expected)
+ {
+ var test = new CSharpAnalyzerTest()
+ {
+ TestState =
+ {
+ Sources = { code }
+ },
+ };
+
+ test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
+
+ // ExpectedDiagnostics cannot be set, so we need to AddRange here...
+ test.TestState.ExpectedDiagnostics.AddRange(expected);
+
+ return test.RunAsync();
+ }
+
+ private const string TestTypeDefs = """
+ using Robust.Shared.GameObjects;
+ using System;
+
+ namespace Robust.Shared.GameObjects
+ {
+ public interface IComponent;
+ public abstract class Component : IComponent;
+
+ public readonly struct EntityUid;
+
+ public delegate void ComponentEventRefHandler(EntityUid uid, TComp component, ref TEvent args)
+ where TComp : IComponent
+ where TEvent : notnull;
+
+ public delegate void ComponentEventHandler(EntityUid uid, TComp component, TEvent args)
+ where TComp : IComponent
+ where TEvent : notnull;
+
+ public interface IEntitySystem;
+ public abstract class EntitySystem : IEntitySystem
+ {
+ public virtual void Initialize() { }
+ public void SubscribeLocalEvent(
+ ComponentEventRefHandler handler,
+ Type[]? before = null,
+ Type[]? after = null)
+ where TComp : IComponent
+ where TEvent : notnull
+ { }
+
+ public void SubscribeLocalEvent(
+ ComponentEventHandler handler,
+ Type[]? before = null,
+ Type[]? after = null)
+ where TComp : IComponent
+ where TEvent : notnull
+ { }
+ }
+ }
+
+ namespace Robust.Shared.Analyzers
+ {
+ public sealed class SubscribeLocalEventAttribute : Attribute;
+ }
+
+ public readonly struct TestEvent;
+ public readonly struct TestEvent2;
+ public readonly struct TestEvent3;
+ public sealed partial class TestComponent : IComponent;
+ """;
+
+ [Test]
+ [Description("Tests that a SubscribeLocalEvent invocation in an EntitySystem Intialize method is flagged as elligible for conversion.")]
+ public async Task FlagSubscribeLocalEvent()
+ {
+ const string code = """
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code,
+ // /0/Test0.cs(9,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(9, 9, 9, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a subscription using an anonymous delegate is not flagged as elligible for conversion.")]
+ public async Task IgnoreAnonymousDelegate()
+ {
+ const string code = """
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent((u, c, ref _) => OnTest(u, c));
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp) { }
+ }
+ """;
+
+ await Verifier(code, []);
+ }
+
+ [Test]
+ [Description("Tests that a subscription in a method containing preprocessor directives is not flagged as elligible for conversion.")]
+ public async Task IgnoreWithPreprocessorDirectives()
+ {
+ const string code = """
+
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ #if DEBUG
+ SubscribeLocalEvent(OnTest);
+ #else
+ SubscribeLocalEvent(OnTest2);
+ #endif
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args) { }
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent args) { }
+ }
+ """;
+
+ await Verifier(code, []);
+ }
+
+ [Test]
+ [Description("Tests that a subscription using a generic type parameter is not flagged as elligible for conversion.")]
+ public async Task IgnoreWithGenericComponent()
+ {
+ const string code = """
+
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ where TComp : Component
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest);
+ }
+
+ private void OnTest(EntityUid uid, TComp comp, ref TestEvent args) { }
+ }
+ """;
+
+ await Verifier(code, []);
+ }
+
+ [Test]
+ [Description("Tests that subscriptions using generic methods as event handlers are not flagged as elligible for conversion.")]
+ public async Task IgnoreWithGenericHandler()
+ {
+ const string code = """
+
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest);
+ SubscribeLocalEvent(OnTest);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref T args) where T : TestEventArgs { }
+ }
+
+ public class TestEventArgs;
+ public sealed class TestEventClassA : TestEventArgs;
+ public sealed class TestEventClassB : TestEventArgs;
+ """;
+
+ await Verifier(code, []);
+ }
+
+ [Test]
+ [Description("Tests that subscriptions using event handlers with abstract event types are not flagged as elligible for conversion.")]
+ public async Task IgnoreWithAbstractHandler()
+ {
+ const string code = """
+
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest);
+ SubscribeLocalEvent(OnTest);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, TestEventArgs args) { }
+ }
+
+ public abstract class TestEventArgs;
+ public sealed class TestEventClassA : TestEventArgs;
+ public sealed class TestEventClassB : TestEventArgs;
+ """;
+
+ await Verifier(code, []);
+ }
+
+ [Test]
+ [Description("Tests that subscriptions within if statement blocks are not flagged as elligible for conversion.")]
+ public async Task IgnoreWithIfStatement()
+ {
+ const string code = """
+
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ if (true)
+ SubscribeLocalEvent(OnTest);
+ else
+ SubscribeLocalEvent(OnTest2);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args) { }
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent args) { }
+ }
+ """;
+
+ await Verifier(code, []);
+ }
+}
diff --git a/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionFixerTest.cs b/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionFixerTest.cs
new file mode 100644
index 000000000..2b1c237d1
--- /dev/null
+++ b/Robust.Analyzers.Tests/EntitySystemSubscriptionConversionFixerTest.cs
@@ -0,0 +1,743 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using NUnit.Framework;
+using VerifyCS =
+ Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier;
+
+namespace Robust.Analyzers.Tests;
+
+public sealed class EntitySystemSubscriptionConversionFixerTest
+{
+ private static Task Verifier(string code, string fixedCode, params DiagnosticResult[] expected)
+ {
+ var test = new CSharpCodeFixTest()
+ {
+ TestState =
+ {
+ Sources = { code },
+ },
+ FixedState =
+ {
+ Sources = { fixedCode },
+ }
+ };
+
+ test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
+ test.FixedState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
+
+ test.TestState.ExpectedDiagnostics.AddRange(expected);
+
+ return test.RunAsync();
+ }
+
+ private static Task Verifier(string[] code, string[] fixedCode, params DiagnosticResult[] expected)
+ {
+ var test = new CSharpCodeFixTest();
+
+ foreach (var file in code)
+ {
+ test.TestState.Sources.Add(file);
+ }
+ foreach (var file in fixedCode)
+ {
+ test.FixedState.Sources.Add(file);
+ }
+
+ test.TestState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
+ test.FixedState.Sources.Add(("TestTypeDefs.cs", TestTypeDefs));
+
+ test.TestState.ExpectedDiagnostics.AddRange(expected);
+
+ return test.RunAsync();
+ }
+
+ private const string TestTypeDefs = """
+ using Robust.Shared.GameObjects;
+ using System;
+
+ namespace Robust.Shared.GameObjects
+ {
+ public interface IComponent;
+ public abstract class Component : IComponent;
+
+ public readonly struct EntityUid;
+
+ public delegate void ComponentEventRefHandler(EntityUid uid, TComp component, ref TEvent args)
+ where TComp : IComponent
+ where TEvent : notnull;
+ public delegate void EntityEventHandler(T ev);
+ public delegate void EntitySessionEventHandler(T msg, string foo);
+
+ public interface IEntitySystem;
+ public abstract class EntitySystem : IEntitySystem
+ {
+ public virtual void Initialize() { }
+ public void SubscribeLocalEvent(
+ ComponentEventRefHandler handler,
+ Type[]? before = null, Type[]? after = null)
+ where TComp : IComponent
+ where TEvent : notnull
+ { }
+ protected void SubscribeNetworkEvent(
+ EntityEventHandler handler,
+ Type[]? before = null, Type[]? after = null)
+ where T : notnull
+ { }
+ protected void SubscribeAllEvent(
+ EntitySessionEventHandler handler,
+ Type[]? before = null, Type[]? after = null)
+ where T : notnull
+ { }
+ }
+ }
+
+ namespace Robust.Shared.Analyzers
+ {
+ public sealed class SubscribeLocalEventAttribute(Type[]? before = null, Type[]? after = null) : Attribute;
+ public sealed class SubscribeNetworkEventAttribute(Type[]? before = null, Type[]? after = null) : Attribute;
+ public sealed class EventSubscriptionAttribute(Type[]? before = null, Type[]? after = null) : Attribute;
+ }
+
+ public readonly struct TestEvent;
+ public readonly struct TestEvent2;
+ public sealed partial class TestComponent : IComponent;
+ public sealed class TestNetworkEvent;
+
+ public sealed class SomeOtherSystemA : EntitySystem;
+ public sealed class SomeOtherSystemB : EntitySystem;
+ """;
+
+ [Test]
+ [Description("Tests that a SubscribeLocalEvent invocation is correctly converted to an attribute.")]
+ public async Task ConvertLocalEvent()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that multiple SubscribeLocalEvent invocations are correctly converted to attributes.")]
+ public async Task ConvertLocalEvent_Multiple()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ SubscribeLocalEvent(OnTest2);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent2 args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent2 args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute"),
+ // /0/Test0.cs(11,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(11, 9, 11, 64).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that multiple SubscribeLocalEvent invocations are correctly converted to attributes when there is a gap between them.")]
+ public async Task ConvertLocalEvent_MultipleWithGap()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+
+ SubscribeLocalEvent(OnTest2);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent2 args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest2(EntityUid uid, TestComponent comp, ref TestEvent2 args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute"),
+ // /0/Test0.cs(12,9): info RA0057:Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(12, 9, 12, 64).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a SubscribeLocalEvent invocation with before and after parameters is correctly converted to an attribute.")]
+ public async Task ConvertLocalEvent_WithBeforeAfter()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest, before: [typeof(SomeOtherSystemA)], after: new[] { typeof(SomeOtherSystemB) });
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent(before: [typeof(SomeOtherSystemA)], after: [typeof(SomeOtherSystemB)])]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 141).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a SubscribeLocalEvent invocation a before parameter is correctly converted to an attribute.")]
+ public async Task ConvertLocalEvent_WithBefore()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest, before: new[] { typeof(SomeOtherSystemA) });
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent(before: [typeof(SomeOtherSystemA)])]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 106).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a SubscribeLocalEvent invocation an after parameter is correctly converted to an attribute.")]
+ public async Task ConvertLocalEvent_WithAfter()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest, after: [typeof(SomeOtherSystemA), typeof(SomeOtherSystemB)]);
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent(after: [typeof(SomeOtherSystemA), typeof(SomeOtherSystemB)])]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 123).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a class that isn't marked partial is given the partial modifier when converted.")]
+ public async Task ConvertLocalEvent_AddPartial()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a class is given the using directive for the SubscribeLocalEventAttribute namespace when converted.")]
+ public async Task ConvertLocalEvent_AddUsingDirective()
+ {
+ const string code = """
+ using Robust.Shared.GameObjects;
+
+ public sealed class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(9,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(9, 9, 9, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that the conversion isn't confused by other methods with the same name as the event handler.")]
+ public async Task ConvertLocalEvent_HandlerOverload()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(string foo) { }
+
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ private void OnTest(string foo) { }
+
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a SubscribeNetworkEvent invocation is correctly converted to an attribute.")]
+ public async Task ConvertNetworkEvent()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(TestNetworkEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [SubscribeNetworkEvent]
+ private void OnTest(TestNetworkEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeNetworkEvent can be converted to use SubscribeNetworkEventAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 56).WithArguments("SubscribeNetworkEvent", "SubscribeNetworkEventAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that a SubscribeAllEvent invocation is correctly converted to an attribute.")]
+ public async Task ConvertAllEvent()
+ {
+ const string code = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeAllEvent(OnTest); // Comment here
+ }
+
+ private void OnTest(TestNetworkEvent args, string foo)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixedCode = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+
+ [EventSubscription]
+ private void OnTest(TestNetworkEvent args, string foo)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier(code, fixedCode,
+ // /0/Test0.cs(10,9): info RA0058: Event subscription using SubscribeAllEvent can be converted to use EventSubscriptionAttribute
+ VerifyCS.Diagnostic().WithSpan(10, 9, 10, 52).WithArguments("SubscribeAllEvent", "EventSubscriptionAttribute")
+ );
+ }
+
+ [Test]
+ [Description("Tests that the conversion works correctly when the Initialize and event handler methods are declared in separate files (partial classes).")]
+ public async Task ConvertLocalEvent_WithPartials()
+ {
+ const string code1 = """
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnTest); // Comment here
+ }
+ }
+ """;
+
+ const string code2 = """
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ const string fixed1 = """
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ }
+ }
+ """;
+
+ const string fixed2 = """
+ using Robust.Shared.Analyzers;
+ using Robust.Shared.GameObjects;
+
+ public sealed partial class InitalizeBasedSystem : EntitySystem
+ {
+ [SubscribeLocalEvent]
+ private void OnTest(EntityUid uid, TestComponent comp, ref TestEvent args)
+ {
+ // Do something
+ }
+ }
+ """;
+
+ await Verifier([code1, code2], [fixed1, fixed2],
+ // /0/Test0.cs(9,9): info RA0057: Event subscription using SubscribeLocalEvent can be converted to use SubscribeLocalEventAttribute
+ VerifyCS.Diagnostic().WithSpan(9, 9, 9, 62).WithArguments("SubscribeLocalEvent", "SubscribeLocalEventAttribute")
+ );
+ }
+}
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