using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; using Content.Shared.Examine; using Content.Shared.Construction.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Coordinates.Helpers; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Interaction; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Popups; using Content.Shared.Tools.Components; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; using Content.Shared.Tag; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Utility; using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem; namespace Content.Shared.Construction.EntitySystems; public sealed partial class AnchorableSystem : EntitySystem { [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedToolSystem _tool = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; private EntityQuery _physicsQuery; public readonly ProtoId Unstackable = "Unstackable"; public override void Initialize() { base.Initialize(); _physicsQuery = GetEntityQuery(); SubscribeLocalEvent(OnInteractUsing, before: new[] { typeof(ItemSlotsSystem) }, after: new[] { typeof(SharedConstructionSystem) }); SubscribeLocalEvent(OnAnchorComplete); SubscribeLocalEvent(OnUnanchorComplete); SubscribeLocalEvent(OnAnchoredExamine); SubscribeLocalEvent(OnAnchorStartup); SubscribeLocalEvent(OnAnchorStateChange); } private void OnAnchorStartup(EntityUid uid, AnchorableComponent comp, ComponentStartup args) { _appearance.SetData(uid, AnchorVisuals.Anchored, Transform(uid).Anchored); } private void OnAnchorStateChange(EntityUid uid, AnchorableComponent comp, AnchorStateChangedEvent args) { _appearance.SetData(uid, AnchorVisuals.Anchored, args.Anchored); } /// /// Tries to unanchor the entity. /// /// true if unanchored, false otherwise private void TryUnAnchor(EntityUid uid, EntityUid userUid, EntityUid usingUid, AnchorableComponent? anchorable = null, TransformComponent? transform = null, ToolComponent? usingTool = null) { if (!Resolve(uid, ref anchorable, ref transform)) return; if (!Resolve(usingUid, ref usingTool)) return; if (!Valid(uid, userUid, usingUid, false)) return; // Log unanchor attempt (server only) _adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to unanchor {ToPrettyString(uid):entity} from {transform.Coordinates:targetlocation}"); _tool.UseTool(usingUid, userUid, uid, anchorable.Delay, usingTool.Qualities, new TryUnanchorCompletedEvent()); } private void OnInteractUsing(EntityUid uid, AnchorableComponent anchorable, InteractUsingEvent args) { if (args.Handled) return; // If the used entity doesn't have a tool, return early. if (!TryComp(args.Used, out ToolComponent? usedTool) || !_tool.HasQuality(args.Used, anchorable.Tool, usedTool)) return; args.Handled = true; TryToggleAnchor(uid, args.User, args.Used, anchorable, usingTool: usedTool); } private void OnAnchoredExamine(EntityUid uid, AnchorableComponent component, ExaminedEvent args) { var isAnchored = Comp(uid).Anchored; if (isAnchored && (component.Flags & AnchorableFlags.Unanchorable) == 0x0) return; if (!isAnchored && (component.Flags & AnchorableFlags.Anchorable) == 0x0) return; var messageId = isAnchored ? "examinable-anchored" : "examinable-unanchored"; args.PushMarkup(Loc.GetString(messageId, ("target", uid))); } private void OnUnanchorComplete(EntityUid uid, AnchorableComponent component, TryUnanchorCompletedEvent args) { if (args.Cancelled || args.Used is not { } used) return; var xform = Transform(uid); RaiseLocalEvent(uid, new BeforeUnanchoredEvent(args.User, used)); _transformSystem.Unanchor(uid, xform); RaiseLocalEvent(uid, new UserUnanchoredEvent(args.User, used)); _popup.PopupClient(Loc.GetString("anchorable-unanchored"), uid, args.User); _adminLogger.Add( LogType.Unanchor, LogImpact.Low, $"{ToPrettyString(args.User):user} unanchored {ToPrettyString(uid):anchored} using {ToPrettyString(used):using}" ); } private void OnAnchorComplete(EntityUid uid, AnchorableComponent component, TryAnchorCompletedEvent args) { if (args.Cancelled || args.Used is not { } used) return; var xform = Transform(uid); if (TryComp(uid, out var anchorBody) && !TileFree(xform.Coordinates, anchorBody)) { _popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, args.User); return; } // Snap rotation to cardinal (multiple of 90) var rot = xform.LocalRotation; xform.LocalRotation = Math.Round(rot / (Math.PI / 2)) * (Math.PI / 2); if (TryComp(uid, out var pullable) && pullable.Puller != null) { _pulling.TryStopPull(uid, pullable); } // TODO: Anchoring snaps rn anyway! if (component.Snap) { var coordinates = xform.Coordinates.SnapToGrid(EntityManager, _mapManager); if (AnyUnstackable(uid, coordinates)) { _popup.PopupClient(Loc.GetString("construction-step-condition-no-unstackable-in-tile"), uid, args.User); return; } _transformSystem.SetCoordinates(uid, coordinates); } RaiseLocalEvent(uid, new BeforeAnchoredEvent(args.User, used)); if (!xform.Anchored) _transformSystem.AnchorEntity(uid, xform); RaiseLocalEvent(uid, new UserAnchoredEvent(args.User, used)); _popup.PopupClient(Loc.GetString("anchorable-anchored"), uid, args.User); _adminLogger.Add( LogType.Anchor, LogImpact.Low, $"{ToPrettyString(args.User):user} anchored {ToPrettyString(uid):anchored} using {ToPrettyString(used):using}" ); } /// /// Tries to toggle the anchored status of this component's owner. /// override is used due to popup and adminlog being server side systems in this case. /// /// true if toggled, false otherwise public void TryToggleAnchor(EntityUid uid, EntityUid userUid, EntityUid usingUid, AnchorableComponent? anchorable = null, TransformComponent? transform = null, PullableComponent? pullable = null, ToolComponent? usingTool = null) { if (!Resolve(uid, ref transform)) return; if (transform.Anchored) { TryUnAnchor(uid, userUid, usingUid, anchorable, transform, usingTool); } else { TryAnchor(uid, userUid, usingUid, anchorable, transform, pullable, usingTool); } } /// /// Tries to anchor the entity. /// /// true if anchored, false otherwise private void TryAnchor(EntityUid uid, EntityUid userUid, EntityUid usingUid, AnchorableComponent? anchorable = null, TransformComponent? transform = null, PullableComponent? pullable = null, ToolComponent? usingTool = null) { if (!Resolve(uid, ref anchorable, ref transform)) return; // Optional resolves. Resolve(uid, ref pullable, false); if (!Resolve(usingUid, ref usingTool)) return; if (!Valid(uid, userUid, usingUid, true, anchorable, usingTool)) return; // Log anchor attempt (server only) _adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to anchor {ToPrettyString(uid):entity} to {transform.Coordinates:targetlocation}"); if (TryComp(uid, out var anchorBody) && !TileFree(transform.Coordinates, anchorBody)) { _popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, userUid); return; } if (AnyUnstackable(uid, transform.Coordinates)) { _popup.PopupClient(Loc.GetString("construction-step-condition-no-unstackable-in-tile"), uid, userUid); return; } _tool.UseTool(usingUid, userUid, uid, anchorable.Delay, usingTool.Qualities, new TryAnchorCompletedEvent()); } private bool Valid( EntityUid uid, EntityUid userUid, EntityUid usingUid, bool anchoring, AnchorableComponent? anchorable = null, ToolComponent? usingTool = null) { if (!Resolve(uid, ref anchorable)) return false; if (!Resolve(usingUid, ref usingTool)) return false; if (anchoring && (anchorable.Flags & AnchorableFlags.Anchorable) == 0x0) return false; if (!anchoring && (anchorable.Flags & AnchorableFlags.Unanchorable) == 0x0) return false; BaseAnchoredAttemptEvent attempt = anchoring ? new AnchorAttemptEvent(userUid, usingUid) : new UnanchorAttemptEvent(userUid, usingUid); // Need to cast the event or it will be raised as BaseAnchoredAttemptEvent. if (anchoring) RaiseLocalEvent(uid, (AnchorAttemptEvent)attempt); else RaiseLocalEvent(uid, (UnanchorAttemptEvent)attempt); anchorable.Delay += attempt.Delay; return !attempt.Cancelled; } /// /// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body. /// public bool TileFree(EntityCoordinates coordinates, PhysicsComponent anchorBody) { // Probably ignore CanCollide on the anchoring body? var gridUid = _transformSystem.GetGrid(coordinates); if (!TryComp(gridUid, out var grid)) return false; var tileIndices = _map.TileIndicesFor((gridUid.Value, grid), coordinates); return TileFree((gridUid.Value, grid), tileIndices, anchorBody.CollisionLayer, anchorBody.CollisionMask); } /// /// Returns true if no hard anchored entities match the collision layer or mask specified. /// /// public bool TileFree(Entity grid, Vector2i gridIndices, int collisionLayer = 0, int collisionMask = 0) { var enumerator = _map.GetAnchoredEntitiesEnumerator(grid, grid.Comp, gridIndices); while (enumerator.MoveNext(out var ent)) { if (!_physicsQuery.TryGetComponent(ent, out var body) || !body.CanCollide || !body.Hard) { continue; } if ((body.CollisionMask & collisionLayer) != 0x0 || (body.CollisionLayer & collisionMask) != 0x0) { return false; } } return true; } [Obsolete("Use the Entity version")] public bool TileFree(MapGridComponent grid, Vector2i gridIndices, int collisionLayer = 0, int collisionMask = 0) { return TileFree((grid.Owner, grid), gridIndices, collisionLayer, collisionMask); } /// /// Returns true if any unstackables are also on the corresponding tile. /// public bool AnyUnstackable(EntityUid uid, EntityCoordinates location) { DebugTools.Assert(!Transform(uid).Anchored); // If we are unstackable, iterate through any other entities anchored on the current square return _tagSystem.HasTag(uid, Unstackable) && AnyUnstackablesAnchoredAt(location); } public bool AnyUnstackablesAnchoredAt(EntityCoordinates location) { var gridUid = _transformSystem.GetGrid(location); if (!TryComp(gridUid, out var grid)) return false; var enumerator = _map.GetAnchoredEntitiesEnumerator(gridUid.Value, grid, _map.LocalToTile(gridUid.Value, grid, location)); while (enumerator.MoveNext(out var entity)) { // If we find another unstackable here, return true. if (_tagSystem.HasTag(entity.Value, Unstackable)) return true; } return false; } [Serializable, NetSerializable] private sealed partial class TryUnanchorCompletedEvent : SimpleDoAfterEvent { } [Serializable, NetSerializable] private sealed partial class TryAnchorCompletedEvent : SimpleDoAfterEvent { } } [Serializable, NetSerializable] public enum AnchorVisuals : byte { Anchored }