using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Shared.Animations; using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Shared.GameObjects { /// /// Stores the position and orientation of the entity. /// [RegisterComponent, NetworkedComponent] public sealed partial class TransformComponent : Component, IComponentDebug { [Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; // Currently this field just exists for VV. In future, it might become a real field [ViewVariables] private NetEntity NetParent => _entMan.GetNetEntity(_parent); [DataField("parent")] internal EntityUid _parent; [DataField("pos")] internal Vector2 _localPosition = Vector2.Zero; // holds offset from grid, or offset from parent [DataField("rot")] internal Angle _localRotation; // local rotation [DataField("noRot")] internal bool _noLocalRotation; [DataField("anchored")] internal bool _anchored; /// /// The broadphase that this entity is currently stored on, if any. /// /// /// Maybe this should be moved to its own component eventually, but at least currently comps are not structs /// and this data is required whenever any entity moves, so this will just save a component lookup. /// [ViewVariables] internal BroadphaseData? Broadphase; internal bool MatricesDirty = true; private Matrix3 _localMatrix = Matrix3.Identity; private Matrix3 _invLocalMatrix = Matrix3.Identity; // these should just be system methods, but existing component functions like InvWorldMatrix still rely on // getting these so those have to be fully ECS-ed first. public Matrix3 LocalMatrix { get { if (MatricesDirty) RebuildMatrices(); return _localMatrix; } } public Matrix3 InvLocalMatrix { get { if (MatricesDirty) RebuildMatrices(); return _invLocalMatrix; } } // used for lerping [ViewVariables] public Vector2? NextPosition { get; internal set; } [ViewVariables] public Angle? NextRotation { get; internal set; } [ViewVariables] public Vector2 PrevPosition { get; internal set; } [ViewVariables] public Angle PrevRotation { get; internal set; } [ViewVariables] public bool ActivelyLerping; [ViewVariables] public GameTick LastLerp = GameTick.Zero; [ViewVariables] internal readonly HashSet _children = new(); [Dependency] private readonly IMapManager _mapManager = default!; /// /// Returns the index of the map which this object is on /// [ViewVariables] public MapId MapID { get; internal set; } internal bool _mapIdInitialized; internal bool _gridInitialized; // TODO: Cache this. /// /// The EntityUid of the map which this object is on, if any. /// public EntityUid? MapUid { get; internal set; } /// /// The EntityUid of the grid which this object is on, if any. /// [ViewVariables] public EntityUid? GridUid => _gridUid; [Access(typeof(SharedTransformSystem))] internal EntityUid? _gridUid = null; /// /// Disables or enables to ability to locally rotate the entity. When set it removes any local rotation. /// [ViewVariables(VVAccess.ReadWrite)] public bool NoLocalRotation { get => _noLocalRotation; set { if (value) LocalRotation = Angle.Zero; _noLocalRotation = value; _entMan.Dirty(Owner, this); } } /// /// Current rotation offset of the entity. /// [ViewVariables(VVAccess.ReadWrite)] [Animatable] public Angle LocalRotation { get => _localRotation; set { if(_noLocalRotation) return; if (_localRotation.EqualsApprox(value)) return; var oldRotation = _localRotation; _localRotation = value; _entMan.Dirty(Owner, this); MatricesDirty = true; if (!Initialized) return; var moveEvent = new MoveEvent(Owner, Coordinates, Coordinates, oldRotation, _localRotation, this, _gameTiming.ApplyingState); _entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent, true); } } /// /// Current world rotation of the entity. /// [ViewVariables(VVAccess.ReadWrite)] [Obsolete("Use the system method instead")] public Angle WorldRotation { get { var parent = _parent; var xformQuery = _entMan.GetEntityQuery(); var rotation = _localRotation; while (parent.IsValid()) { var parentXform = xformQuery.GetComponent(parent); rotation += parentXform._localRotation; parent = parentXform.ParentUid; } return rotation; } set { if (NoLocalRotation) return; var current = WorldRotation; var diff = value - current; LocalRotation += diff; } } // lazy VV convenience variable. [ViewVariables] private TransformComponent? _parentXform => !_parent.IsValid() ? null : _entMan.GetComponent(_parent); /// /// The UID of the parent entity that this entity is attached to. /// public EntityUid ParentUid => _parent; /// /// Matrix for transforming points from local to world space. /// [Obsolete("Use the system method instead")] public Matrix3 WorldMatrix { get { var xformQuery = _entMan.GetEntityQuery(); var parent = _parent; var myMatrix = LocalMatrix; while (parent.IsValid()) { var parentXform = xformQuery.GetComponent(parent); var parentMatrix = parentXform.LocalMatrix; parent = parentXform.ParentUid; Matrix3.Multiply(in myMatrix, in parentMatrix, out var result); myMatrix = result; } return myMatrix; } } /// /// Matrix for transforming points from world to local space. /// [Obsolete("Use the system method instead")] public Matrix3 InvWorldMatrix { get { var xformQuery = _entMan.GetEntityQuery(); var parent = _parent; var myMatrix = InvLocalMatrix; while (parent.IsValid()) { var parentXform = xformQuery.GetComponent(parent); var parentMatrix = parentXform.InvLocalMatrix; parent = parentXform.ParentUid; Matrix3.Multiply(in parentMatrix, in myMatrix, out var result); myMatrix = result; } return myMatrix; } } /// /// Current position offset of the entity relative to the world. /// Can de-parent from its parent if the parent is a grid. /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] [Obsolete("Use the system method instead")] public Vector2 WorldPosition { get { if (_parent.IsValid()) { // parent coords to world coords return _entMan.GetComponent(ParentUid).WorldMatrix.Transform(_localPosition); } else { return Vector2.Zero; } } set { if (!_parent.IsValid()) { DebugTools.Assert("Parent is invalid while attempting to set WorldPosition - did you try to move root node?"); return; } // world coords to parent coords var newPos = _entMan.GetComponent(ParentUid).InvWorldMatrix.Transform(value); LocalPosition = newPos; } } /// /// Position offset of this entity relative to its parent. /// [ViewVariables(VVAccess.ReadWrite)] public EntityCoordinates Coordinates { get { var valid = _parent.IsValid(); return new EntityCoordinates(valid ? _parent : Owner, valid ? LocalPosition : Vector2.Zero); } [Obsolete("Use the system's setter method instead.")] set => _entMan.EntitySysManager.GetEntitySystem().SetCoordinates(Owner, this, value); } /// /// Current position offset of the entity relative to the world. /// This is effectively a more complete version of /// [ViewVariables(VVAccess.ReadWrite)] [Obsolete("Use TransformSystem.GetMapCoordinates")] public MapCoordinates MapPosition => new(WorldPosition, MapID); /// /// Local offset of this entity relative to its parent /// ( if it's not null, to otherwise). /// [Animatable] [ViewVariables(VVAccess.ReadWrite)] public Vector2 LocalPosition { get => _localPosition; [Obsolete("Use the system method instead")] set { if(Anchored) return; if (_localPosition.EqualsApprox(value)) return; var oldGridPos = Coordinates; _localPosition = value; _entMan.Dirty(Owner, this); MatricesDirty = true; if (!Initialized) return; var moveEvent = new MoveEvent(Owner, oldGridPos, Coordinates, _localRotation, _localRotation, this, _gameTiming.ApplyingState); _entMan.EventBus.RaiseLocalEvent(Owner, ref moveEvent, true); } } /// /// Is this transform anchored to a grid tile? /// [ViewVariables(VVAccess.ReadWrite)] public bool Anchored { get => _anchored; [Obsolete("Use the SharedTransformSystem.AnchorEntity/Unanchor methods instead.")] set { // This will be set again when the transform initializes, actually anchoring it. if (!Initialized) { _anchored = value; } else if (value && !_anchored && _mapManager.TryFindGridAt(MapPosition, out _, out var grid)) { _anchored = _entMan.EntitySysManager.GetEntitySystem().AnchorEntity(Owner, this, grid); } else if (!value && _anchored) { // An anchored entity is always parented to the grid. // If Transform.Anchored is true in the prototype but the entity was not spawned with a grid as the parent, // then this will be false. _entMan.EntitySysManager.GetEntitySystem().Unanchor(Owner, this); } } } [ViewVariables] public IEnumerable Children { get { if (_children.Count == 0) yield break; var xforms = _entMan.GetEntityQuery(); var children = ChildEnumerator; while (children.MoveNext(out var child)) { yield return xforms.GetComponent(child.Value); } } } [ViewVariables] public IEnumerable ChildEntities => _children; public TransformChildrenEnumerator ChildEnumerator => new(_children.GetEnumerator()); [ViewVariables] public int ChildCount => _children.Count; [ViewVariables] public EntityUid LerpParent; public bool PredictedLerp; /// /// Detaches this entity from its parent. /// [Obsolete("Use the system's method instead.")] public void AttachToGridOrMap() { _entMan.EntitySysManager.GetEntitySystem().AttachToGridOrMap(Owner, this); } /// /// Sets another entity as the parent entity, maintaining world position. /// /// [Obsolete("Use TransformSystem.SetParent() instead")] public void AttachParent(TransformComponent newParent) { _entMan.EntitySysManager.GetEntitySystem().SetParent(Owner, this, newParent.Owner, newParent); } internal void ChangeMapId(MapId newMapId, EntityQuery xformQuery) { if (newMapId == MapID) return; EntityUid? newUid = newMapId == MapId.Nullspace ? null : _mapManager.GetMapEntityId(newMapId); //Set Paused state var mapPaused = _mapManager.IsMapPaused(newMapId); var metaEnts = _entMan.GetEntityQuery(); var metaData = metaEnts.GetComponent(Owner); var metaSystem = _entMan.EntitySysManager.GetEntitySystem(); metaSystem.SetEntityPaused(Owner, mapPaused, metaData); MapUid = newUid; MapID = newMapId; UpdateChildMapIdsRecursive(MapID, newUid, mapPaused, xformQuery, metaEnts, metaSystem); } internal void UpdateChildMapIdsRecursive( MapId newMapId, EntityUid? newUid, bool mapPaused, EntityQuery xformQuery, EntityQuery metaQuery, MetaDataSystem system) { var childEnumerator = ChildEnumerator; while (childEnumerator.MoveNext(out var child)) { //Set Paused state var metaData = metaQuery.GetComponent(child.Value); system.SetEntityPaused(child.Value, mapPaused, metaData); var concrete = xformQuery.GetComponent(child.Value); concrete.MapUid = newUid; concrete.MapID = newMapId; if (concrete.ChildCount != 0) { concrete.UpdateChildMapIdsRecursive(newMapId, newUid, mapPaused, xformQuery, metaQuery, system); } } } [Obsolete("Use TransformSystem.SetParent() instead")] public void AttachParent(EntityUid parent) { _entMan.EntitySysManager.GetEntitySystem().SetParent(Owner, this, parent, _entMan.GetEntityQuery()); } /// /// Get the WorldPosition and WorldRotation of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation() { // Worldmatrix needs calculating anyway for worldpos so we'll just drop it. var (worldPos, worldRot, _) = GetWorldPositionRotationMatrix(); return (worldPos, worldRot); } /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation) GetWorldPositionRotation(EntityQuery xforms) { var (worldPos, worldRot, _) = GetWorldPositionRotationMatrix(xforms); return (worldPos, worldRot); } /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 WorldMatrix) GetWorldPositionRotationMatrix(EntityQuery xforms) { var parent = _parent; var worldRot = _localRotation; var worldMatrix = LocalMatrix; // By doing these all at once we can elide multiple IsValid + GetComponent calls while (parent.IsValid()) { var xform = xforms.GetComponent(parent); worldRot += xform.LocalRotation; var parentMatrix = xform.LocalMatrix; Matrix3.Multiply(in worldMatrix, in parentMatrix, out var result); worldMatrix = result; parent = xform.ParentUid; } var worldPosition = new Vector2(worldMatrix.R0C2, worldMatrix.R1C2); return (worldPosition, worldRot, worldMatrix); } /// /// Get the WorldPosition, WorldRotation, and WorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 WorldMatrix) GetWorldPositionRotationMatrix() { var xforms = _entMan.GetEntityQuery(); return GetWorldPositionRotationMatrix(xforms); } /// /// Get the WorldPosition, WorldRotation, and InvWorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 InvWorldMatrix) GetWorldPositionRotationInvMatrix() { var xformQuery = _entMan.GetEntityQuery(); return GetWorldPositionRotationInvMatrix(xformQuery); } /// /// Get the WorldPosition, WorldRotation, and InvWorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 InvWorldMatrix) GetWorldPositionRotationInvMatrix(EntityQuery xformQuery) { var (worldPos, worldRot, _, invWorldMatrix) = GetWorldPositionRotationMatrixWithInv(xformQuery); return (worldPos, worldRot, invWorldMatrix); } /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 WorldMatrix, Matrix3 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv() { var xformQuery = _entMan.GetEntityQuery(); return GetWorldPositionRotationMatrixWithInv(xformQuery); } /// /// Get the WorldPosition, WorldRotation, WorldMatrix, and InvWorldMatrix of this entity faster than each individually. /// [Obsolete("Use the system method instead")] public (Vector2 WorldPosition, Angle WorldRotation, Matrix3 WorldMatrix, Matrix3 InvWorldMatrix) GetWorldPositionRotationMatrixWithInv(EntityQuery xformQuery) { var parent = _parent; var worldRot = _localRotation; var invMatrix = InvLocalMatrix; var worldMatrix = LocalMatrix; // By doing these all at once we can avoid multiple IsValid + GetComponent calls while (parent.IsValid()) { var xform = xformQuery.GetComponent(parent); worldRot += xform.LocalRotation; var parentMatrix = xform.LocalMatrix; Matrix3.Multiply(in worldMatrix, in parentMatrix, out var result); worldMatrix = result; var parentInvMatrix = xform.InvLocalMatrix; Matrix3.Multiply(in parentInvMatrix, in invMatrix, out var invResult); invMatrix = invResult; parent = xform.ParentUid; } var worldPosition = new Vector2(worldMatrix.R0C2, worldMatrix.R1C2); return (worldPosition, worldRot, worldMatrix, invMatrix); } public void RebuildMatrices() { MatricesDirty = false; if (!_parent.IsValid()) // Root Node { _localMatrix = Matrix3.Identity; _invLocalMatrix = Matrix3.Identity; } _localMatrix = Matrix3.CreateTransform(_localPosition, _localRotation); _invLocalMatrix = Matrix3.CreateInverseTransform(_localPosition, _localRotation); } public string GetDebugString() { return $"pos/rot/wpos/wrot: {Coordinates}/{LocalRotation}/{WorldPosition}/{WorldRotation}"; } } /// /// 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 { public MoveEvent(EntityUid sender, EntityCoordinates oldPos, EntityCoordinates newPos, Angle oldRotation, Angle newRotation, TransformComponent component, bool stateHandling) { Sender = sender; OldPosition = oldPos; NewPosition = newPos; OldRotation = oldRotation; NewRotation = newRotation; Component = component; FromStateHandling = stateHandling; } public readonly EntityUid Sender; public readonly EntityCoordinates OldPosition; public readonly EntityCoordinates NewPosition; public readonly Angle OldRotation; public readonly Angle NewRotation; public readonly TransformComponent Component; public bool ParentChanged => NewPosition.EntityId != OldPosition.EntityId; /// /// If true, this event was generated during component state handling. This means it can be ignored in some instances. /// public readonly bool FromStateHandling; } public struct TransformChildrenEnumerator : IDisposable { private HashSet.Enumerator _children; public TransformChildrenEnumerator(HashSet.Enumerator children) { _children = children; } public bool MoveNext([NotNullWhen(true)] out EntityUid? child) { if (!_children.MoveNext()) { child = null; return false; } child = _children.Current; return true; } public void Dispose() { _children.Dispose(); } } /// /// Raised when the Anchor state of the transform is changed. /// [ByRefEvent] public readonly struct AnchorStateChangedEvent { public readonly TransformComponent Transform; public EntityUid Entity => Transform.Owner; public bool Anchored => Transform.Anchored; /// /// If true, the entity is being detached to null-space /// public readonly bool Detaching; public AnchorStateChangedEvent(TransformComponent transform, bool detaching = false) { Detaching = detaching; Transform = transform; } } /// /// Raised when an entity is re-anchored to another grid. /// [ByRefEvent] public readonly struct ReAnchorEvent { public readonly EntityUid Entity; public readonly EntityUid OldGrid; public readonly EntityUid Grid; public readonly TransformComponent Xform; /// /// Tile on both the old and new grid being re-anchored. /// public readonly Vector2i TilePos; public ReAnchorEvent(EntityUid uid, EntityUid oldGrid, EntityUid grid, Vector2i tilePos, TransformComponent xform) { Entity = uid; OldGrid = oldGrid; Grid = grid; TilePos = tilePos; Xform = xform; } } /// /// Data used to store information about the broad-phase that any given entity is currently on. /// /// /// A null value means that this entity is simply not on a broadphase (e.g., in null-space or in a container). /// An invalid entity UID indicates that this entity has intentionally been removed from broadphases and should /// not automatically be re-added by movement events. /// internal record struct BroadphaseData(EntityUid Uid, EntityUid PhysicsMap, bool CanCollide, bool Static) { public bool IsValid() => Uid.IsValid(); public bool Valid => IsValid(); public static readonly BroadphaseData Invalid = default; // TODO include MapId if ever grids are allowed to enter null-space (leave PVS). } }