using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics.Components; using Robust.Shared.Utility; using System; using System.Numerics; namespace Robust.Shared.Containers; public abstract partial class SharedContainerSystem { /// /// Attempts to insert the entity into this container. /// /// /// If the insertion is successful, the inserted entity will end up parented to the /// container entity, and the inserted entity's local position will be set to the zero vector. /// /// The entity to insert. /// The container to insert into. /// The container's transform component. /// Whether to bypass normal insertion checks. /// False if the entity could not be inserted. /// /// Thrown if this container is a child of the entity, /// which would cause infinite loops. /// public bool Insert(Entity toInsert, BaseContainer container, TransformComponent? containerXform = null, bool force = false) { var (uid, transform, meta, physics) = toInsert; // Cannot Use Resolve(ref toInsert) as the physics component is optional if (!Resolve(uid, ref transform, ref meta)) return false; DebugTools.AssertOwner(container.Owner, containerXform); DebugTools.AssertOwner(toInsert, physics); DebugTools.Assert(!container.ExpectedEntities.Contains(GetNetEntity(toInsert)), "entity is expected"); DebugTools.Assert(container.Manager.Containers.ContainsKey(container.ID), "manager does not own the container"); // If someone is attempting to insert an entity into a container that is getting deleted, then we will // automatically delete that entity. I.e., the insertion automatically "succeeds" and both entities get deleted. // This is consistent with what happens if you attempt to attach an entity to a terminating parent. if (!TryComp(container.Owner, out MetaDataComponent? ownerMeta)) { Log.Error($"Attempted to insert an entity {ToPrettyString(toInsert)} into a non-existent entity. Trace: {Environment.StackTrace}"); QueueDel(toInsert); return false; } if (ownerMeta.EntityLifeStage >= EntityLifeStage.Terminating) { Log.Error($"Attempted to insert an entity {ToPrettyString(toInsert)} into an entity that is terminating. Entity: {ToPrettyString(container.Owner)}. Trace: {Environment.StackTrace}"); QueueDel(toInsert); return false; } //Verify we can insert into this container if (!force && !CanInsert(uid, container, containerXform: containerXform)) return false; if (meta.EntityLifeStage >= EntityLifeStage.Terminating) { Log.Error($"Attempted to insert a terminating entity {ToPrettyString(uid)} into a container {container.ID} in entity: {ToPrettyString(container.Owner)}. Trace: {Environment.StackTrace}"); return false; } // remove from any old containers. if ((meta.Flags & MetaDataFlags.InContainer) != 0 && TryComp(transform.ParentUid, out ContainerManagerComponent? oldManager) && TryGetContainingContainer(transform.ParentUid, toInsert, out var oldContainer, oldManager) && !Remove((uid, transform, meta), oldContainer, reparent: false, force: false)) { // failed to remove from container --> cannot insert. return false; } // Update metadata first, so that parent change events can check IsInContainer. DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) == 0, "invalid metadata flags before insertion"); meta.Flags |= MetaDataFlags.InContainer; // Remove the entity and any children from broadphases. // This is done before changing can collide to avoid unecceary updates. // TODO maybe combine with RecursivelyUpdatePhysics to avoid fetching components and iterating parents twice? _lookup.RemoveFromEntityTree(toInsert, transform); DebugTools.Assert(transform.Broadphase == null || !transform.Broadphase.Value.IsValid(), "invalid broadphase"); // Avoid unnecessary broadphase updates while unanchoring, changing physics collision, and re-parenting. var old = transform.Broadphase; transform.Broadphase = BroadphaseData.Invalid; // Unanchor the entity (without changing physics body types). _transform.Unanchor(toInsert, transform, false); // Next, update physics. Note that this cannot just be done in the physics system via parent change events, // because the insertion may not result in a parent change. This could alternatively be done via a // got-inserted event, but really that event should run after the entity was actually inserted (so that // parent/map have updated). But we are better of disabling collision before doing map/parent changes. PhysicsQuery.Resolve(toInsert, ref physics, logMissing: false); RecursivelyUpdatePhysics((toInsert, transform, physics)); // Attach to new parent var oldParent = transform.ParentUid; _transform.SetCoordinates(toInsert, transform, new EntityCoordinates(container.Owner, Vector2.Zero), Angle.Zero); transform.Broadphase = old; // the transform.AttachParent() could previously result in the flag being unset, so check that this hasn't happened. DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after insertion"); // Implementation specific insert logic container.InternalInsert(toInsert, EntityManager); // Update any relevant joint relays // Can't be done above as the container flag isn't set yet. RecursivelyUpdateJoints((toInsert, transform)); // Raise container events (after re-parenting and internal remove). RaiseLocalEvent(container.Owner, new EntInsertedIntoContainerMessage(toInsert, oldParent, container), true); RaiseLocalEvent(toInsert, new EntGotInsertedIntoContainerMessage(toInsert, container), true); // The sheer number of asserts tells you about how little I trust container and parenting code. DebugTools.Assert((meta.Flags & MetaDataFlags.InContainer) != 0, "invalid metadata flags after events"); DebugTools.Assert(!transform.Anchored, "entity is anchored"); DebugTools.AssertEqual(transform.ParentUid, container.Owner, "Wrong parent"); DebugTools.AssertEqual(transform.LocalPosition, Vector2.Zero); DebugTools.Assert(MathHelper.CloseTo(transform.LocalRotation.Theta, Angle.Zero), "Angle is not zero"); DebugTools.Assert(!PhysicsQuery.TryGetComponent(toInsert, out var phys) || (!phys.Awake && !phys.CanCollide), "Invalid physics"); Dirty(container.Owner, container.Manager); return true; } /// /// Attempts to insert an entity into a container. If it fails, it will instead drop the entity next to the /// container entity. /// /// Whether or not the entity was successfully inserted public bool InsertOrDrop(Entity toInsert, BaseContainer container, TransformComponent? containerXform = null) { if (!Resolve(toInsert.Owner, ref toInsert.Comp1) || !Resolve(container.Owner, ref containerXform)) return false; if (Insert(toInsert, container, containerXform)) return true; _transform.DropNextTo(toInsert, (container.Owner, containerXform)); return false; } /// /// Checks if the entity can be inserted into the given container. /// /// If true, this will check whether the entity could be inserted if the container were /// empty. public bool CanInsert( EntityUid toInsert, BaseContainer container, bool assumeEmpty = false, TransformComponent? containerXform = null) { if (container.Owner == toInsert) return false; if (!assumeEmpty && container.Contains(toInsert)) return false; if (!container.CanInsert(toInsert, assumeEmpty, EntityManager)) return false; // no, you can't put maps or grids into containers if (_mapQuery.HasComponent(toInsert) || _gridQuery.HasComponent(toInsert)) return false; // Prevent circular insertion. if (_transform.ContainsEntity(toInsert, (container.Owner, containerXform))) return false; var insertAttemptEvent = new ContainerIsInsertingAttemptEvent(container, toInsert, assumeEmpty); RaiseLocalEvent(container.Owner, insertAttemptEvent, true); if (insertAttemptEvent.Cancelled) return false; var gettingInsertedAttemptEvent = new ContainerGettingInsertedAttemptEvent(container, toInsert, assumeEmpty); RaiseLocalEvent(toInsert, gettingInsertedAttemptEvent, true); return !gettingInsertedAttemptEvent.Cancelled; } private void RecursivelyUpdatePhysics(Entity entity) { if (entity.Comp2 is { } physics) { // TODO CONTAINER // Is this actually needed? // I.e., shouldn't this just do a if (_timing.ApplyingState) return // Here we intentionally don't dirty the physics comp. Client-side state handling will apply these same // changes. This also ensures that the server doesn't have to send the physics comp state to every // player for any entity inside of a container during init. _physics.SetLinearVelocity(entity, Vector2.Zero, false, body: physics); _physics.SetAngularVelocity(entity, 0, false, body: physics); _physics.SetCanCollide(entity, false, false, body: physics); } foreach (var child in entity.Comp1._children) { var childXform = TransformQuery.GetComponent(child); PhysicsQuery.TryGetComponent(child, out var childPhysics); RecursivelyUpdatePhysics((child, childXform, childPhysics)); } } internal void RecursivelyUpdateJoints(Entity entity) { if (_timing.ApplyingState) return; if (JointQuery.TryGetComponent(entity, out var jointComp)) { // TODO: This is going to be going up while joints going down, although these aren't too common // in SS14 atm. _joint.RefreshRelay(entity, jointComp); } foreach (var child in entity.Comp._children) { var childXform = TransformQuery.GetComponent(child); RecursivelyUpdateJoints((child, childXform)); } } }