using Robust.Shared.GameStates; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Serialization; using Robust.Shared.Utility; using System; using System.Collections.Generic; using System.Numerics; using Robust.Shared.Containers; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; namespace Robust.Shared.GameObjects { public abstract partial class SharedTransformSystem : EntitySystem { [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private EntityLookupSystem _lookup = default!; [Dependency] private SharedMapSystem _map = default!; [Dependency] private MetaDataSystem _metaData = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private INetManager _netMan = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedGridTraversalSystem _traversal = default!; private EntityQuery _mapQuery; private EntityQuery _gridQuery; private EntityQuery _metaQuery; protected EntityQuery XformQuery; public delegate void MoveEventHandler(ref MoveEvent ev); /// /// Invoked as an alternative to broadcasting move events, which can be expensive. /// Systems which want to subscribe broadcast to (which you probably shouldn't) /// should subscribe to this instead /// public event MoveEventHandler? OnGlobalMoveEvent; /// /// Internal move event handlers. This gets invoked before the global & directed move events. This is mainly /// for exception tolerance, we want to ensure that PVS, physics & entity lookups get updated before some /// content code throws an exception. /// internal event MoveEventHandler? OnBeforeMoveEvent; public override void Initialize() { base.Initialize(); UpdatesOutsidePrediction = true; _mapQuery = GetEntityQuery(); _gridQuery = GetEntityQuery(); _metaQuery = GetEntityQuery(); XformQuery = GetEntityQuery(); SubscribeLocalEvent(MapManagerOnTileChanged); SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnCompStartup); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnGridAdd); } private void MapManagerOnTileChanged(ref TileChangedEvent e) { foreach (var change in e.Changes) { if(change.NewTile != Tile.Empty) continue; // TODO optimize this for when multiple tiles get empties simultaneously (e.g., explosions). DeparentAllEntsOnTile(e.Entity, change.GridIndices); } } /// /// De-parents and unanchors all entities on a grid-tile. /// /// /// Used when a tile on a grid is removed (becomes space). Only de-parents entities if they are actually /// parented to that grid. No more disemboweling mobs. /// private void DeparentAllEntsOnTile(EntityUid gridId, Vector2i tileIndices) { if (!TryComp(gridId, out BroadphaseComponent? lookup) || !TryComp(gridId, out var grid)) return; if (!XformQuery.TryGetComponent(gridId, out var gridXform)) return; if (!XformQuery.TryGetComponent(gridXform.MapUid, out var mapTransform)) return; var aabb = _lookup.GetLocalBounds(tileIndices, grid.TileSize); foreach (var entity in _lookup.GetLocalEntitiesIntersecting(lookup, aabb, LookupFlags.Uncontained | LookupFlags.Approximate)) { if (!XformQuery.TryGetComponent(entity, out var xform) || xform.ParentUid != gridId) continue; if (!aabb.Contains(xform.LocalPosition)) continue; // If a tile is being removed due to an explosion or somesuch, some entities are likely being deleted. // Avoid unnecessary entity updates. if (EntityManager.IsQueuedForDeletion(entity)) DetachEntity(entity, xform, MetaData(entity), gridXform); else SetParent(entity, xform, gridXform.MapUid.Value, mapTransform); } } public EntityCoordinates GetMoverCoordinates(EntityUid uid) { return GetMoverCoordinates(uid, XformQuery.GetComponent(uid)); } public EntityCoordinates GetMoverCoordinates(EntityUid uid, TransformComponent xform) { // Nullspace (or map) if (!xform.ParentUid.IsValid()) return xform.Coordinates; // GriddUid is only set after init. if (!xform._gridInitialized) InitializeGridUid(uid, xform); // Is the entity directly parented to the grid? if (xform.GridUid == xform.ParentUid) return xform.Coordinates; DebugTools.Assert(!_gridQuery.HasComp(uid) && !_mapQuery.HasComp(uid)); // Not parented to grid so convert their pos back to the grid. var worldPos = GetWorldPosition(xform, XformQuery); return xform.GridUid == null ? new EntityCoordinates(xform.MapUid ?? xform.ParentUid, worldPos) : new EntityCoordinates(xform.GridUid.Value, Vector2.Transform(worldPos, XformQuery.GetComponent(xform.GridUid.Value).InvLocalMatrix)); } public EntityCoordinates GetMoverCoordinates(EntityCoordinates coordinates, EntityQuery xformQuery) { return GetMoverCoordinates(coordinates); } /// /// Variant of that uses a entity coordinates, rather than an entity's transform. /// public EntityCoordinates GetMoverCoordinates(EntityCoordinates coordinates) { var parentUid = coordinates.EntityId; // Nullspace coordinates? if (!parentUid.IsValid()) return coordinates; var parentXform = XformQuery.GetComponent(parentUid); // GriddUid is only set after init. if (!parentXform._gridInitialized) InitializeGridUid(parentUid, parentXform); // Is the entity directly parented to the grid? if (parentXform.GridUid == parentUid) return coordinates; // Is the entity directly parented to the map? var mapId = parentXform.MapUid; if (mapId == parentUid) return coordinates; DebugTools.Assert(!HasComp(parentUid) && !HasComp(parentUid)); // Not parented to grid so convert their pos back to the grid. var worldPos = Vector2.Transform(coordinates.Position, GetWorldMatrix(parentXform, XformQuery)); return parentXform.GridUid == null ? new EntityCoordinates(mapId ?? parentUid, worldPos) : new EntityCoordinates(parentXform.GridUid.Value, Vector2.Transform(worldPos, XformQuery.GetComponent(parentXform.GridUid.Value).InvLocalMatrix)); } /// /// Variant of that also returns the entity's world rotation /// public (EntityCoordinates Coords, Angle worldRot) GetMoverCoordinateRotation(EntityUid uid, TransformComponent xform) { // Nullspace (or map) if (!xform.ParentUid.IsValid()) return (xform.Coordinates, xform.LocalRotation); // GriddUid is only set after init. if (!xform._gridInitialized) InitializeGridUid(uid, xform); // Is the entity directly parented to the grid? if (xform.GridUid == xform.ParentUid) return (xform.Coordinates, GetWorldRotation(xform, XformQuery)); DebugTools.Assert(!HasComp(uid) && !HasComp(uid)); var (pos, worldRot) = GetWorldPositionRotation(xform, XformQuery); var coords = xform.GridUid == null ? new EntityCoordinates(xform.MapUid ?? xform.ParentUid, pos) : new EntityCoordinates(xform.GridUid.Value, Vector2.Transform(pos, XformQuery.GetComponent(xform.GridUid.Value).InvLocalMatrix)); return (coords, worldRot); } /// /// Helper method that returns the grid or map tile an entity is on. /// public Vector2i GetGridOrMapTilePosition(EntityUid uid, TransformComponent? xform = null) { if(!Resolve(uid, ref xform, false)) return Vector2i.Zero; // Fast path, we're not on a grid. if (xform.GridUid == null) return GetWorldPosition(xform).Floored(); // We're on a grid, need to convert the coordinates to grid tiles. return _map.CoordinatesToTile(xform.GridUid.Value, Comp(xform.GridUid.Value), xform.Coordinates); } /// /// Helper method that returns the grid tile an entity is on. /// public Vector2i GetGridTilePositionOrDefault(Entity entity, MapGridComponent? grid = null) { var xform = entity.Comp; if(!Resolve(entity.Owner, ref xform) || xform.GridUid == null) return Vector2i.Zero; if (!Resolve(xform.GridUid.Value, ref grid)) return Vector2i.Zero; return _map.CoordinatesToTile(xform.GridUid.Value, grid, xform.Coordinates); } /// /// Helper method that returns the grid tile an entity is on. /// public bool TryGetGridTilePosition(Entity entity, out Vector2i indices, MapGridComponent? grid = null) { indices = default; var xform = entity.Comp; if(!Resolve(entity.Owner, ref xform) || xform.GridUid == null) return false; if (!Resolve(xform.GridUid.Value, ref grid)) return false; indices = _map.CoordinatesToTile(xform.GridUid.Value, grid, xform.Coordinates); return true; } internal void RaiseMoveEvent( Entity ent, EntityUid oldParent, Vector2 oldPosition, Angle oldRotation, EntityUid? oldMap, bool checkTraversal = true) { var pos = ent.Comp1._parent == EntityUid.Invalid ? default : new EntityCoordinates(ent.Comp1._parent, ent.Comp1._localPosition); var oldPos = oldParent == EntityUid.Invalid ? default : new EntityCoordinates(oldParent, oldPosition); var ev = new MoveEvent(ent, oldPos, pos, oldRotation, ent.Comp1._localRotation); if (oldParent != ent.Comp1._parent) { _physics.OnParentChange(ent, oldParent, oldMap); OnBeforeMoveEvent?.Invoke(ref ev); var entParentChangedMessage = new EntParentChangedMessage(ev.Sender, oldParent, oldMap, ev.Component); RaiseLocalEvent(ev.Sender, ref entParentChangedMessage, true); } else { OnBeforeMoveEvent?.Invoke(ref ev); } RaiseLocalEvent(ev.Sender, ref ev); OnGlobalMoveEvent?.Invoke(ref ev); // Finally, handle grid traversal. This is handled separately to avoid out-of-order move events. // I.e., if the traversal raises its own move event, this ensures that all the old move event handlers // have finished running first. Ideally this shouldn't be required, but this is here just in case if (checkTraversal) { _traversal.CheckTraverse(ent); } } } [ByRefEvent] public readonly struct TransformStartupEvent(Entity entity) { public readonly Entity Entity = entity; public TransformComponent Component => Entity.Comp; } /// /// Serialized state of a TransformComponent. /// [Serializable, NetSerializable] internal readonly record struct TransformComponentState : IComponentState { /// /// Current parent entity of this entity. /// public readonly NetEntity ParentID; // TODO Delta-states // If the transform component ever gets delta states, then the client state manager needs to be updated. // Currently it explicitly looks for a "TransformComponentState" when determining an entity's parent for the // sake of sorting the states that need to be applied base on the transform hierarchy. /// /// Current position offset of the entity. /// public readonly Vector2 LocalPosition; /// /// Current rotation offset of the entity. /// public readonly Angle Rotation; /// /// Is the transform able to be locally rotated? /// public readonly bool NoLocalRotation; /// /// True if the transform is anchored to a tile. /// public readonly bool Anchored; /// /// Constructs a new state snapshot of a TransformComponent. /// /// Current position offset of this entity. /// Current direction offset of this entity. /// Current parent transform of this entity. /// public TransformComponentState(Vector2 localPosition, Angle rotation, NetEntity parentId, bool noLocalRotation, bool anchored) { LocalPosition = localPosition; Rotation = rotation; ParentID = parentId; NoLocalRotation = noLocalRotation; Anchored = anchored; } } }