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 readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly SharedContainerSystem _container = default!; private EntityQuery _gridQuery; private EntityQuery _metaQuery; protected EntityQuery XformQuery; private readonly Queue _gridMoves = new(); private readonly Queue _otherMoves = new(); public override void Initialize() { base.Initialize(); UpdatesOutsidePrediction = true; _gridQuery = GetEntityQuery(); _metaQuery = GetEntityQuery(); XformQuery = GetEntityQuery(); SubscribeLocalEvent(MapManagerOnTileChanged); SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnCompStartup); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnGridAdd); } private void MapManagerOnTileChanged(ref TileChangedEvent e) { if(e.NewTile.Tile != Tile.Empty) return; // TODO optimize this for when multiple tiles get empties simultaneously (e.g., explosions). DeparentAllEntsOnTile(e.NewTile.GridUid, e.NewTile.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) || !_mapManager.TryGetGrid(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.GetEntitiesIntersecting(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)) DetachParentToNull(entity, xform, gridXform); else SetParent(entity, xform, gridXform.MapUid.Value, mapTransform); } } public void DeferMoveEvent(ref MoveEvent moveEvent) { if (EntityManager.HasComponent(moveEvent.Sender)) _gridMoves.Enqueue(moveEvent); else _otherMoves.Enqueue(moveEvent); } public override void Update(float frameTime) { base.Update(frameTime); // Process grid moves first. Process(_gridMoves); Process(_otherMoves); void Process(Queue queue) { while (queue.TryDequeue(out var ev)) { if (EntityManager.Deleted(ev.Sender)) { continue; } // Hopefully we can remove this when PVS gets updated to not use NaNs if (!ev.NewPosition.IsValid(EntityManager)) { continue; } RaiseLocalEvent(ev.Sender, ref ev, true); } } } 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(!_mapManager.IsGrid(uid) && !_mapManager.IsMap(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, XformQuery.GetComponent(xform.GridUid.Value).InvLocalMatrix.Transform(worldPos)); } 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(!_mapManager.IsGrid(parentUid) && !_mapManager.IsMap(parentUid)); // Not parented to grid so convert their pos back to the grid. var worldPos = GetWorldMatrix(parentXform, XformQuery).Transform(coordinates.Position); return parentXform.GridUid == null ? new EntityCoordinates(mapId ?? parentUid, worldPos) : new EntityCoordinates(parentXform.GridUid.Value, XformQuery.GetComponent(parentXform.GridUid.Value).InvLocalMatrix.Transform(worldPos)); } /// /// 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(!_mapManager.IsGrid(uid) && !_mapManager.IsMap(uid)); var (pos, worldRot) = GetWorldPositionRotation(xform, XformQuery); var coords = xform.GridUid == null ? new EntityCoordinates(xform.MapUid ?? xform.ParentUid, pos) : new EntityCoordinates(xform.GridUid.Value, XformQuery.GetComponent(xform.GridUid.Value).InvLocalMatrix.Transform(pos)); 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; } } [ByRefEvent] public readonly struct TransformStartupEvent { public readonly TransformComponent Component; public TransformStartupEvent(TransformComponent component) { Component = component; } } /// /// Serialized state of a TransformComponent. /// [Serializable, NetSerializable] internal sealed class TransformComponentState : ComponentState { /// /// Current parent entity of this entity. /// public readonly NetEntity ParentID; /// /// 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; } } }