From 1e843bf3a2f341fb3ec82fd7401d6796055c8cb5 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sun, 30 Oct 2022 05:36:23 +1300 Subject: [PATCH] More Transform ECS (#3368) --- Robust.Server/Maps/MapLoader.cs | 4 +- Robust.Server/Physics/GridFixtureSystem.cs | 2 +- .../Containers/SharedContainerSystem.cs | 22 +- .../Transform/TransformComponent.cs | 170 +------- Robust.Shared/GameObjects/EntityManager.cs | 32 +- .../GameObjects/Systems/EntityLookupSystem.cs | 5 - .../SharedTransformSystem.Component.cs | 389 +++++++++++------- .../Systems/SharedTransformSystem.cs | 2 +- Robust.Shared/Map/MapManager.GridTrees.cs | 9 +- Robust.Shared/Map/MapManager.MapCollection.cs | 1 + Robust.Shared/Physics/PhysicsHelpers.cs | 2 + .../Systems/SharedPhysicsSystem.Components.cs | 8 +- Robust.UnitTesting/RobustUnitTest.cs | 22 +- .../Systems/AnchoredSystemTests.cs | 5 +- .../Shared/Map/MapPauseTests.cs | 4 +- 15 files changed, 351 insertions(+), 326 deletions(-) diff --git a/Robust.Server/Maps/MapLoader.cs b/Robust.Server/Maps/MapLoader.cs index 9dbdb3b43..91c1d1b69 100644 --- a/Robust.Server/Maps/MapLoader.cs +++ b/Robust.Server/Maps/MapLoader.cs @@ -780,6 +780,8 @@ namespace Robust.Server.Maps if (_loadOptions is null || _loadOptions.TransformMatrix.EqualsApprox(Matrix3.Identity)) return; + var xformSys = _serverEntityManager.EntitySysManager.GetEntitySystem(); + foreach (var entity in Entities) { if (!_xformQuery!.Value.TryGetComponent(entity, out var transform) || @@ -787,7 +789,7 @@ namespace Robust.Server.Maps var off = _loadOptions.TransformMatrix.Transform(transform.Coordinates.Position); - transform.Coordinates = transform.Coordinates.WithPosition(off); + xformSys.SetCoordinates(transform, transform.Coordinates.WithPosition(off)); transform.WorldRotation += _loadOptions.Rotation; } } diff --git a/Robust.Server/Physics/GridFixtureSystem.cs b/Robust.Server/Physics/GridFixtureSystem.cs index 39a09ba05..fe2c48047 100644 --- a/Robust.Server/Physics/GridFixtureSystem.cs +++ b/Robust.Server/Physics/GridFixtureSystem.cs @@ -318,7 +318,7 @@ namespace Robust.Server.Physics if (entXform.ParentUid != mapGrid.GridEntityId || !bounds.Contains(entXform.LocalPosition)) continue; - entXform.AttachParent(splitXform); + _xformSystem.SetParent(entXform, splitXform.Owner, xformQuery, splitXform); } } diff --git a/Robust.Shared/Containers/SharedContainerSystem.cs b/Robust.Shared/Containers/SharedContainerSystem.cs index 903db719b..de3053e60 100644 --- a/Robust.Shared/Containers/SharedContainerSystem.cs +++ b/Robust.Shared/Containers/SharedContainerSystem.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Utility; @@ -9,6 +10,8 @@ namespace Robust.Shared.Containers { public abstract class SharedContainerSystem : EntitySystem { + [Dependency] private readonly SharedTransformSystem _xforms = default!; + /// public override void Initialize() { @@ -364,11 +367,16 @@ namespace Robust.Shared.Containers else container.Remove(entity); - if (moveTo.HasValue) - Transform(entity).Coordinates = moveTo.Value; + if (moveTo.HasValue || attachToGridOrMap) + { + var xform = Transform(entity); - if (attachToGridOrMap) - Transform(entity).AttachToGridOrMap(); + if (moveTo.HasValue) + _xforms.SetCoordinates(xform, moveTo.Value); + + if (attachToGridOrMap) + xform.AttachToGridOrMap(); + } } } @@ -387,8 +395,8 @@ namespace Robust.Shared.Containers public void AttachParentToContainerOrGrid(TransformComponent transform) { - if (transform.Parent == null - || !TryGetContainingContainer(transform.Parent.Owner, out var container) + if (!transform.ParentUid.IsValid() + || !TryGetContainingContainer(transform.ParentUid, out var container) || !TryInsertIntoContainer(transform, container)) transform.AttachToGridOrMap(); } @@ -397,7 +405,7 @@ namespace Robust.Shared.Containers { if (container.Insert(transform.Owner)) return true; - if (Transform(container.Owner).Parent != null + if (Transform(container.Owner).ParentUid.IsValid() && TryGetContainingContainer(container.Owner, out var newContainer)) return TryInsertIntoContainer(transform, newContainer); diff --git a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs index 31ecc3f38..011f35228 100644 --- a/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs +++ b/Robust.Shared/GameObjects/Components/Transform/TransformComponent.cs @@ -154,6 +154,9 @@ namespace Robust.Shared.GameObjects if (!DeferUpdates) { MatricesDirty = true; + if (!Initialized) + return; + var moveEvent = new MoveEvent(Owner, Coordinates, Coordinates, oldRotation, _localRotation, this, _gameTiming.ApplyingState); _entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent, true); } @@ -201,36 +204,12 @@ namespace Robust.Shared.GameObjects public TransformComponent? Parent { get => !_parent.IsValid() ? null : _entMan.GetComponent(_parent); - internal set - { - if (value == null) - { - AttachToGridOrMap(); - return; - } - - if (_anchored && (value).Owner != _parent) - { - Anchored = false; - } - - AttachParent(value); - } } /// /// The UID of the parent entity that this entity is attached to. /// - [ViewVariables(VVAccess.ReadWrite)] - public EntityUid ParentUid - { - get => _parent; - set - { - if (value == _parent) return; - Parent = _entMan.GetComponent(value); - } - } + public EntityUid ParentUid => _parent; /// /// Matrix for transforming points from local to world space. @@ -328,99 +307,10 @@ namespace Robust.Shared.GameObjects var valid = _parent.IsValid(); return new EntityCoordinates(valid ? _parent : Owner, valid ? LocalPosition : Vector2.Zero); } - // NOTE: This setter must be callable from before initialize (inheriting from AttachParent's note) + [Obsolete("Use the system's setter method instead.")] set { - // unless the parent is changing, nothing to do here - if(value.EntityId == _parent && _anchored) - return; - - var sameParent = value.EntityId == _parent; - - if (!sameParent) - { - // Need to set anchored before we update position so that we can clear snapgrid cells correctly. - if(_parent != EntityUid.Invalid) // Allow setting Transform.Parent in Prototypes - Anchored = false; // changing the parent un-anchors the entity - } - - var oldPosition = Coordinates; - var oldRotation = LocalRotation; - _localPosition = value.Position; - var changedParent = false; - - if (!sameParent) - { - var xformQuery = _entMan.GetEntityQuery(); - changedParent = true; - var newParent = xformQuery.GetComponent(value.EntityId); - - DebugTools.Assert(newParent != this, - $"Can't parent a {nameof(TransformComponent)} to itself."); - - if (newParent.LifeStage > ComponentLifeStage.Running || - _entMan.GetComponent(newParent.Owner).EntityLifeStage > EntityLifeStage.MapInitialized) - { - var msg = $"Attempted to re-parent to a terminating object. Entity: {_entMan.ToPrettyString(Owner)}, new parent: {_entMan.ToPrettyString(value.EntityId)}"; -#if EXCEPTION_TOLERANCE - Logger.Error(msg); - _entMan.DeleteEntity(Owner); -#else - throw new InvalidOperationException(msg); -#endif - } - - // That's already our parent, don't bother attaching again. - - var oldParent = _parent.IsValid() ? xformQuery.GetComponent(_parent) : null; - var uid = Owner; - oldParent?._children.Remove(uid); - newParent._children.Add(uid); - - // offset position from world to parent - _parent = value.EntityId; - var oldMapId = MapID; - ChangeMapId(newParent.MapID, xformQuery); - - // Cache new GridID before raising the event. - _entMan.EntitySysManager.GetEntitySystem().SetGridId(this, FindGridEntityId(xformQuery), xformQuery); - - // preserve world rotation - if (LifeStage == ComponentLifeStage.Running) - { - oldRotation = _localRotation; - _localRotation += (oldParent?.WorldRotation ?? Angle.Zero) - newParent.WorldRotation; - } - - var entParentChangedMessage = new EntParentChangedMessage(Owner, oldParent?.Owner, oldMapId, this); - _entMan.EventBus.RaiseLocalEvent(Owner, ref entParentChangedMessage, true); - } - - // These conditions roughly emulate the effects of the code before I changed things, - // in regards to when to rebuild matrices. - // This may not in fact be the right thing. - if (changedParent || !DeferUpdates) - MatricesDirty = true; - - Dirty(_entMan); - - if (!DeferUpdates) - { - //TODO: This is a hack, look into WHY we can't call GridPosition before the comp is Running - if (Running) - { - if (!oldPosition.Equals(Coordinates) || !oldRotation.Equals(_localRotation)) - { - var moveEvent = new MoveEvent(Owner, oldPosition, Coordinates, oldRotation, _localRotation, this, _gameTiming.ApplyingState); - _entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent, true); - } - } - } - else - { - _oldCoords ??= oldPosition; - _oldLocalRotation ??= oldRotation; - } + _entMan.EntitySysManager.GetEntitySystem().SetCoordinates(this, value); } } @@ -455,6 +345,9 @@ namespace Robust.Shared.GameObjects if (!DeferUpdates) { MatricesDirty = true; + if (!Initialized) + return; + var moveEvent = new MoveEvent(Owner, oldGridPos, Coordinates, _localRotation, _localRotation, this, _gameTiming.ApplyingState); _entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent, true); } @@ -620,7 +513,7 @@ namespace Robust.Shared.GameObjects if (!_mapManager.IsMap(Owner)) Logger.Warning($"Detached a non-map entity ({_entMan.ToPrettyString(Owner)}) to null-space. Unless this entity is being deleted, this should not happen."); - DetachParentToNull(); + _entMan.EntitySysManager.GetEntitySystem().DetachParentToNull(this); return; } @@ -640,29 +533,14 @@ namespace Robust.Shared.GameObjects Dirty(_entMan); } - [Obsolete("Use transform system")] - public void DetachParentToNull() - { - _entMan.EntitySysManager.GetEntitySystem().DetachParentToNull(this); - } - /// /// Sets another entity as the parent entity, maintaining world position. /// /// + [Obsolete("Use TransformSystem.SetParent() instead")] public void AttachParent(TransformComponent newParent) { - //NOTE: This function must be callable from before initialize - - // don't attach to something we're already attached to - if (ParentUid == newParent.Owner) - return; - - DebugTools.Assert(newParent != this, - $"Can't parent a {nameof(TransformComponent)} to itself."); - - // offset position from world to parent, and set - Coordinates = new EntityCoordinates(newParent.Owner, newParent.InvWorldMatrix.Transform(WorldPosition)); + _entMan.EntitySysManager.GetEntitySystem().SetParent(this, newParent.Owner, newParent); } internal void ChangeMapId(MapId newMapId, EntityQuery xformQuery) @@ -707,10 +585,10 @@ namespace Robust.Shared.GameObjects } } + [Obsolete("Use TransformSystem.SetParent() instead")] public void AttachParent(EntityUid parent) { - var transform = _entMan.GetComponent(parent); - AttachParent(transform); + _entMan.EntitySysManager.GetEntitySystem().SetParent(this, parent, _entMan.GetEntityQuery()); } /// @@ -801,7 +679,7 @@ namespace Robust.Shared.GameObjects var invMatrix = InvLocalMatrix; var worldMatrix = LocalMatrix; - // By doing these all at once we can elide multiple IsValid + GetComponent calls + // By doing these all at once we can avoid multiple IsValid + GetComponent calls while (parent.IsValid()) { var xform = xformQuery.GetComponent(parent); @@ -841,23 +719,15 @@ namespace Robust.Shared.GameObjects { return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}"; } - - internal void SetAnchored(bool value, bool issueEvent = true) - { - _anchored = value; - Dirty(_entMan); - - if (issueEvent) - { - var anchorStateChangedEvent = new AnchorStateChangedEvent(this, false); - _entMan.EventBus.RaiseLocalEvent(Owner, ref anchorStateChangedEvent, true); - } - } } /// /// Raised whenever an entity translates or rotates relative to their parent. /// + /// + /// This will also get raised if the entity's parent changes, even if the local position and rotation remains + /// unchanged. + /// [ByRefEvent] public readonly struct MoveEvent { @@ -929,7 +799,7 @@ namespace Robust.Shared.GameObjects /// public readonly bool Detaching; - public AnchorStateChangedEvent(TransformComponent transform, bool detaching) + public AnchorStateChangedEvent(TransformComponent transform, bool detaching = false) { Detaching = detaching; Transform = transform; diff --git a/Robust.Shared/GameObjects/EntityManager.cs b/Robust.Shared/GameObjects/EntityManager.cs index 37fc93307..a60417829 100644 --- a/Robust.Shared/GameObjects/EntityManager.cs +++ b/Robust.Shared/GameObjects/EntityManager.cs @@ -1,17 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Prometheus; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Profiling; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Robust.Shared.GameObjects { @@ -30,6 +28,10 @@ namespace Robust.Shared.GameObjects [Dependency] private readonly ISerializationManager _serManager = default!; [Dependency] private readonly ProfManager _prof = default!; + // I feel like PJB might shed me for putting a system dependency here, but its required for setting entity + // positions on spawn.... + private SharedTransformSystem _xforms = default!; + #endregion Dependencies /// @@ -99,6 +101,7 @@ namespace Robust.Shared.GameObjects _entitySystemManager.Initialize(); Started = true; _eventBus.CalcOrdering(); + _xforms = _entitySystemManager.GetEntitySystem(); } public virtual void Shutdown() @@ -181,7 +184,7 @@ namespace Robust.Shared.GameObjects if (coordinates.IsValid(this)) { - GetComponent(newEntity).Coordinates = coordinates; + _xforms.SetCoordinates(GetComponent(newEntity), coordinates, unanchor: false); } return newEntity; @@ -210,13 +213,18 @@ namespace Robust.Shared.GameObjects if (mapXform == null) throw new ArgumentException($"Attempted to spawn entity on an invalid map. Coordinates: {coordinates}"); - transform.AttachParent(mapXform); + EntityCoordinates coords; + if (transform.Anchored && _mapManager.TryFindGridAt(coordinates, out var grid)) + { + coords = new(grid.GridEntityId, grid.WorldToLocal(coordinates.Position)); + _xforms.SetCoordinates(transform, coords, unanchor: false); + } + else + { + coords = new EntityCoordinates(mapEnt, coordinates.Position); + _xforms.SetCoordinates(transform, coords, null, mapXform); + } - // TODO: Look at this bullshit. Please code a way to force-move an entity regardless of anchoring. - var oldAnchored = transform.Anchored; - transform.Anchored = false; - transform.WorldPosition = coordinates.Position; - transform.Anchored = oldAnchored; return newEntity; } diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index ede41ee88..7f0b2e816 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -128,7 +128,6 @@ namespace Robust.Shared.GameObjects // TODO: Only container children need updating so could manually do this slightly better. AddToEntityTree(lookup, xform, aabb, xformQuery, metaQuery, contQuery, lookupRotation); } - #region DynamicTree private void OnMapChange(MapChangedEvent ev) @@ -518,10 +517,6 @@ namespace Robust.Shared.GameObjects { var xformQuery = GetEntityQuery(); - // TODO remove this check after #3368 gets merged - if (args.Component.LifeStage < ComponentLifeStage.Initialized && args.Component.GridUid == null) - _transform.SetGridId(args.Component, args.Component.FindGridEntityId(xformQuery)); - // Is this a grid? if (args.Component.GridUid == args.Sender) return; diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs index d0b171fc7..3565d3159 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.Component.cs @@ -1,22 +1,22 @@ -using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; using JetBrains.Annotations; using Robust.Shared.GameStates; -using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; +using System; +using System.Runtime.CompilerServices; namespace Robust.Shared.GameObjects; public abstract partial class SharedTransformSystem { [IoC.Dependency] private readonly IGameTiming _gameTiming = default!; + [IoC.Dependency] private readonly EntityLookupSystem _lookup = default!; + [IoC.Dependency] private readonly SharedPhysicsSystem _physics = default!; #region Anchoring @@ -62,26 +62,26 @@ public abstract partial class SharedTransformSystem public bool AnchorEntity(TransformComponent xform, IMapGrid grid, Vector2i tileIndices) { - var result = grid.AddToSnapGridCell(tileIndices, xform.Owner); + if (!grid.AddToSnapGridCell(tileIndices, xform.Owner)) + return false; - if (result) + var wasAnchored = xform._anchored; + xform._anchored = true; + + // Mark as static before doing position changes, to avoid the velocity change on parent change. + _physics.TrySetBodyType(xform.Owner, BodyType.Static); + + if (!wasAnchored && xform.Running) { - // Mark as static first to avoid the velocity change on parent change. - if (TryComp(xform.Owner, out var physicsComponent)) - physicsComponent.BodyType = BodyType.Static; - - // anchor snapping - // Internally it will do the parent update; doing it separately just triggers a redundant move. - xform.Coordinates = new EntityCoordinates(grid.GridEntityId, grid.GridTileToLocal(tileIndices).Position); - xform.SetAnchored(result); + var ev = new AnchorStateChangedEvent(xform); + RaiseLocalEvent(xform.Owner, ref ev, true); } - return result; - } + // Anchor snapping. Note that set coordiantes will dirty the component for us. + var pos = new EntityCoordinates(grid.GridEntityId, grid.GridTileToLocal(tileIndices).Position); + SetCoordinates(xform, pos, unanchor: false); - public bool AnchorEntity(TransformComponent xform, IMapGridComponent component) - { - return AnchorEntity(xform, component.Grid); + return true; } public bool AnchorEntity(TransformComponent xform, IMapGrid grid) @@ -92,35 +92,41 @@ public abstract partial class SharedTransformSystem public bool AnchorEntity(TransformComponent xform) { - if (!_mapManager.TryGetGrid(xform.GridUid, out var grid)) - { - return false; - } - - var tileIndices = grid.TileIndicesFor(xform.Coordinates); - return AnchorEntity(xform, grid, tileIndices); + return _mapManager.TryGetGrid(xform.GridUid, out var grid) + && AnchorEntity(xform, grid, grid.TileIndicesFor(xform.Coordinates)); } public void Unanchor(TransformComponent xform) { - //HACK: Client grid pivot causes this. - //TODO: make grid components the actual grid - if(xform.GridUid == null) + if (!xform._anchored) return; - UnanchorEntity(xform, Comp(xform.GridUid.Value)); - } + Dirty(xform); + xform._anchored = false; + _physics.TrySetBodyType(xform.Owner, BodyType.Dynamic); - public void UnanchorEntity(TransformComponent xform, IMapGridComponent grid) - { - var tileIndices = grid.Grid.TileIndicesFor(xform.Coordinates); - grid.Grid.RemoveFromSnapGridCell(tileIndices, xform.Owner); - if (TryComp(xform.Owner, out var physicsComponent)) + if (xform.LifeStage < ComponentLifeStage.Initialized) + return; + + if (TryComp(xform.GridUid, out IMapGridComponent? grid)) { - physicsComponent.BodyType = BodyType.Dynamic; + var tileIndices = grid.Grid.TileIndicesFor(xform.Coordinates); + grid.Grid.RemoveFromSnapGridCell(tileIndices, xform.Owner); + } + else if (xform.Initialized) + { + //HACK: Client grid pivot causes this. + //TODO: make grid components the actual grid + + // I have NFI what the comment above is on about, but this doesn't seem good, so lets log an error if it happens. + Logger.Error($"Missing grid while unanchoring {ToPrettyString(xform.Owner)}"); } - xform.SetAnchored(false); + if (!xform.Running) + return; + + var ev = new AnchorStateChangedEvent(xform); + RaiseLocalEvent(xform.Owner, ref ev, true); } #endregion @@ -233,26 +239,60 @@ public abstract partial class SharedTransformSystem if (component.GridUid == null) SetGridId(component, component.FindGridEntityId(xformQuery)); - component.MatricesDirty = true; - } - private void OnCompStartup(EntityUid uid, TransformComponent component, ComponentStartup args) - { - // Re-Anchor the entity if needed. - if (component._anchored && _mapManager.TryFindGridAt(component.MapPosition, out var grid)) + component.MatricesDirty = true; + + if (!component._anchored) + return; + + IMapGrid? grid; + + // First try find grid via parent: + if (component.GridUid == component.ParentUid && TryComp(component.ParentUid, out IMapGridComponent? gridComp)) { - if (!grid.IsAnchored(component.Coordinates, uid)) - { - AnchorEntity(component, grid); - } + grid = gridComp.Grid; } else + { + // Entity may not be directly parented to the grid (e.g., spawned using some relative entity coordiantes) + // in that case, we attempt to attach to a grid. + var pos = new MapCoordinates(GetWorldPosition(component), component.MapID); + _mapManager.TryFindGridAt(pos, out grid); + } + + if (grid == null) + { + Unanchor(component); + return; + } + + if (!AnchorEntity(component, grid)) component._anchored = false; + } - // Keep the cached matrices in sync with the fields. - Dirty(component); + private void OnCompStartup(EntityUid uid, TransformComponent xform, ComponentStartup args) + { + // TODO PERFORMANCE remove AnchorStateChangedEvent and EntParentChangedMessage events here. - var ev = new TransformStartupEvent(component); + // I hate this. Apparently some entities rely on this to perform their initialization logic (e.g., power + // receivers or lights?). Those components should just do their own init logic, instead of wasting time raising + // this event on every entity that gets created. + if (xform.Anchored) + { + DebugTools.Assert(xform.ParentUid == xform.GridUid && xform.ParentUid.IsValid()); + var anchorEv = new AnchorStateChangedEvent(xform); + RaiseLocalEvent(uid, ref anchorEv, true); + } + + // I hate this too. Once again, required for shit like containers because they CBF doing their own init logic + // and rely on parent changed messages instead. Might also be used by broadphase stuff? + var parentEv = new EntParentChangedMessage(uid, null, MapId.Nullspace, xform); + RaiseLocalEvent(uid, ref parentEv, true); + + // there should be no deferred events before startup has finished. + DebugTools.Assert(xform._oldCoords == null && xform._oldLocalRotation == null); + + var ev = new TransformStartupEvent(xform); RaiseLocalEvent(uid, ref ev, true); } @@ -265,7 +305,8 @@ public abstract partial class SharedTransformSystem /// public void SetGridId(TransformComponent xform, EntityUid? gridId, EntityQuery? xformQuery = null) { - if (xform._gridUid == gridId) return; + if (xform._gridUid == gridId) + return; DebugTools.Assert(gridId == null || HasComp(gridId)); @@ -327,6 +368,93 @@ public abstract partial class SharedTransformSystem #endregion + #region Coordinates + + public void SetCoordinates(EntityUid uid, EntityCoordinates value) + { + SetCoordinates(Transform(uid), value); + } + + /// + /// This sets the local position and parent of an entity. + /// + /// Final local rotation. If not specified, this will attempt to preserve world + /// rotation. + /// Whether or not to unanchor the entity before moving. Note that this will still move the + /// entity even when false. If you set this to false, you need to manually manage the grid lookup changes and ensure + /// the final position is valid + public void SetCoordinates(TransformComponent xform, EntityCoordinates value, Angle? rotation = null, TransformComponent? newParent = null, bool unanchor = true) + { + // NOTE: This setter must be callable from before initialize. + + if (xform.ParentUid == value.EntityId + && xform._localPosition.EqualsApprox(value.Position) + && (rotation == null || MathHelper.CloseTo(rotation.Value.Theta, xform._localRotation.Theta))) + { + return; + } + + var oldPosition = xform.Coordinates; + var oldRotation = xform._localRotation; + + if (xform.Anchored && unanchor) + Unanchor(xform); + + // Set new values + Dirty(xform); + xform.MatricesDirty = true; + xform._localPosition = value.Position; + + if (rotation != null) + xform._localRotation = rotation.Value; + + // Perform parent change logic + if (value.EntityId != xform._parent) + { + var xformQuery = GetEntityQuery(); + newParent ??= xformQuery.GetComponent(value.EntityId); + DebugTools.Assert(newParent.Owner == value.EntityId); + + DebugTools.Assert(value.EntityId != xform.Owner, $"Can't parent a {nameof(TransformComponent)} to itself."); + + if (newParent.LifeStage > ComponentLifeStage.Running || LifeStage(value.EntityId) > EntityLifeStage.MapInitialized) + { + Logger.Error($"Attempted to re-parent to a terminating object. Entity: {ToPrettyString(xform.Owner)}, new parent: {ToPrettyString(value.EntityId)}"); + QueueDel(xform.Owner); + return; + } + + var oldParent = xform._parent.IsValid() ? xformQuery.GetComponent(xform._parent) : null; + oldParent?._children.Remove(xform.Owner); + newParent._children.Add(xform.Owner); + + xform._parent = value.EntityId; + var oldMapId = xform.MapID; + xform.ChangeMapId(newParent.MapID, xformQuery); + SetGridId(xform, xform.FindGridEntityId(xformQuery), xformQuery); + + if (xform.Initialized) + { + // preserve world rotation + if (rotation == null && oldParent != null) + xform._localRotation += GetWorldRotation(oldParent, xformQuery) - GetWorldRotation(newParent, xformQuery); + + var entParentChangedMessage = new EntParentChangedMessage(xform.Owner, oldParent?.Owner, oldMapId, xform); + RaiseLocalEvent(xform.Owner, ref entParentChangedMessage, true); + } + } + + DebugTools.Assert(!xform.DeferUpdates); // breaks anchoring lookup logic if deferred. If this changes, also need to relocate the `xform.MatricesDirty = true` + + if (!xform.Initialized) + return; + + var moveEvent = new MoveEvent(xform.Owner, oldPosition, xform.Coordinates, oldRotation, xform._localRotation, xform, _gameTiming.ApplyingState); + RaiseLocalEvent(xform.Owner, ref moveEvent, true); + } + + #endregion + #region Parent public TransformComponent? GetParent(EntityUid uid) @@ -350,30 +478,29 @@ public abstract partial class SharedTransformSystem return xformQuery.GetComponent(xform.ParentUid); } - /* TODO: Need to peel out relevant bits of AttachParent e.g. children updates. - public void SetParent(TransformComponent xform, EntityUid parent, bool move = true) + public void SetParent(EntityUid uid, EntityUid parent) { - if (xform.ParentUid == parent) return; - - if (!parent.IsValid()) - { - xform.AttachToGridOrMap(); - return; - } - - if (xform.Anchored) - { - xform.Anchored = false; - } - - if (move) - xform.AttachParent(parent); - else - xform._parent = parent; - - Dirty(xform); + var query = GetEntityQuery(); + SetParent(query.GetComponent(uid), parent, query); + } + + public void SetParent(TransformComponent xform, EntityUid parent, TransformComponent? parentXform = null) + { + SetParent(xform, parent, GetEntityQuery(), parentXform); + } + + public void SetParent(TransformComponent xform, EntityUid parent, EntityQuery xformQuery, TransformComponent? parentXform = null) + { + if (xform.ParentUid == parent || !xformQuery.Resolve(parent, ref parentXform)) + return; + + var (_, parRot, parInvMatrix) = parentXform.GetWorldPositionRotationInvMatrix(xformQuery); + var (pos, rot) = GetWorldPositionRotation(xform, xformQuery); + var newPos = parInvMatrix.Transform(pos); + var newRot = rot - parRot; + + SetCoordinates(xform, new EntityCoordinates(parent, newPos), newRot, parentXform); } - */ #endregion @@ -398,97 +525,78 @@ public abstract partial class SharedTransformSystem component.Anchored); } - internal void OnHandleState(EntityUid uid, TransformComponent component, ref ComponentHandleState args) + internal void OnHandleState(EntityUid uid, TransformComponent xform, ref ComponentHandleState args) { if (args.Current is TransformComponentState newState) { var newParentId = newState.ParentID; - var rebuildMatrices = false; + var oldAnchored = xform.Anchored; - // Update to new parent. - // if the new one isn't valid (e.g. PVS) then we'll just return early after yeeting them to nullspace. - if (component.ParentUid != newParentId) + // update actual position data, if required + if (!xform.LocalPosition.EqualsApprox(newState.LocalPosition) + || !xform.LocalRotation.EqualsApprox(newState.Rotation) + || xform.ParentUid != newParentId) { - if (!newParentId.IsValid()) + // remove from any old grid lookups + if (xform.Anchored && TryComp(xform.ParentUid, out IMapGridComponent? grid)) { - DetachParentToNull(component); + var tileIndices = grid.Grid.TileIndicesFor(xform.Coordinates); + grid.Grid.RemoveFromSnapGridCell(tileIndices, xform.Owner); } - else + + // Set anchor state true during the move event unless the entity wasn't and isn't being anchored. This avoids unnecessary entity lookup changes. + xform._anchored |= newState.Anchored; + + // Update the action position, rotation, and parent (and hence also map, grid, etc). + SetCoordinates(xform, new EntityCoordinates(newParentId, newState.LocalPosition), newState.Rotation, unanchor: false); + + xform._anchored = newState.Anchored; + + // Add to any new grid lookups. Normal entity lookups will either have been handled by the move event, + // or by the following AnchorStateChangedEvent + if (xform._anchored && xform.Initialized) { - if (!Exists(newParentId)) + if (xform.ParentUid == xform.GridUid && TryComp(xform.GridUid, out IMapGridComponent? newGrid)) { -#if !EXCEPTION_TOLERANCE - throw new InvalidOperationException($"Unable to find new parent {newParentId}! This probably means the server never sent it."); -#else - _logger.Error($"Unable to find new parent {newParentId}! Deleting {ToPrettyString(uid)}"); - QueueDel(uid); - return; -#endif + var tileIndices = newGrid.Grid.TileIndicesFor(xform.Coordinates); + newGrid.Grid.AddToSnapGridCell(tileIndices, xform.Owner); + } + else + { + DebugTools.Assert("New transform state coordinates are incompatible with anchoring."); + xform._anchored = false; } - - component.AttachParent(Transform(newParentId)); } - - rebuildMatrices = true; - } - - if (!component.LocalPosition.EqualsApprox(newState.LocalPosition) || !component.LocalRotation.EqualsApprox(newState.Rotation)) - { - var oldPos = component.Coordinates; - component._localPosition = newState.LocalPosition; - var oldRot = component.LocalRotation; - component._localRotation = newState.Rotation; - - var ev = new MoveEvent(uid, oldPos, component.Coordinates, oldRot, component.LocalRotation, component, _gameTiming.ApplyingState); - DeferMoveEvent(ref ev); - - rebuildMatrices = true; - } - - component._prevPosition = newState.LocalPosition; - component._prevRotation = newState.Rotation; - - // Anchored currently does a TryFindGridAt internally which may fail in particularly... violent situations. - if (newState.Anchored && !component.Anchored) - { - DebugTools.Assert(component.GridUid != null); - var iGrid = Comp(component.GridUid!.Value); - AnchorEntity(component, iGrid); - DebugTools.Assert(component.Anchored); } else { - component.Anchored = newState.Anchored; + xform.Anchored = newState.Anchored; } - component._noLocalRotation = newState.NoLocalRotation; - - // This is not possible, because client entities don't exist on the server, so the parent HAS to be a shared entity. - // If this assert fails, the code above that sets the parent is broken. - DebugTools.Assert(!component.ParentUid.IsClientSide(), "Transform received a state, but is still parented to a client entity."); - - // Whatever happened on the client, these should still be correct - DebugTools.Assert(component.ParentUid == newState.ParentID); - DebugTools.Assert(component.Anchored == newState.Anchored); - - if (rebuildMatrices) + if (oldAnchored != newState.Anchored && xform.Initialized) { - component.MatricesDirty = true; + var ev = new AnchorStateChangedEvent(xform); + RaiseLocalEvent(xform.Owner, ref ev, true); } + + xform._prevPosition = newState.LocalPosition; + xform._prevRotation = newState.Rotation; + xform._noLocalRotation = newState.NoLocalRotation; - Dirty(component); + DebugTools.Assert(xform.ParentUid == newState.ParentID, "Transform state failed to set parent"); + DebugTools.Assert(xform.Anchored == newState.Anchored, "Transform state failed to set anchored"); } if (args.Next is TransformComponentState nextTransform) { - component._nextPosition = nextTransform.LocalPosition; - component._nextRotation = nextTransform.Rotation; - component.LerpParent = nextTransform.ParentID; - ActivateLerp(component); + xform._nextPosition = nextTransform.LocalPosition; + xform._nextRotation = nextTransform.Rotation; + xform.LerpParent = nextTransform.ParentID; + ActivateLerp(xform); } else { - DeactivateLerp(component); + DeactivateLerp(xform); } } @@ -735,6 +843,9 @@ public abstract partial class SharedTransformSystem if (!xform.DeferUpdates) { xform.MatricesDirty = true; + if (!xform.Initialized) + return; + var moveEvent = new MoveEvent(xform.Owner, oldPosition, xform.Coordinates, oldRotation, rot, xform, _gameTiming.ApplyingState); RaiseLocalEvent(xform.Owner, ref moveEvent, true); } diff --git a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs index 25fe94de1..79847ee5c 100644 --- a/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs +++ b/Robust.Shared/GameObjects/Systems/SharedTransformSystem.cs @@ -103,7 +103,7 @@ namespace Robust.Shared.GameObjects } if (aabb.Contains(xform.LocalPosition)) - xform.AttachParent(mapTransform); + SetParent(xform, mapTransform.Owner, parentXform: mapTransform); } } diff --git a/Robust.Shared/Map/MapManager.GridTrees.cs b/Robust.Shared/Map/MapManager.GridTrees.cs index 6218d6c18..5d18b4952 100644 --- a/Robust.Shared/Map/MapManager.GridTrees.cs +++ b/Robust.Shared/Map/MapManager.GridTrees.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.Log; @@ -103,7 +104,7 @@ internal partial class MapManager { var aabb = GetWorldAABB(grid); var proxy = _gridTrees[mapId].CreateProxy(in aabb, grid); - + DebugTools.Assert(grid.MapProxy == DynamicTree.Proxy.Free); grid.MapProxy = proxy; _movedGrids[mapId].Add(grid); @@ -147,7 +148,10 @@ internal partial class MapManager // oh boy // Want gridinit to handle this hence specialcase those situations. - if (lifestage < EntityLifeStage.Initialized) return; + // oh boy oh boy, its even worse now. + // transform now raises parent change events on startup, because container code is a POS. + if (lifestage < EntityLifeStage.Initialized || args.Transform.LifeStage == ComponentLifeStage.Starting) + return; // Make sure we cleanup old map for moved grid stuff. var mapId = args.Transform.MapID; @@ -161,6 +165,7 @@ internal partial class MapManager RemoveGrid(aGrid, args.OldMapId); } + DebugTools.Assert(aGrid.MapProxy == DynamicTree.Proxy.Free); if (_movedGrids.TryGetValue(mapId, out var newMovedGrids)) { newMovedGrids.Add(component.Grid); diff --git a/Robust.Shared/Map/MapManager.MapCollection.cs b/Robust.Shared/Map/MapManager.MapCollection.cs index 79da7ef0c..fcf9d6aba 100644 --- a/Robust.Shared/Map/MapManager.MapCollection.cs +++ b/Robust.Shared/Map/MapManager.MapCollection.cs @@ -273,6 +273,7 @@ internal partial class MapManager var mapComp = EntityManager.AddComponent(newEnt); mapComp.WorldMap = actualId; + EntityManager.Dirty(mapComp); EntityManager.InitializeComponents(newEnt); EntityManager.StartComponents(newEnt); Logger.DebugS("map", $"Binding map {actualId} to entity {newEnt}"); diff --git a/Robust.Shared/Physics/PhysicsHelpers.cs b/Robust.Shared/Physics/PhysicsHelpers.cs index f95512ae3..d2b52bdc2 100644 --- a/Robust.Shared/Physics/PhysicsHelpers.cs +++ b/Robust.Shared/Physics/PhysicsHelpers.cs @@ -2,11 +2,13 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Physics.Components; +using System; namespace Robust.Shared.Physics { public static class PhysicsHelpers { + [Obsolete("Wtf is this, this isn't how you calculate this???? Use the existing system method instead.")] public static Vector2 GlobalLinearVelocity(this EntityUid entity) { var entMan = IoCManager.Resolve(); diff --git a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs index fc48098c3..12dfdb5a0 100644 --- a/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs +++ b/Robust.Shared/Physics/Systems/SharedPhysicsSystem.Components.cs @@ -229,7 +229,7 @@ public partial class SharedPhysicsSystem body.InvI = 0.0f; body._localCenter = Vector2.Zero; - if (!Resolve(body.Owner, ref fixtures)) + if (!Resolve(body.Owner, ref fixtures, false)) return; var localCenter = Vector2.Zero; @@ -382,6 +382,12 @@ public partial class SharedPhysicsSystem Dirty(body); } + public void TrySetBodyType(EntityUid uid, BodyType value) + { + if (TryComp(uid, out PhysicsComponent? body)) + SetBodyType(body, value); + } + public void SetBodyType(PhysicsComponent body, BodyType value) { if (body._bodyType == value) diff --git a/Robust.UnitTesting/RobustUnitTest.cs b/Robust.UnitTesting/RobustUnitTest.cs index ce7ab5bf9..0dc00a2cd 100644 --- a/Robust.UnitTesting/RobustUnitTest.cs +++ b/Robust.UnitTesting/RobustUnitTest.cs @@ -85,17 +85,30 @@ namespace Robust.UnitTesting // uhhh so maybe these are the wrong system for the client, but I CBF adding sprite system and all the rest, // and it was like this when I found it. - systems.LoadExtraSystemType(); - systems.LoadExtraSystemType(); + + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); if (Project == UnitTestProject.Client) { systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); } else { systems.LoadExtraSystemType(); systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); + systems.LoadExtraSystemType(); } var entMan = IoCManager.Resolve(); @@ -124,6 +137,11 @@ namespace Robust.UnitTesting compFactory.RegisterClass(); } + if (!compFactory.AllRegisteredTypes.Contains(typeof(JointComponent))) + { + compFactory.RegisterClass(); + } + // So by default EntityManager does its own EntitySystemManager initialize during Startup. // We want to bypass this and load our own systems hence we will manually initialize it here. entMan.Initialize(); diff --git a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs b/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs index 5d3e6c7a4..56962d531 100644 --- a/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs +++ b/Robust.UnitTesting/Shared/GameObjects/Systems/AnchoredSystemTests.cs @@ -201,7 +201,6 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems // Act IoCManager.Resolve().GetComponent(ent1).WorldPosition = new Vector2(99, 99); IoCManager.Resolve().GetComponent(ent1).LocalPosition = new Vector2(99, 99); - IoCManager.Resolve().GetComponent(ent1).Coordinates = new EntityCoordinates(grid.GridEntityId, 99, 99); // make sure not to change parent, that would un-anchor Assert.That(IoCManager.Resolve().GetComponent(ent1).MapPosition, Is.EqualTo(coordinates)); Assert.That(calledCount, Is.EqualTo(0)); @@ -232,7 +231,7 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems IoCManager.Resolve().GetComponent(ent1).Anchored = true; // Act - IoCManager.Resolve().GetComponent(ent1).ParentUid = mapMan.GetMapEntityId(TestMapId); + entMan.EntitySysManager.GetEntitySystem().SetParent(ent1, mapMan.GetMapEntityId(TestMapId)); Assert.That(IoCManager.Resolve().GetComponent(ent1).Anchored, Is.False); Assert.That(grid.GetAnchoredEntities(tileIndices).Count(), Is.EqualTo(0)); @@ -258,7 +257,7 @@ namespace Robust.UnitTesting.Shared.GameObjects.Systems IoCManager.Resolve().GetComponent(ent1).Anchored = true; // Act - IoCManager.Resolve().GetComponent(ent1).ParentUid = grid.GridEntityId; + entMan.EntitySysManager.GetEntitySystem().SetParent(ent1, grid.GridEntityId); Assert.That(grid.GetAnchoredEntities(tileIndices).First(), Is.EqualTo(ent1)); Assert.That(grid.GetTileRef(tileIndices).Tile, Is.Not.EqualTo(Tile.Empty)); diff --git a/Robust.UnitTesting/Shared/Map/MapPauseTests.cs b/Robust.UnitTesting/Shared/Map/MapPauseTests.cs index f21dd8a68..485c78957 100644 --- a/Robust.UnitTesting/Shared/Map/MapPauseTests.cs +++ b/Robust.UnitTesting/Shared/Map/MapPauseTests.cs @@ -173,7 +173,7 @@ internal sealed class MapPauseTests mapMan.SetMapPaused(map2, false); // Act - xform.ParentUid = mapMan.GetMapEntityId(map2); + entMan.EntitySysManager.GetEntitySystem().SetParent(xform.Owner, mapMan.GetMapEntityId(map2)); var metaData = entMan.GetComponent(newEnt); Assert.That(metaData.EntityPaused, Is.False); @@ -201,7 +201,7 @@ internal sealed class MapPauseTests mapMan.SetMapPaused(map2, true); // Act - xform.ParentUid = mapMan.GetMapEntityId(map2); + entMan.EntitySysManager.GetEntitySystem().SetParent(xform.Owner, mapMan.GetMapEntityId(map2)); var metaData = entMan.GetComponent(newEnt); Assert.That(metaData.EntityPaused, Is.True);