CatStation/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
TemporalOroboros 84b2f07157
The IMapManager is dead (#44002)
* Axes the easy IMapManager references

* Axes the references to unported (obsolete) IMapManager methods

* Converts salvageruler command to a LocalizedEntityCommand so it can use SharedMapSystem

* Removes IMapManager from GameTicker
This required axing the map nuke during restart, but that should be fine?
Maps are entities so EntityManager.FlushEntities should delete them all and that's the preceding line

* submodule update(?)

---------

Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
2026-07-01 20:44:54 -07:00

417 lines
15 KiB
C#

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.Station;
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 ISharedAdminLogManager _adminLogger = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private PullingSystem _pulling = default!;
[Dependency] private SharedMapSystem _map = default!;
[Dependency] private SharedStationSystem _stationSystem = null!;
[Dependency] private SharedToolSystem _tool = default!;
[Dependency] private SharedTransformSystem _transformSystem = default!;
[Dependency] private TagSystem _tagSystem = default!;
[Dependency] private SharedAppearanceSystem _appearance = default!;
[Dependency] private EntityQuery<PhysicsComponent> _physicsQuery = default!;
public readonly ProtoId<TagPrototype> Unstackable = "Unstackable";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnchorableComponent, InteractUsingEvent>(OnInteractUsing,
before: new[] { typeof(ItemSlotsSystem) }, after: new[] { typeof(SharedConstructionSystem) });
SubscribeLocalEvent<AnchorableComponent, TryAnchorCompletedEvent>(OnAnchorComplete);
SubscribeLocalEvent<AnchorableComponent, TryUnanchorCompletedEvent>(OnUnanchorComplete);
SubscribeLocalEvent<AnchorableComponent, ExaminedEvent>(OnAnchoredExamine);
SubscribeLocalEvent<AnchorableComponent, ComponentStartup>(OnAnchorStartup);
SubscribeLocalEvent<AnchorableComponent, AnchorStateChangedEvent>(OnAnchorStateChange);
SubscribeLocalEvent<AnchorOnlyOnStationComponent, AnchorAttemptEvent>(OnAnchorOnStation);
}
private void OnAnchorStartup(EntityUid uid, AnchorableComponent comp, ComponentStartup args)
{
_appearance.SetData(uid, AnchorVisuals.Anchored, Transform(uid).Anchored);
}
private void OnAnchorOnStation(Entity<AnchorOnlyOnStationComponent> ent, ref AnchorAttemptEvent args)
{
if (_stationSystem.IsOnStation(ent, ent.Comp.OnlyCountLargestGrid))
return;
args.FailMessage = Loc.GetString(ent.Comp.PopupMessageAnchorFail);
args.Cancel();
}
private void OnAnchorStateChange(EntityUid uid, AnchorableComponent comp, AnchorStateChangedEvent args)
{
_appearance.SetData(uid, AnchorVisuals.Anchored, args.Anchored);
}
/// <summary>
/// Tries to unanchor the entity.
/// </summary>
/// <returns>true if unanchored, false otherwise</returns>
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, out var failMessage))
{
if (failMessage != null)
_popup.PopupClient(failMessage, uid, userUid);
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<TransformComponent>(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<PhysicsComponent>(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<PullableComponent>(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);
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}"
);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>true if toggled, false otherwise</returns>
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);
}
}
/// <summary>
/// Tries to anchor the entity.
/// </summary>
/// <returns>true if anchored, false otherwise</returns>
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, out var failMessage, anchorable, usingTool))
{
if (failMessage != null)
_popup.PopupClient(Loc.GetString(failMessage), uid, userUid);
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 (!CanAnchorAt(uid, transform.Coordinates, 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,
out string? failMessage,
AnchorableComponent? anchorable = null,
ToolComponent? usingTool = null)
{
failMessage = 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;
failMessage = attempt.FailMessage;
return !attempt.Cancelled;
}
public bool CanAnchorAt(Entity<PhysicsComponent?> entity, EntityUid? user = null)
{
return CanAnchorAt(entity, Transform(entity).Coordinates, user);
}
public bool CanAnchorAt(Entity<PhysicsComponent?> entity, EntityCoordinates coordinates, EntityUid? user = null)
{
if (!Resolve(entity, ref entity.Comp))
return true;
if (TileFree(coordinates, entity.Comp))
return true;
_popup.PopupClient(Loc.GetString("anchorable-occupied"), entity, user);
return false;
}
/// <summary>
/// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body.
/// </summary>
public bool TileFree(EntityCoordinates coordinates, PhysicsComponent anchorBody)
{
// Probably ignore CanCollide on the anchoring body?
var gridUid = _transformSystem.GetGrid(coordinates);
if (!TryComp<MapGridComponent>(gridUid, out var grid))
return false;
var tileIndices = _map.TileIndicesFor((gridUid.Value, grid), coordinates);
return TileFree((gridUid.Value, grid), tileIndices, anchorBody.CollisionLayer, anchorBody.CollisionMask);
}
/// <summary>
/// Returns true if no hard anchored entities match the collision layer or mask specified.
/// </summary>
/// <param name="grid"></param>
public bool TileFree(Entity<MapGridComponent> 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<MapGridComponent> version")]
public bool TileFree(MapGridComponent grid, Vector2i gridIndices, int collisionLayer = 0, int collisionMask = 0)
{
return TileFree((grid.Owner, grid), gridIndices, collisionLayer, collisionMask);
}
/// <summary>
/// Returns true if any unstackables are also on the corresponding tile.
/// </summary>
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<MapGridComponent>(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
}