diff --git a/MSBuild/Robust.Engine.Version.props b/MSBuild/Robust.Engine.Version.props index eb4a9d76311..3f86b934a0f 100644 --- a/MSBuild/Robust.Engine.Version.props +++ b/MSBuild/Robust.Engine.Version.props @@ -1,4 +1,4 @@ - 202.1.1 + 204.0.0 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 5e57688372e..64e8362b8f7 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -54,6 +54,51 @@ END TEMPLATE--> *None yet* +## 204.0.0 + +### Breaking changes + +* Make EntityManager abstract and make IEntityManager.EntityNetManager not nullable. +* Make VVAccess.ReadWrite default for all Datafields instead of VVAccess.ReadOnly + +### New features + +* `TextEdit.OnTextChanged` +* Add Pick and PickAndTake versions for System.Random for ICollections. + +### Bugfixes + +* Fix `IClipboardManager.GetText()` returning null in some cases. +* Fix possible NRE in server-side console command completion code. +* Fix possible NRE on DebugConsole logs. +* Fix exception when VVing non-networked components. + +### Other + +* Remove "Do not use from content" from IComponent. + + +## 203.0.0 + +### Breaking changes + +* `IComponentFactory.RegisterIgnore()` no longer supports overwriting existing registrations, components should get ignored before they are registered. +* Event bus subscriptions are now locked after `IEntityManager` has started, instead of after the first component gets added. Any event subscriptions now need to happen before startup (but after init). +* Event bus subscriptions must now be locked before raising any events. +* Delete FodyWeavers.xsd as it hasn't been used for a long time. +* Remove physics sleep cancelling as it was, in hindsight, a bad idea. + +### New features + +* `RobustUnitTest` now has a `ExtraComponents` field for automatically registering additional components. +* `IComponentFactory.RegisterIgnore()` now accepts more than one string. +* Added `IComponentFactory.RegisterTypes` for simultaneously registering multiple components. + +### Bugfixes + +* Clamp volume calculations for audio rather than throwing. + + ## 202.1.1 ### Bugfixes @@ -61,7 +106,7 @@ END TEMPLATE--> * Reverted some map/grid initialisation changes that might've been causing broadphase/physics errors. * Fixed PVS sometimes sending entities without first sending their children. * Fixed a container state handling bug caused by containers not removing expected entities when shutting down. -* Fixed a `EnsureEntity` state handling bug caused by improper handling of entity deletions. +* Fixed a `EnsureEntity` state handling bug caused by improper handling of entity deletions. * Fixed a bad NetSyncEnabled debug assert. @@ -77,7 +122,7 @@ END TEMPLATE--> ### Breaking changes * Various entity manager methods now have a new `where T : IComponent` constraint. -* The `IComponentFactory.ComponentAdded` event has been renamed to `ComponentsAdded` and now provides an array of component registrations. +* The `IComponentFactory.ComponentAdded` event has been renamed to `ComponentsAdded` and now provides an array of component registrations. * `IComponentFactory.RegisterIgnore()` no longer supports overwriting existing registrations, components should get ignored before they are registered. ### New features diff --git a/Robust.Client/FodyWeavers.xsd b/Robust.Client/FodyWeavers.xsd deleted file mode 100644 index 15adf7906a8..00000000000 --- a/Robust.Client/FodyWeavers.xsd +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - Defines if sequence points should be generated for each emitted IL instruction. Default value: Debug - - - - - - Insert sequence points in Debug builds only (this is the default). - - - - - Insert sequence points in Release builds only. - - - - - Always insert sequence points. - - - - - Never insert sequence points. - - - - - - - - Defines how warnings should be handled. Default value: Warnings - - - - - - Emit build warnings (this is the default). - - - - - Do not emit warnings. - - - - - Treat warnings as errors. - - - - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs index 2ae9430a1ef..5aa9d69497e 100644 --- a/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs +++ b/Robust.Client/Graphics/Clyde/Windowing/Glfw.Windows.cs @@ -654,7 +654,7 @@ public Task ClipboardGetText(WindowReg mainWindow) private static void WinThreadGetClipboard(CmdGetClipboard cmd) { - var clipboard = GLFW.GetClipboardString((Window*) cmd.Window); + var clipboard = GLFW.GetClipboardString((Window*) cmd.Window) ?? ""; // Don't have to care about synchronization I don't think so just fire this immediately. cmd.Tcs.TrySetResult(clipboard); } diff --git a/Robust.Client/UserInterface/Controls/TextEdit.cs b/Robust.Client/UserInterface/Controls/TextEdit.cs index 30d60fce687..d22575698fa 100644 --- a/Robust.Client/UserInterface/Controls/TextEdit.cs +++ b/Robust.Client/UserInterface/Controls/TextEdit.cs @@ -90,6 +90,8 @@ public sealed class TextEdit : Control internal bool DebugOverlay; private Vector2? _lastDebugMousePos; + public event Action? OnTextChanged; + public TextEdit() { IoCManager.InjectDependencies(this); @@ -315,7 +317,7 @@ protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) if (changed) { _selectionStart = _cursorPosition; - // OnTextChanged?.Invoke(new LineEditEventArgs(this, _text)); + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); // _updatePseudoClass(); // OnBackspace?.Invoke(new LineEditBackspaceEventArgs(oldText, _text, cursor, selectStart)); } @@ -349,7 +351,7 @@ protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) if (changed) { _selectionStart = _cursorPosition; - // OnTextChanged?.Invoke(new LineEditEventArgs(this, _text)); + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); // _updatePseudoClass(); } @@ -382,7 +384,10 @@ protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) } if (changed) + { _selectionStart = _cursorPosition; + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); + } InvalidateHorizontalCursorPos(); args.Handle(); @@ -411,7 +416,10 @@ protected internal override void KeyBindDown(GUIBoundKeyEventArgs args) } if (changed) + { _selectionStart = _cursorPosition; + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); + } InvalidateHorizontalCursorPos(); args.Handle(); @@ -748,6 +756,7 @@ protected internal override void TextEditing(GUITextEditingEventArgs args) var startPos = _cursorPosition; TextRope = Rope.Insert(TextRope, startPos.Index, ev.Text); + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); _selectionStart = _cursorPosition = new CursorPos(startPos.Index + startChars, LineBreakBias.Top); _imeData = (startPos, ev.Text.Length); @@ -844,6 +853,7 @@ public void InsertAtCursor(string text) var upper = SelectionUpper.Index; TextRope = Rope.ReplaceSubstring(TextRope, lower, upper - lower, text); + OnTextChanged?.Invoke(new TextEditEventArgs(this, _textRope)); _selectionStart = _cursorPosition = new CursorPos(lower + text.Length, LineBreakBias.Top); // OnTextChanged?.Invoke(new LineEditEventArgs(this, _text)); @@ -1441,6 +1451,12 @@ protected internal override void KeyboardFocusExited() AbortIme(delete: false); } + public sealed class TextEditEventArgs(TextEdit control, Rope.Node textRope) : EventArgs + { + public TextEdit Control { get; } = control; + public Rope.Node TextRope { get; } = textRope; + } + /// /// Specifies which line the cursor is positioned at when on a word-wrapping break. /// diff --git a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs index 86fb7afb45c..59104cc0aca 100644 --- a/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs +++ b/Robust.Client/UserInterface/CustomControls/DebugConsole.xaml.cs @@ -39,7 +39,7 @@ public interface IDebugConsoleView // And also if Update() stops firing due to an exception loop the console will still work. // (At least from the main thread, which is what's throwing the exceptions..) [GenerateTypedNameReferences] - public sealed partial class DebugConsole : Control, IDebugConsoleView, IPostInjectInit + public sealed partial class DebugConsole : Control, IDebugConsoleView { [Dependency] private readonly IClientConsoleHost _consoleHost = default!; [Dependency] private readonly IResourceManager _resourceManager = default!; @@ -49,7 +49,7 @@ public sealed partial class DebugConsole : Control, IDebugConsoleView, IPostInje private static readonly ResPath HistoryPath = new("/debug_console_history.json"); private readonly ConcurrentQueue _messageQueue = new(); - private ISawmill _logger = default!; + private readonly ISawmill _logger; public DebugConsole() { @@ -57,6 +57,8 @@ public DebugConsole() IoCManager.InjectDependencies(this); + _logger = _logMan.GetSawmill("dbgconsole"); + InitCompletions(); CommandBar.OnTextChanged += OnCommandChanged; @@ -282,10 +284,5 @@ await Task.Run(async () => } }); } - - void IPostInjectInit.PostInject() - { - _logger = _logMan.GetSawmill("dbgconsole"); - } } } diff --git a/Robust.Server/Console/ServerConsoleHost.cs b/Robust.Server/Console/ServerConsoleHost.cs index b392fc5b2eb..1c1400ef806 100644 --- a/Robust.Server/Console/ServerConsoleHost.cs +++ b/Robust.Server/Console/ServerConsoleHost.cs @@ -273,6 +273,9 @@ private async void HandleConCompletions(MsgConCompletion message) } done: + + result ??= CompletionResult.Empty; + var msg = new MsgConCompletionResp { Result = result, diff --git a/Robust.Server/ViewVariables/ViewVariablesSession.cs b/Robust.Server/ViewVariables/ViewVariablesSession.cs index eeaa1c039bb..bbd10681e7b 100644 --- a/Robust.Server/ViewVariables/ViewVariablesSession.cs +++ b/Robust.Server/ViewVariables/ViewVariablesSession.cs @@ -97,7 +97,7 @@ public void Modify(object[] propertyIndex, object value) // Auto-dirty component. Only works when modifying a field that is directly on a component, // Does not work for nested objects. - if (Object is Component comp) + if (Object is Component { NetSyncEnabled: true } comp) EntityManager.Dirty(comp.Owner, comp); } diff --git a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs index 9be9d0bc823..4db97c30c09 100644 --- a/Robust.Shared/Audio/Systems/SharedAudioSystem.cs +++ b/Robust.Shared/Audio/Systems/SharedAudioSystem.cs @@ -154,7 +154,7 @@ public static float GainToVolume(float value) { if (value < 0f) { - throw new InvalidOperationException($"Tried to get volume calculation for gain of {value}."); + value = 0f; } return 10f * MathF.Log10(value); diff --git a/Robust.Shared/GameObjects/Component.cs b/Robust.Shared/GameObjects/Component.cs index c21445cdd38..cddbb8cea36 100644 --- a/Robust.Shared/GameObjects/Component.cs +++ b/Robust.Shared/GameObjects/Component.cs @@ -16,8 +16,13 @@ public abstract partial class Component : IComponent [ViewVariables(VVAccess.ReadWrite)] private bool _netSync { get; set; } = true; - [Obsolete("Do not use from content")] - public bool Networked { get; set; } = true; + internal bool Networked { get; set; } = true; + + bool IComponent.Networked + { + get => Networked; + set => Networked = value; + } /// public bool NetSyncEnabled @@ -31,9 +36,14 @@ public bool NetSyncEnabled [Obsolete("Update your API to allow accessing Owner through other means")] public EntityUid Owner { get; set; } = EntityUid.Invalid; - /// [ViewVariables] - public ComponentLifeStage LifeStage { get; [Obsolete("Do not use from content")] set; } = ComponentLifeStage.PreAdd; + public ComponentLifeStage LifeStage { get; internal set; } = ComponentLifeStage.PreAdd; + + ComponentLifeStage IComponent.LifeStage + { + get => LifeStage; + set => LifeStage = value; + } public virtual bool SendOnlyToOwner => false; @@ -51,13 +61,29 @@ public bool NetSyncEnabled [ViewVariables] public bool Deleted => LifeStage >= ComponentLifeStage.Removing; - /// + /// + /// This is the tick the component was created. + /// [ViewVariables] - public GameTick CreationTick { get; [Obsolete("Do not use from content")] set; } + public GameTick CreationTick { get; internal set; } - /// + GameTick IComponent.CreationTick + { + get => CreationTick; + set => CreationTick = value; + } + + /// + /// Marks the component as dirty so that the network will re-sync it with clients. + /// [ViewVariables] - public GameTick LastModifiedTick { get; [Obsolete("Do not use from content")] set; } + public GameTick LastModifiedTick { get; internal set; } + + GameTick IComponent.LastModifiedTick + { + get => LastModifiedTick; + set => LastModifiedTick = value; + } /// [Obsolete] @@ -69,15 +95,23 @@ public void Dirty(IEntityManager? entManager = null) // these two methods clear the LastModifiedTick/CreationTick to mark it as "not different from prototype load". // This is used as optimization in the game state system to avoid sending redundant component data. - [Obsolete("Do not use from content")] - public virtual void ClearTicks() + void IComponent.ClearTicks() + { + ClearTicks(); + } + + private protected virtual void ClearTicks() { LastModifiedTick = GameTick.Zero; ClearCreationTick(); } - [Obsolete("Do not use from content")] - public void ClearCreationTick() + void IComponent.ClearCreationTick() + { + ClearCreationTick(); + } + + private protected void ClearCreationTick() { CreationTick = GameTick.Zero; } diff --git a/Robust.Shared/GameObjects/ComponentFactory.cs b/Robust.Shared/GameObjects/ComponentFactory.cs index c81d7365230..d0437ada609 100644 --- a/Robust.Shared/GameObjects/ComponentFactory.cs +++ b/Robust.Shared/GameObjects/ComponentFactory.cs @@ -388,10 +388,10 @@ public bool TryGetRegistration(IComponent component, [NotNullWhen(true)] out Com public void DoAutoRegistrations() { var types = _reflectionManager.FindTypesWithAttribute().ToArray(); - RegisterClasses(types, false); + RegisterTypesInternal(types, false); } - private void RegisterClasses(Type[] types, bool overwrite) + private void RegisterTypesInternal(Type[] types, bool overwrite) { var names = _names.ToDictionary(); var lowerCaseNames = _lowerCaseNames.ToDictionary(); @@ -419,7 +419,19 @@ private void RegisterClasses(Type[] types, bool overwrite) public void RegisterClass(bool overwrite = false) where T : IComponent, new() { - RegisterClasses(new []{typeof(T)}, overwrite); + RegisterTypesInternal(new []{typeof(T)}, overwrite); + } + + /// + public void RegisterTypes(params Type[] types) + { + foreach (var type in types) + { + if (!type.IsAssignableTo(typeof(IComponent)) || !type.HasParameterlessConstructor()) + throw new InvalidOperationException($"Invalid type: {type}"); + } + + RegisterTypesInternal(types, false); } public IEnumerable GetAllRefTypes() diff --git a/Robust.Shared/GameObjects/Components/MetaDataComponent.cs b/Robust.Shared/GameObjects/Components/MetaDataComponent.cs index a19cf7221d1..8ebf986bca7 100644 --- a/Robust.Shared/GameObjects/Components/MetaDataComponent.cs +++ b/Robust.Shared/GameObjects/Components/MetaDataComponent.cs @@ -192,8 +192,7 @@ internal set [ViewVariables] internal PvsChunkLocation? LastPvsLocation; - [Obsolete("Do not use from content")] - public override void ClearTicks() + private protected override void ClearTicks() { // Do not clear modified ticks. // MetaDataComponent is used in the game state system to carry initial data like prototype ID. diff --git a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs index 837df689745..736209cd9e2 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Broadcast.cs @@ -297,7 +297,10 @@ public void QueueEvent(EventSource source, EntityEventArgs toRaise) private void UnsubscribeEvent(Type eventType, BroadcastRegistration tuple, IEntityEventSubscriber subscriber) { - if (_eventData.TryGetValue(eventType, out var subscriptions) + if (_subscriptionLock) + throw new InvalidOperationException("Subscription locked."); + + if (_eventDataUnfrozen.TryGetValue(eventType, out var subscriptions) && subscriptions.BroadcastRegistrations.Contains(tuple)) subscriptions.BroadcastRegistrations.Remove(tuple); @@ -307,7 +310,7 @@ private void UnsubscribeEvent(Type eventType, BroadcastRegistration tuple, IEnti private void ProcessSingleEvent(EventSource source, ref Unit unitRef, Type eventType) { - if (!_eventData.TryGetValue(eventType, out var subs)) + if (!_eventData!.TryGetValue(eventType, out var subs)) return; if (subs.IsOrdered && !subs.OrderingUpToDate) diff --git a/Robust.Shared/GameObjects/EntityEventBus.Common.cs b/Robust.Shared/GameObjects/EntityEventBus.Common.cs index 0e43936e4f6..c251e42fa59 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Common.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Common.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -14,7 +15,8 @@ internal sealed partial class EntityEventBus : IEventBus private IComponentFactory _comFac; // Data on individual events. Used to check ordering info and fire broadcast events. - private readonly Dictionary _eventData = new(); + private FrozenDictionary _eventData = FrozenDictionary.Empty; + private readonly Dictionary _eventDataUnfrozen = new(); // Inverse subscriptions to be able to unsubscribe an IEntityEventSubscriber. private readonly Dictionary> _inverseEventSubscriptions @@ -28,10 +30,18 @@ private readonly Dictionary _entEventTables = new(); // CompType -> EventType -> Handler - internal Dictionary?[] _entSubscriptions = + internal FrozenDictionary?[] _entSubscriptions = default!; + + // Variant of _entSubscriptions that omits any events with the ComponentEventAttribute + internal FrozenDictionary?[] _entSubscriptionsNoCompEv = default!; + + // pre-freeze _entSubscriptions data + internal Dictionary?[] _entSubscriptionsUnfrozen = Array.Empty?>(); // EventType -> { CompType1, ... CompType N } + // Only required to sort ordered subscriptions, which only happens during initialization + // so doesn't need to be a frozen dictionary. private Dictionary> _entSubscriptionsInv = new(); // prevents shitcode, get your subscriptions figured out before you start spawning entities @@ -52,7 +62,10 @@ private static ref Unit ExtractUnitRef(ref object obj, Type objType) private void RegisterCommon(Type eventType, OrderingData? data, out EventData subs) { - subs = _eventData.GetOrNew(eventType); + if (_subscriptionLock) + throw new InvalidOperationException("Subscription locked."); + + subs = _eventDataUnfrozen.GetOrNew(eventType); if (data == null) return; diff --git a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs index fe38bca7d39..d8ccd62431b 100644 --- a/Robust.Shared/GameObjects/EntityEventBus.Directed.cs +++ b/Robust.Shared/GameObjects/EntityEventBus.Directed.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Robust.Shared.Collections; @@ -122,16 +124,7 @@ public EntityEventBus(IEntityManager entMan) // Dynamic handling of components is only for RobustUnitTest compatibility spaghetti. _comFac.ComponentsAdded += ComFacOnComponentsAdded; - - InitEntSubscriptionsArray(); - } - - private void InitEntSubscriptionsArray() - { - foreach (var reg in _comFac.GetAllRegistrations()) - { - CompIdx.AssignArray(ref _entSubscriptions, reg.Idx, new Dictionary()); - } + ComFacOnComponentsAdded(_comFac.GetAllRegistrations().ToArray()); } /// @@ -329,9 +322,12 @@ public void UnsubscribeLocalEvent() private void ComFacOnComponentsAdded(ComponentRegistration[] regs) { + if (_subscriptionLock) + throw new InvalidOperationException("Subscription locked."); + foreach (var reg in regs) { - CompIdx.RefArray(ref _entSubscriptions, reg.Idx) ??= new Dictionary(); + CompIdx.RefArray(ref _entSubscriptionsUnfrozen, reg.Idx) ??= new(); } } @@ -346,10 +342,43 @@ public void OnEntityDeleted(EntityUid e) } public void OnComponentAdded(in AddedComponentEventArgs e) + { + EntAddComponent(e.BaseArgs.Owner, e.ComponentType.Idx); + } + + internal void LockSubscriptions() { _subscriptionLock = true; + _eventData = _eventDataUnfrozen.ToFrozenDictionary(); - EntAddComponent(e.BaseArgs.Owner, e.ComponentType.Idx); + _entSubscriptions = _entSubscriptionsUnfrozen + .Select(x => x?.ToFrozenDictionary()) + .ToArray(); + + _entSubscriptionsNoCompEv = _entSubscriptionsUnfrozen.Select(FreezeWithoutComponentEvent).ToArray(); + + CalcOrdering(); + } + + /// + /// Freezes a dictionary while committing events with the . + /// This avoids unnecessarily adding one-off events to the list of subscriptions. + /// + private FrozenDictionary? FreezeWithoutComponentEvent( + Dictionary? input) + { + if (input == null) + return null; + + return input.Where(x => !IsComponentEvent(x.Key)) + .ToFrozenDictionary(); + } + + private bool IsComponentEvent(Type t) + { + var isCompEv = _eventData[t].ComponentEvent; + DebugTools.Assert(isCompEv == t.HasCustomAttribute()); + return isCompEv; } public void OnComponentRemoved(in RemovedComponentEventArgs e) @@ -366,7 +395,8 @@ private void EntAddSubscription( if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); - if (compType.Value >= _entSubscriptions.Length || _entSubscriptions[compType.Value] is not { } compSubs) + if (compType.Value >= _entSubscriptionsUnfrozen.Length + || _entSubscriptionsUnfrozen[compType.Value] is not { } compSubs) { if (IgnoreUnregisteredComponents) return; @@ -375,13 +405,13 @@ private void EntAddSubscription( } if (compSubs.ContainsKey(eventType)) + { throw new InvalidOperationException( $"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}"); + } compSubs.Add(eventType, registration); - - var invSubs = _entSubscriptionsInv.GetOrNew(eventType); - invSubs.Add(compType); + _entSubscriptionsInv.GetOrNew(eventType).Add(compType); RegisterCommon(eventType, registration.Ordering, out var data); data.ComponentEvent = eventType.HasCustomAttribute(); @@ -408,7 +438,8 @@ private void EntUnsubscribe(CompIdx compType, Type eventType) if (_subscriptionLock) throw new InvalidOperationException("Subscription locked."); - if (compType.Value >= _entSubscriptions.Length || _entSubscriptions[compType.Value] is not { } compSubs) + if (compType.Value >= _entSubscriptionsUnfrozen.Length + || _entSubscriptionsUnfrozen[compType.Value] is not { } compSubs) { if (IgnoreUnregisteredComponents) return; @@ -435,14 +466,14 @@ private void EntRemoveEntity(EntityUid euid) private void EntAddComponent(EntityUid euid, CompIdx compType) { + DebugTools.Assert(_subscriptionLock); + var eventTable = _entEventTables[euid]; - var compSubs = _entSubscriptions[compType.Value]!; + var compSubs = _entSubscriptionsNoCompEv[compType.Value]!; foreach (var evType in compSubs.Keys) { - // Skip adding this to significantly reduce memory use and GC noise on entity create. - if (_eventData[evType].ComponentEvent) - continue; + DebugTools.Assert(!_eventData[evType].ComponentEvent); if (eventTable.Free < 0) GrowEventTable(eventTable); @@ -607,17 +638,16 @@ private bool EntTryGetSubscriptions(Type eventType, EntityUid euid, out Subscrip return true; } - private void EntClear() + public void ClearSubscriptions() { - _entEventTables = new(); _subscriptionLock = false; - } - - public void ClearEventTables() - { - EntClear(); - - foreach (var sub in _entSubscriptions) + _eventDataUnfrozen.Clear(); + _entEventTables.Clear(); + _inverseEventSubscriptions.Clear(); + _entSubscriptions = default!; + _entSubscriptionsNoCompEv = default!; + _eventData = FrozenDictionary.Empty; + foreach (var sub in _entSubscriptionsUnfrozen) { sub?.Clear(); } @@ -632,6 +662,8 @@ public void Dispose() _comFac = null!; _entEventTables = null!; _entSubscriptions = null!; + _entSubscriptionsNoCompEv = null!; + _entSubscriptionsUnfrozen = null!; _entSubscriptionsInv = null!; } @@ -639,7 +671,7 @@ private struct SubscriptionsEnumerator { private readonly Type _eventType; private readonly EntityUid _uid; - private readonly Dictionary?[] _subscriptions; + private readonly FrozenDictionary?[] _subscriptions; private readonly IEntityManager _entityManager; private readonly EventTableListEntry[] _list; private int _idx; @@ -648,7 +680,7 @@ public SubscriptionsEnumerator( Type eventType, int startEntry, EventTableListEntry[] list, - Dictionary?[] subscriptions, + FrozenDictionary?[] subscriptions, EntityUid uid, IEntityManager entityManager) { diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 02c4ec88d3e..f903aa7c8f8 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -26,7 +26,7 @@ namespace Robust.Shared.GameObjects /// [Virtual] - public partial class EntityManager : IEntityManager + public abstract partial class EntityManager : IEntityManager { #region Dependencies @@ -61,7 +61,7 @@ public partial class EntityManager : IEntityManager public IEntitySystemManager EntitySysManager => _entitySystemManager; /// - public virtual IEntityNetworkManager? EntityNetManager => null; + public abstract IEntityNetworkManager EntityNetManager { get; } protected readonly Queue QueuedDeletions = new(); protected readonly HashSet QueuedDeletionsSet = new(); @@ -203,7 +203,7 @@ public virtual void Startup() // TODO: Probably better to call this on its own given it's so infrequent. _entitySystemManager.Initialize(); Started = true; - _eventBus.CalcOrdering(); + _eventBus.LockSubscriptions(); _mapSystem = System(); _xforms = System(); _containers = System(); @@ -216,7 +216,7 @@ public virtual void Shutdown() { ShuttingDown = true; FlushEntities(); - _eventBus.ClearEventTables(); + _eventBus.ClearSubscriptions(); _entitySystemManager.Shutdown(); ClearComponents(); ShuttingDown = false; diff --git a/Robust.Shared/GameObjects/IComponent.cs b/Robust.Shared/GameObjects/IComponent.cs index aab4e33894e..c3999910f6c 100644 --- a/Robust.Shared/GameObjects/IComponent.cs +++ b/Robust.Shared/GameObjects/IComponent.cs @@ -16,10 +16,9 @@ public partial interface IComponent /// The current lifetime stage of this component. You can use this to check /// if the component is initialized or being deleted. /// - ComponentLifeStage LifeStage { get; [Obsolete("Do not use from content")] set; } + ComponentLifeStage LifeStage { get; internal set; } - [Obsolete("Do not use from content")] - bool Networked { get; set; } + internal bool Networked { get; set; } /// /// Whether this component should be synchronized with clients when modified. @@ -68,22 +67,21 @@ public partial interface IComponent /// /// Marks the component as dirty so that the network will re-sync it with clients. /// + [Obsolete] void Dirty(IEntityManager? entManager = null); /// /// This is the tick the component was created. /// - GameTick CreationTick { get; set; } + GameTick CreationTick { get; internal set; } /// /// This is the last game tick Dirty() was called. /// - GameTick LastModifiedTick { get; set; } + GameTick LastModifiedTick { get; internal set; } - [Obsolete("Do not use from content")] - void ClearTicks(); + internal void ClearTicks(); - [Obsolete("Do not use from content")] - void ClearCreationTick(); + internal void ClearCreationTick(); } } diff --git a/Robust.Shared/GameObjects/IComponentFactory.cs b/Robust.Shared/GameObjects/IComponentFactory.cs index f98d0f2898c..b7ade646198 100644 --- a/Robust.Shared/GameObjects/IComponentFactory.cs +++ b/Robust.Shared/GameObjects/IComponentFactory.cs @@ -83,6 +83,11 @@ public interface IComponentFactory /// If the component already exists, will this replace it? void RegisterClass(bool overwrite = false) where T : IComponent, new(); + /// + /// Registers component types with the factory. + /// + void RegisterTypes(params Type[] type); + /// /// Registers a component name as being ignored. /// diff --git a/Robust.Shared/GameObjects/IEntityManager.cs b/Robust.Shared/GameObjects/IEntityManager.cs index 7884a4fac53..fc052dc8bfc 100644 --- a/Robust.Shared/GameObjects/IEntityManager.cs +++ b/Robust.Shared/GameObjects/IEntityManager.cs @@ -46,7 +46,7 @@ public partial interface IEntityManager IComponentFactory ComponentFactory { get; } IEntitySystemManager EntitySysManager { get; } - IEntityNetworkManager? EntityNetManager { get; } + IEntityNetworkManager EntityNetManager { get; } IEventBus EventBus { get; } #region Entity Management diff --git a/Robust.Shared/Network/Messages/MsgConCompletionResp.cs b/Robust.Shared/Network/Messages/MsgConCompletionResp.cs index 3a64364c7c7..68bea41274e 100644 --- a/Robust.Shared/Network/Messages/MsgConCompletionResp.cs +++ b/Robust.Shared/Network/Messages/MsgConCompletionResp.cs @@ -4,14 +4,12 @@ namespace Robust.Shared.Network.Messages; -#nullable disable - public sealed class MsgConCompletionResp : NetMessage { public override MsgGroups MsgGroup => MsgGroups.Command; public int Seq { get; set; } - public CompletionResult Result { get; set; } + public CompletionResult Result { get; set; } = default!; public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { diff --git a/Robust.Shared/Physics/PhysicsWakeEvent.cs b/Robust.Shared/Physics/PhysicsWakeEvent.cs index 549d7d7c66a..145e82776ae 100644 --- a/Robust.Shared/Physics/PhysicsWakeEvent.cs +++ b/Robust.Shared/Physics/PhysicsWakeEvent.cs @@ -7,11 +7,4 @@ namespace Robust.Shared.Physics; public readonly record struct PhysicsWakeEvent(EntityUid Entity, PhysicsComponent Body); [ByRefEvent] -public record struct PhysicsSleepEvent(EntityUid Entity, PhysicsComponent Body) -{ - /// - /// Marks the entity as still being awake and cancels sleeping. - /// This only works if is still enabled and the object is not a static object.. - /// - public bool Cancelled; -}; +public record struct PhysicsSleepEvent(EntityUid Entity, PhysicsComponent Body); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index 4f4a8cc6ee1..303d9796b2d 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -401,29 +401,14 @@ public void SetAwake(Entity ent, bool value, bool updateSleepT } else { - // TODO C# event? var ev = new PhysicsSleepEvent(uid, body); RaiseLocalEvent(uid, ref ev, true); - - // Reset the sleep timer. - if (ev.Cancelled && canWake) - { - body.Awake = true; - // TODO C# event? - var wakeEv = new PhysicsWakeEvent(uid, body); - RaiseLocalEvent(uid, ref wakeEv, true); - - if (updateSleepTime) - SetSleepTime(body, 0); - - return; - } - ResetDynamics(body, dirty: false); } - // Update wake system after we are sure that the wake/sleep event wasn't cancelled. - _wakeSystem.UpdateCanCollide(ent, checkTerminating: false, dirty: false); + // Update wake system last, if sleeping but still colliding. + if (!value && body.CanCollide) + _wakeSystem.UpdateCanCollide(ent, checkTerminating: false, dirty: false); if (updateSleepTime) SetSleepTime(body, 0); diff --git a/Robust.Shared/Random/IRobustRandom.cs b/Robust.Shared/Random/IRobustRandom.cs index 4c50b929233..b7a0e2bdbdd 100644 --- a/Robust.Shared/Random/IRobustRandom.cs +++ b/Robust.Shared/Random/IRobustRandom.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; using Robust.Shared.Collections; using Robust.Shared.Maths; +using Robust.Shared.Toolshed.Commands.Generic; namespace Robust.Shared.Random; diff --git a/Robust.Shared/Random/RandomExtensions.cs b/Robust.Shared/Random/RandomExtensions.cs index 1ca88e1bea0..891105128ae 100644 --- a/Robust.Shared/Random/RandomExtensions.cs +++ b/Robust.Shared/Random/RandomExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Robust.Shared.Collections; using Robust.Shared.Maths; using Robust.Shared.Utility; @@ -47,7 +48,7 @@ public static T Pick(this IRobustRandom random, IReadOnlyCollection collec } } - throw new InvalidOperationException("This should be unreachable!"); + throw new UnreachableException("This should be unreachable!"); } public static T PickAndTake(this IRobustRandom random, IList list) @@ -58,6 +59,36 @@ public static T PickAndTake(this IRobustRandom random, IList list) return element; } + /// + /// Picks a random element from a set and returns it. + /// This is O(n) as it has to iterate the collection until the target index. + /// + public static T Pick(this System.Random random, ICollection collection) + { + var index = random.Next(collection.Count); + var i = 0; + foreach (var t in collection) + { + if (i++ == index) + { + return t; + } + } + + throw new UnreachableException("This should be unreachable!"); + } + + /// + /// Picks a random from a collection then removes it and returns it. + /// This is O(n) as it has to iterate the collection until the target index. + /// + public static T PickAndTake(this System.Random random, ICollection set) + { + var tile = Pick(random, set); + set.Remove(tile); + return tile; + } + /// /// Generate a random number from a normal (gaussian) distribution. /// diff --git a/Robust.Shared/ViewVariables/ViewVariablesUtility.cs b/Robust.Shared/ViewVariables/ViewVariablesUtility.cs index 65071f44a29..827bb990d0c 100644 --- a/Robust.Shared/ViewVariables/ViewVariablesUtility.cs +++ b/Robust.Shared/ViewVariables/ViewVariablesUtility.cs @@ -40,7 +40,7 @@ public static bool TryGetViewVariablesAccess(MemberInfo info, [NotNullWhen(true) if (info.HasCustomAttribute() || info.HasCustomAttribute()) { - access = VVAccess.ReadOnly; + access = VVAccess.ReadWrite; return true; } diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index eda739c47d5..0cacf50afee 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; using NUnit.Framework; using Robust.Client.ComponentTrees; @@ -39,6 +38,33 @@ public enum UnitTestProject : byte [Parallelizable] public abstract partial class RobustUnitTest { + protected virtual Type[]? ExtraComponents => null; + private static Type[] _components = new [] + { + typeof(EyeComponent), + typeof(MapComponent), + typeof(MapGridComponent), + typeof(ContainerManagerComponent), + typeof(MetaDataComponent), + typeof(TransformComponent), + typeof(PhysicsComponent), + typeof(PhysicsMapComponent), + typeof(BroadphaseComponent), + typeof(FixturesComponent), + typeof(JointComponent), + typeof(GridTreeComponent), + typeof(MovedGridsComponent), + typeof(JointRelayTargetComponent), + typeof(OccluderComponent), + typeof(OccluderTreeComponent), + typeof(SpriteTreeComponent), + typeof(LightTreeComponent), + typeof(CollisionWakeComponent), + typeof(CollideOnAnchorComponent), + typeof(Gravity2DComponent), + typeof(ActorComponent) + }; + public virtual UnitTestProject Project => UnitTestProject.Server; [OneTimeSetUp] @@ -122,6 +148,7 @@ public void BaseSetup() systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); @@ -133,117 +160,11 @@ public void BaseSetup() // Required components for the engine to work // Why are we still here? Just to suffer? Why can't we just use [RegisterComponent] magic? // TODO End Suffering. + // suffering has been alleviated, but still present var compFactory = deps.Resolve(); - - if (!compFactory.AllRegisteredTypes.Contains(typeof(EyeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(MapComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(MapGridComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(ContainerManagerComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(MetaDataComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(TransformComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(PhysicsComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(PhysicsMapComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(BroadphaseComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(FixturesComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(JointComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(GridTreeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(MovedGridsComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(JointRelayTargetComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(OccluderTreeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(SpriteTreeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(LightTreeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(Gravity2DComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(CollisionWakeComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(CollideOnAnchorComponent))) - { - compFactory.RegisterClass(); - } - - if (!compFactory.AllRegisteredTypes.Contains(typeof(ActorComponent))) - { - compFactory.RegisterClass(); - } + compFactory.RegisterTypes(_components); + if (ExtraComponents != null) + compFactory.RegisterTypes(ExtraComponents); deps.Resolve().Initialize(); @@ -252,7 +173,7 @@ public void BaseSetup() entMan.Initialize(); // RobustUnitTest is complete hot garbage. // This makes EventTables ignore *all* the screwed up component abuse it causes. - entMan.EventBus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); + entMan.EventBus.OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare(); // The nightmare never ends mapMan.Initialize(); systems.Initialize(); diff --git a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs index 6a22e5271c8..515e4709235 100644 --- a/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs +++ b/Robust.UnitTesting/Server/Maps/MapLoaderTest.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using NUnit.Framework; using Robust.Server.GameObjects; @@ -52,24 +53,14 @@ public sealed partial class MapLoaderTest : RobustUnitTest - type: MapDeserializeTest foo: 1 bar: 2 - "; + protected override Type[]? ExtraComponents => new[] { typeof(MapDeserializeTestComponent), typeof(VisibilityComponent), typeof(IgnoreUIRangeComponent)}; + [OneTimeSetUp] public void Setup() { - var compFactory = IoCManager.Resolve(); - compFactory.RegisterClass(); - compFactory.RegisterClass(); - compFactory.RegisterClass(); - compFactory.GenerateNetIds(); IoCManager.Resolve().Initialize(); - - // For some reason RobustUnitTest doesn't discover PVSSystem but this does here so ? - var syssy = IoCManager.Resolve(); - syssy.Shutdown(); - syssy.Initialize(); - var resourceManager = IoCManager.Resolve(); resourceManager.Initialize(null); resourceManager.MountString("/TestMap.yml", MapData); diff --git a/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs b/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs index 7636cab7bdd..6097e79c05f 100644 --- a/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/ComponentFactory_Tests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -13,12 +14,7 @@ public sealed partial class ComponentFactory_Tests : RobustUnitTest private const string TestComponentName = "A"; private const string LowercaseTestComponentName = "a"; private const string NonexistentComponentName = "B"; - - [OneTimeSetUp] - public void OneTimeSetUp() - { - IoCManager.Resolve().RegisterClass(); - } + protected override Type[]? ExtraComponents => new[] {typeof(TestComponent)}; [Test] public void GetComponentAvailabilityTest() diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs index 406022670d4..ff42c0e1022 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.ComponentEvent.cs @@ -41,6 +41,7 @@ public void SubscribeCompEvent() // Subscribe int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); + bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); @@ -98,6 +99,7 @@ public void UnsubscribeCompEvent() int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); bus.UnsubscribeLocalEvent(); + bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); @@ -153,6 +155,7 @@ public void SubscribeCompLifeEvent() // Subscribe int calledCount = 0; bus.SubscribeLocalEvent(HandleTestEvent); + bus.LockSubscriptions(); // add a component to the system entManMock.Raise(m => m.EntityAdded += null, entUid); @@ -232,6 +235,7 @@ void HandlerB(EntityUid uid, Component comp, TestEvent ev) bus.SubscribeLocalEvent(HandlerA, typeof(OrderAComponent), before: new []{typeof(OrderBComponent), typeof(OrderCComponent)}); bus.SubscribeLocalEvent(HandlerB, typeof(OrderBComponent), after: new []{typeof(OrderCComponent)}); bus.SubscribeLocalEvent(HandlerC, typeof(OrderCComponent)); + bus.LockSubscriptions(); // add a component to the system bus.OnEntityAdded(entUid); diff --git a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs index da734c1f55f..44bb208ecc7 100644 --- a/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs +++ b/Robust.UnitTesting/Shared/GameObjects/EntityEventBusTests.SystemEvent.cs @@ -137,6 +137,7 @@ public void SubscribeEvent_MultipleSubscriptions_IndividuallyCalled() bus.SubscribeEvent(EventSource.Local, subscriber, ev => delFooCount++); bus.SubscribeEvent(EventSource.Local, subscriber, ev => delBarCount++); + bus.LockSubscriptions(); // Act & Assert bus.RaiseEvent(EventSource.Local, new TestEventArgs()); @@ -184,6 +185,7 @@ void Handler(TestEventArgs ev) { } bus.SubscribeEvent(EventSource.Local, subscriber, Handler); bus.UnsubscribeEvent(EventSource.Local, subscriber); + bus.LockSubscriptions(); // Act bus.UnsubscribeEvent(EventSource.Local, subscriber); @@ -253,6 +255,7 @@ public void RaiseEvent_NoSubscriptions_Nop() int delCalledCount = 0; bus.SubscribeEvent(EventSource.Local, subscriber, ev => delCalledCount++); + bus.LockSubscriptions(); // Act bus.RaiseEvent(EventSource.Local, new TestEventArgs()); @@ -276,6 +279,7 @@ public void RaiseEvent_Unsubscribed_Nop() bus.SubscribeEvent(EventSource.Local, subscriber, Handler); bus.UnsubscribeEvent(EventSource.Local, subscriber); + bus.LockSubscriptions(); // Act bus.RaiseEvent(EventSource.Local, new TestEventArgs()); @@ -348,6 +352,7 @@ public void UnsubscribeEvents_UnsubscribedHandler_Nop() bus.SubscribeEvent(EventSource.Local, subscriber, Handler); bus.UnsubscribeEvents(subscriber); + bus.LockSubscriptions(); // Act bus.RaiseEvent(EventSource.Local, new TestEventArgs()); @@ -425,6 +430,7 @@ public void ProcessQueue_EventQueued_HandlerRaised() void Handler(TestEventArgs ev) => delCallCount++; bus.SubscribeEvent(EventSource.Local, subscriber, Handler); + bus.LockSubscriptions(); bus.QueueEvent(EventSource.Local, new TestEventArgs()); // Act @@ -464,6 +470,7 @@ void HandlerB(TestEventArgs ev) bus.SubscribeEvent(EventSource.Local, new SubA(), HandlerA, typeof(SubA), before: new []{typeof(SubB), typeof(SubC)}); bus.SubscribeEvent(EventSource.Local, new SubB(), HandlerB, typeof(SubB), after: new []{typeof(SubC)}); bus.SubscribeEvent(EventSource.Local, new SubC(), HandlerC, typeof(SubC)); + bus.LockSubscriptions(); // Act bus.RaiseEvent(EventSource.Local, new TestEventArgs()); diff --git a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs index 3f24e68937e..fbe135b63a3 100644 --- a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; @@ -33,6 +34,7 @@ private static (ISimulation, EntityUid gridId) SimulationFactory() { var sim = RobustServerSimulation .NewSimulation() + .RegisterEntitySystems(f => f.LoadExtraSystemType()) .RegisterPrototypes(f=> { f.LoadString(Prototypes); @@ -69,7 +71,6 @@ public void OnAnchored_WorldPosition_TileCenter() var (sim, gridId) = SimulationFactory(); var entMan = sim.Resolve(); var mapMan = sim.Resolve(); - var xform = entMan.System(); var coordinates = new MapCoordinates(new Vector2(7, 7), TestMapId); @@ -77,20 +78,13 @@ public void OnAnchored_WorldPosition_TileCenter() var grid = mapMan.GetGrid(gridId); grid.SetTile(grid.TileIndicesFor(coordinates), new Tile(1)); - int calledCount = 0; var ent1 = entMan.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after - xform.OnGlobalMoveEvent += MoveEventHandler; // Act + entMan.System().ResetCounters(); entMan.GetComponent(ent1).Anchored = true; - Assert.That(entMan.GetComponent(ent1).WorldPosition, Is.EqualTo(new Vector2(7.5f, 7.5f))); // centered on tile - Assert.That(calledCount, Is.EqualTo(1)); // because the ent was moved from snapping, a MoveEvent was raised. - xform.OnGlobalMoveEvent -= MoveEventHandler; - void MoveEventHandler(ref MoveEvent ev) - { - calledCount++; - } + entMan.System().AssertMoved(false); } [ComponentProtoName("AnchorOnInit")] @@ -105,6 +99,55 @@ public override void Initialize() } } + internal sealed class MoveEventTestSystem : EntitySystem + { + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + _transform.OnGlobalMoveEvent += OnMove; + SubscribeLocalEvent(OnReparent); + } + + + public override void Shutdown() + { + base.Shutdown(); + _transform.OnGlobalMoveEvent -= OnMove; + } + + public bool FailOnMove = false; + public int MoveCounter = 0; + public int ParentCounter = 0; + + private void OnMove(ref MoveEvent ev) + { + MoveCounter++; + if (FailOnMove) + Assert.Fail($"Move event was raised"); + } + private void OnReparent(ref EntParentChangedMessage ev) + { + ParentCounter++; + if (FailOnMove) + Assert.Fail($"Move event was raised"); + } + + public void ResetCounters() + { + ParentCounter = 0; + MoveCounter = 0; + } + + public void AssertMoved(bool parentChanged = true) + { + if (parentChanged) + Assert.That(ParentCounter, Is.EqualTo(1)); + Assert.That(MoveCounter, Is.EqualTo(1)); + } + } + /// /// Ensures that if an entity gets added to lookups when anchored during init by some system. /// @@ -159,23 +202,13 @@ public void OnAnchored_Parent_SetToGrid() var traversal = entMan.System(); traversal.Enabled = false; - - var subscriber = new Subscriber(); - int calledCount = 0; var ent1 = entMan.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after - entMan.EventBus.SubscribeEvent(EventSource.Local, subscriber, ParentChangedHandler); // Act + entMan.System().ResetCounters(); entMan.GetComponent(ent1).Anchored = true; - Assert.That(entMan.GetComponent(ent1).ParentUid, Is.EqualTo(grid.Owner)); - Assert.That(calledCount, Is.EqualTo(1)); - void ParentChangedHandler(ref EntParentChangedMessage ev) - { - Assert.That(ev.Entity, Is.EqualTo(ent1)); - calledCount++; - - } + entMan.System().AssertMoved(); traversal.Enabled = true; } @@ -247,23 +280,16 @@ public void Anchored_SetPosition_Nop() var grid = mapMan.GetGrid(gridId); grid.SetTile(grid.TileIndicesFor(coordinates), new Tile(1)); - int calledCount = 0; var ent1 = entMan.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after entMan.GetComponent(ent1).Anchored = true; // Anchoring will change parent if needed, raising MoveEvent, subscribe after - xform.OnGlobalMoveEvent += MoveEventHandler; + entMan.System().FailOnMove = true; // Act entMan.GetComponent(ent1).WorldPosition = new Vector2(99, 99); entMan.GetComponent(ent1).LocalPosition = new Vector2(99, 99); Assert.That(entMan.GetComponent(ent1).MapPosition, Is.EqualTo(coordinates)); - Assert.That(calledCount, Is.EqualTo(0)); - xform.OnGlobalMoveEvent -= MoveEventHandler; - void MoveEventHandler(ref MoveEvent ev) - { - Assert.Fail("MoveEvent raised when entity is anchored."); - calledCount++; - } + entMan.System().FailOnMove = false; } /// @@ -517,22 +543,13 @@ public void Unanchored_Unanchor_Nop() var traversal = entMan.System(); traversal.Enabled = false; - - var subscriber = new Subscriber(); - int calledCount = 0; var ent1 = entMan.SpawnEntity(null, coordinates); // this raises MoveEvent, subscribe after - entMan.EventBus.SubscribeEvent(EventSource.Local, subscriber, ParentChangedHandler); // Act + entMan.System().FailOnMove = true; entMan.GetComponent(ent1).Anchored = false; - Assert.That(entMan.GetComponent(ent1).ParentUid, Is.EqualTo(mapMan.GetMapEntityId(TestMapId))); - Assert.That(calledCount, Is.EqualTo(0)); - void ParentChangedHandler(ref EntParentChangedMessage ev) - { - Assert.That(ev.Entity, Is.EqualTo(ent1)); - calledCount++; - } + entMan.System().FailOnMove = false; traversal.Enabled = true; } diff --git a/Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs b/Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs index e4cc58012b3..8af756effe8 100644 --- a/Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/Systems/TransformSystemTests.cs @@ -3,7 +3,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Maths; using Robust.UnitTesting.Server; namespace Robust.UnitTesting.Shared.GameObjects.Systems @@ -15,6 +14,7 @@ private static ISimulation SimulationFactory() { var sim = RobustServerSimulation .NewSimulation() + .RegisterEntitySystems(f => f.LoadExtraSystemType()) .InitializeInstance(); // Adds the map with id 1, and spawns entity 1 as the map entity. @@ -31,22 +31,11 @@ public void OnMove_LocalPosChanged_RaiseMoveEvent() { var sim = SimulationFactory(); var entMan = sim.Resolve(); - var xform = entMan.System(); - - int calledCount = 0; - xform.OnGlobalMoveEvent += MoveEventHandler; var ent1 = entMan.SpawnEntity(null, new MapCoordinates(Vector2.Zero, new MapId(1))); + entMan.System().ResetCounters(); IoCManager.Resolve().GetComponent(ent1).LocalPosition = Vector2.One; - - Assert.That(calledCount, Is.EqualTo(1)); - xform.OnGlobalMoveEvent -= MoveEventHandler; - void MoveEventHandler(ref MoveEvent ev) - { - calledCount++; - Assert.That(ev.OldPosition, Is.EqualTo(new EntityCoordinates(EntityUid.FirstUid, Vector2.Zero))); - Assert.That(ev.NewPosition, Is.EqualTo(new EntityCoordinates(EntityUid.FirstUid, Vector2.One))); - } + entMan.System().AssertMoved(false); } /// diff --git a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs index 8df563bcffc..e8bd66296dd 100644 --- a/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs +++ b/Robust.UnitTesting/Shared/Localization/LocalizationTests.cs @@ -16,15 +16,12 @@ namespace Robust.UnitTesting.Shared.Localization [TestFixture] internal sealed class LocalizationTests : RobustUnitTest { + protected override Type[] ExtraComponents => new[] {typeof(GrammarComponent)}; [OneTimeSetUp] public void Setup() { IoCManager.Resolve().Initialize(); - var componentFactory = IoCManager.Resolve(); - componentFactory.RegisterClass(); - componentFactory.GenerateNetIds(); - var res = IoCManager.Resolve(); res.MountString("/Locale/en-US/a.ftl", FluentCode); res.MountString("/EnginePrototypes/a.yml", YAMLCode); diff --git a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs b/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs index 0316c415b21..481879b02f5 100644 --- a/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs +++ b/Robust.UnitTesting/Shared/Prototypes/HotReloadTest.cs @@ -33,19 +33,15 @@ public sealed class HotReloadTest : RobustUnitTest value: 10 - type: {HotReloadTestComponentTwoId}"; - private IComponentFactory _components = default!; private PrototypeManager _prototypes = default!; private IMapManager _maps = default!; private IEntityManager _entities = default!; + protected override Type[]? ExtraComponents => new[] {typeof(HotReloadTestOneComponent), typeof(HotReloadTestTwoComponent)}; + [OneTimeSetUp] public void Setup() { - _components = IoCManager.Resolve(); - _components.RegisterClass(); - _components.RegisterClass(); - _components.GenerateNetIds(); - IoCManager.Resolve().Initialize(); _prototypes = (PrototypeManager) IoCManager.Resolve(); _prototypes.RegisterKind(typeof(EntityPrototype)); diff --git a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs index bb1a8b1b46d..bcf803e0b85 100644 --- a/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs +++ b/Robust.UnitTesting/Shared/Prototypes/PrototypeManager_Test.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using JetBrains.Annotations; using NUnit.Framework; @@ -21,13 +22,11 @@ public sealed class PrototypeManager_Test : RobustUnitTest private const string LoadStringTestDummyId = "LoadStringTestDummy"; private IPrototypeManager manager = default!; + protected override Type[] ExtraComponents => new[] {typeof(TestBasicPrototypeComponent), typeof(PointLightComponent)}; + [OneTimeSetUp] public void Setup() { - var factory = IoCManager.Resolve(); - factory.RegisterClass(); - factory.RegisterClass(); - IoCManager.Resolve().Initialize(); manager = IoCManager.Resolve(); manager.RegisterKind(typeof(EntityPrototype)); diff --git a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs index a2691cbdbab..3fc8251ac33 100644 --- a/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/InheritanceSerializationTest.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -43,16 +44,11 @@ public sealed partial class InheritanceSerializationTest : RobustUnitTest inheritorField: {InheritorComponentFieldValue} finalField: {FinalComponentFieldValue}"; + protected override Type[]? ExtraComponents => new[] {typeof(TestBaseComponent), typeof(TestInheritorComponent), typeof(TestFinalComponent)}; + [Test] public void Test() { - var componentFactory = IoCManager.Resolve(); - - componentFactory.RegisterClass(); - componentFactory.RegisterClass(); - componentFactory.RegisterClass(); - componentFactory.GenerateNetIds(); - var serializationManager = IoCManager.Resolve(); serializationManager.Initialize(); diff --git a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs index 37e75089b8b..cdeab9532b0 100644 --- a/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs +++ b/Robust.UnitTesting/Shared/Serialization/TypeSerializers/ComponentRegistrySerializerTest.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using NUnit.Framework; using Robust.Shared.GameObjects; @@ -19,12 +20,7 @@ namespace Robust.UnitTesting.Shared.Serialization.TypeSerializers [TestOf(typeof(ComponentRegistrySerializer))] public sealed class ComponentRegistrySerializerTest : SerializationTest { - [OneTimeSetUp] - public new void OneTimeSetup() - { - var componentFactory = IoCManager.Resolve(); - componentFactory.RegisterClass(); - } + protected override Type[]? ExtraComponents => new[] {typeof(TestComponent)}; [Test] public void SerializationTest()