2023-06-29 08:35:54 -04:00
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 ;
2024-03-19 14:30:56 +11:00
using Content.Shared.Movement.Pulling.Components ;
using Content.Shared.Movement.Pulling.Systems ;
2023-06-29 08:35:54 -04:00
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 ;
2024-06-02 11:11:19 +10:00
using Robust.Shared.Prototypes ;
2023-06-29 08:35:54 -04:00
using Robust.Shared.Serialization ;
using Robust.Shared.Utility ;
2023-10-24 00:20:33 +11:00
using SharedToolSystem = Content . Shared . Tools . Systems . SharedToolSystem ;
2023-06-29 08:35:54 -04:00
namespace Content.Shared.Construction.EntitySystems ;
2023-08-22 18:14:33 -07:00
public sealed partial class AnchorableSystem : EntitySystem
2023-06-29 08:35:54 -04:00
{
[Dependency] private readonly IMapManager _mapManager = default ! ;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
2024-03-19 14:30:56 +11:00
[Dependency] private readonly PullingSystem _pulling = default ! ;
2025-05-24 11:05:09 -04:00
[Dependency] private readonly SharedMapSystem _map = default ! ;
2023-06-29 08:35:54 -04:00
[Dependency] private readonly SharedToolSystem _tool = default ! ;
[Dependency] private readonly SharedTransformSystem _transformSystem = default ! ;
2025-05-24 11:05:09 -04:00
[Dependency] private readonly TagSystem _tagSystem = default ! ;
2024-12-11 05:50:20 -08:00
[Dependency] private readonly SharedAppearanceSystem _appearance = default ! ;
2023-06-29 08:35:54 -04:00
private EntityQuery < PhysicsComponent > _physicsQuery ;
2024-06-02 11:11:19 +10:00
public readonly ProtoId < TagPrototype > Unstackable = "Unstackable" ;
2023-06-29 08:35:54 -04:00
public override void Initialize ( )
{
base . Initialize ( ) ;
_physicsQuery = GetEntityQuery < PhysicsComponent > ( ) ;
SubscribeLocalEvent < AnchorableComponent , InteractUsingEvent > ( OnInteractUsing ,
before : new [ ] { typeof ( ItemSlotsSystem ) } , after : new [ ] { typeof ( SharedConstructionSystem ) } ) ;
SubscribeLocalEvent < AnchorableComponent , TryAnchorCompletedEvent > ( OnAnchorComplete ) ;
SubscribeLocalEvent < AnchorableComponent , TryUnanchorCompletedEvent > ( OnUnanchorComplete ) ;
SubscribeLocalEvent < AnchorableComponent , ExaminedEvent > ( OnAnchoredExamine ) ;
2024-12-11 05:50:20 -08:00
SubscribeLocalEvent < AnchorableComponent , ComponentStartup > ( OnAnchorStartup ) ;
SubscribeLocalEvent < AnchorableComponent , AnchorStateChangedEvent > ( 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 ) ;
2023-06-29 08:35:54 -04:00
}
/// <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 ) )
return ;
2024-02-29 09:00:12 +11:00
// 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}" ) ;
2023-06-29 08:35:54 -04:00
_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.
2024-06-02 22:28:53 -05:00
if ( ! TryComp ( args . Used , out ToolComponent ? usedTool ) | | ! _tool . HasQuality ( args . Used , anchorable . Tool , usedTool ) )
2023-06-29 08:35:54 -04:00
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 ;
2025-08-27 10:04:39 -05:00
if ( isAnchored & & ( component . Flags & AnchorableFlags . Unanchorable ) = = 0x0 )
return ;
if ( ! isAnchored & & ( component . Flags & AnchorableFlags . Anchorable ) = = 0x0 )
return ;
2023-06-29 08:35:54 -04:00
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 ) ) ;
2025-02-21 00:12:12 +11:00
_transformSystem . Unanchor ( uid , xform ) ;
2023-06-29 08:35:54 -04:00
RaiseLocalEvent ( uid , new UserUnanchoredEvent ( args . User , used ) ) ;
_popup . PopupClient ( Loc . GetString ( "anchorable-unanchored" ) , uid , args . User ) ;
_adminLogger . Add (
LogType . Unanchor ,
LogImpact . Low ,
2025-06-26 19:50:49 -04:00
$"{ToPrettyString(args.User):user} unanchored {ToPrettyString(uid):anchored} using {ToPrettyString(used):using}"
2023-06-29 08:35:54 -04:00
) ;
}
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 ;
2024-02-28 00:51:20 +11:00
xform . LocalRotation = Math . Round ( rot / ( Math . PI / 2 ) ) * ( Math . PI / 2 ) ;
2023-06-29 08:35:54 -04:00
2024-03-19 14:30:56 +11:00
if ( TryComp < PullableComponent > ( uid , out var pullable ) & & pullable . Puller ! = null )
2023-06-29 08:35:54 -04:00
{
2024-03-19 14:30:56 +11:00
_pulling . TryStopPull ( uid , pullable ) ;
2023-06-29 08:35:54 -04:00
}
// 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 )
2025-02-21 00:12:12 +11:00
_transformSystem . AnchorEntity ( uid , xform ) ;
2023-06-29 08:35:54 -04:00
RaiseLocalEvent ( uid , new UserAnchoredEvent ( args . User , used ) ) ;
_popup . PopupClient ( Loc . GetString ( "anchorable-anchored" ) , uid , args . User ) ;
_adminLogger . Add (
LogType . Anchor ,
LogImpact . Low ,
2025-06-26 19:50:49 -04:00
$"{ToPrettyString(args.User):user} anchored {ToPrettyString(uid):anchored} using {ToPrettyString(used):using}"
2023-06-29 08:35:54 -04:00
) ;
}
/// <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 ,
2024-03-19 14:30:56 +11:00
PullableComponent ? pullable = null ,
2023-06-29 08:35:54 -04:00
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 ,
2024-03-19 14:30:56 +11:00
PullableComponent ? pullable = null ,
2023-06-29 08:35:54 -04:00
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 ;
2024-02-29 09:00:12 +11:00
// 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}" ) ;
2023-06-29 08:35:54 -04:00
if ( TryComp < PhysicsComponent > ( 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 ;
2024-02-29 09:00:12 +11:00
if ( anchoring & & ( anchorable . Flags & AnchorableFlags . Anchorable ) = = 0x0 )
return false ;
if ( ! anchoring & & ( anchorable . Flags & AnchorableFlags . Unanchorable ) = = 0x0 )
return false ;
2023-06-29 08:35:54 -04:00
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 )
2025-05-24 11:05:09 -04:00
RaiseLocalEvent ( uid , ( AnchorAttemptEvent ) attempt ) ;
2023-06-29 08:35:54 -04:00
else
2025-05-24 11:05:09 -04:00
RaiseLocalEvent ( uid , ( UnanchorAttemptEvent ) attempt ) ;
2023-06-29 08:35:54 -04:00
anchorable . Delay + = attempt . Delay ;
return ! attempt . Cancelled ;
}
2025-04-19 04:17:13 +02:00
/// <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 )
2023-06-29 08:35:54 -04:00
{
// Probably ignore CanCollide on the anchoring body?
2025-04-16 11:02:41 +00:00
var gridUid = _transformSystem . GetGrid ( coordinates ) ;
2023-06-29 08:35:54 -04:00
2024-03-22 03:08:40 -04:00
if ( ! TryComp < MapGridComponent > ( gridUid , out var grid ) )
2023-06-29 08:35:54 -04:00
return false ;
2025-05-24 11:05:09 -04:00
var tileIndices = _map . TileIndicesFor ( ( gridUid . Value , grid ) , coordinates ) ;
return TileFree ( ( gridUid . Value , grid ) , tileIndices , anchorBody . CollisionLayer , anchorBody . CollisionMask ) ;
2023-06-29 08:35:54 -04:00
}
/// <summary>
/// Returns true if no hard anchored entities match the collision layer or mask specified.
/// </summary>
/// <param name="grid"></param>
2025-05-24 11:05:09 -04:00
public bool TileFree ( Entity < MapGridComponent > grid , Vector2i gridIndices , int collisionLayer = 0 , int collisionMask = 0 )
2023-06-29 08:35:54 -04:00
{
2025-05-24 11:05:09 -04:00
var enumerator = _map . GetAnchoredEntitiesEnumerator ( grid , grid . Comp , gridIndices ) ;
2023-06-29 08:35:54 -04:00
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 ;
}
2025-05-24 11:05:09 -04:00
[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 ) ;
}
2023-06-29 08:35:54 -04:00
/// <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
2024-06-02 11:11:19 +10:00
return _tagSystem . HasTag ( uid , Unstackable ) & & AnyUnstackablesAnchoredAt ( location ) ;
2023-06-29 08:35:54 -04:00
}
public bool AnyUnstackablesAnchoredAt ( EntityCoordinates location )
{
2025-04-16 11:02:41 +00:00
var gridUid = _transformSystem . GetGrid ( location ) ;
2023-06-29 08:35:54 -04:00
if ( ! TryComp < MapGridComponent > ( gridUid , out var grid ) )
return false ;
2025-05-24 11:05:09 -04:00
var enumerator = _map . GetAnchoredEntitiesEnumerator ( gridUid . Value , grid , _map . LocalToTile ( gridUid . Value , grid , location ) ) ;
2023-06-29 08:35:54 -04:00
while ( enumerator . MoveNext ( out var entity ) )
{
// If we find another unstackable here, return true.
2024-06-02 11:11:19 +10:00
if ( _tagSystem . HasTag ( entity . Value , Unstackable ) )
2023-06-29 08:35:54 -04:00
return true ;
}
return false ;
}
[Serializable, NetSerializable]
2023-08-22 18:14:33 -07:00
private sealed partial class TryUnanchorCompletedEvent : SimpleDoAfterEvent
2023-06-29 08:35:54 -04:00
{
}
[Serializable, NetSerializable]
2023-08-22 18:14:33 -07:00
private sealed partial class TryAnchorCompletedEvent : SimpleDoAfterEvent
2023-06-29 08:35:54 -04:00
{
}
}
2024-12-11 05:50:20 -08:00
[Serializable, NetSerializable]
public enum AnchorVisuals : byte
{
Anchored
}