2022-02-26 18:24:08 +13:00
using Content.Shared.ActionBlocker ;
using Content.Shared.Actions.ActionTypes ;
using Content.Shared.Administration.Logs ;
using Content.Shared.Database ;
using Content.Shared.Hands ;
using Content.Shared.Interaction ;
using Content.Shared.Inventory.Events ;
using Robust.Shared.Containers ;
using Robust.Shared.GameStates ;
using Robust.Shared.Map ;
2022-04-14 16:17:34 +12:00
using Robust.Shared.Prototypes ;
2022-02-26 18:24:08 +13:00
using Robust.Shared.Timing ;
using System.Linq ;
namespace Content.Shared.Actions ;
public abstract class SharedActionsSystem : EntitySystem
{
2022-10-04 14:24:19 +11:00
[Dependency] protected readonly IGameTiming GameTiming = default ! ;
2022-05-28 23:41:17 -07:00
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2022-02-26 18:24:08 +13:00
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default ! ;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default ! ;
[Dependency] private readonly SharedContainerSystem _containerSystem = default ! ;
[Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default ! ;
2022-10-04 14:24:19 +11:00
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
2023-05-01 04:29:18 -04:00
[Dependency] private readonly SharedTransformSystem _transformSystem = default ! ;
2022-02-26 18:24:08 +13:00
public override void Initialize ( )
{
base . Initialize ( ) ;
SubscribeLocalEvent < ActionsComponent , DidEquipEvent > ( OnDidEquip ) ;
SubscribeLocalEvent < ActionsComponent , DidEquipHandEvent > ( OnHandEquipped ) ;
SubscribeLocalEvent < ActionsComponent , DidUnequipEvent > ( OnDidUnequip ) ;
SubscribeLocalEvent < ActionsComponent , DidUnequipHandEvent > ( OnHandUnequipped ) ;
SubscribeLocalEvent < ActionsComponent , ComponentGetState > ( GetState ) ;
SubscribeAllEvent < RequestPerformActionEvent > ( OnActionRequest ) ;
}
#region ComponentStateManagement
2022-04-23 15:31:45 +12:00
public virtual void Dirty ( ActionType action )
2022-02-26 18:24:08 +13:00
{
if ( action . AttachedEntity = = null )
return ;
if ( ! TryComp ( action . AttachedEntity , out ActionsComponent ? comp ) )
{
action . AttachedEntity = null ;
return ;
}
Dirty ( comp ) ;
}
public void SetToggled ( ActionType action , bool toggled )
{
if ( action . Toggled = = toggled )
return ;
action . Toggled = toggled ;
Dirty ( action ) ;
}
public void SetEnabled ( ActionType action , bool enabled )
{
if ( action . Enabled = = enabled )
return ;
action . Enabled = enabled ;
Dirty ( action ) ;
}
public void SetCharges ( ActionType action , int? charges )
{
if ( action . Charges = = charges )
return ;
action . Charges = charges ;
Dirty ( action ) ;
}
private void GetState ( EntityUid uid , ActionsComponent component , ref ComponentGetState args )
{
args . State = new ActionsComponentState ( component . Actions . ToList ( ) ) ;
}
#endregion
#region Execution
/// <summary>
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
2022-04-14 16:17:34 +12:00
/// will raise the relevant <see cref="InstantActionEvent"/>
2022-02-26 18:24:08 +13:00
/// </summary>
private void OnActionRequest ( RequestPerformActionEvent ev , EntitySessionEventArgs args )
{
2023-05-01 04:29:18 -04:00
if ( args . SenderSession . AttachedEntity is not { } user )
2022-02-26 18:24:08 +13:00
return ;
if ( ! TryComp ( user , out ActionsComponent ? component ) )
return ;
// Does the user actually have the requested action?
if ( ! component . Actions . TryGetValue ( ev . Action , out var act ) )
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2022-09-04 17:21:14 -07:00
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {ev.Action.DisplayName}." ) ;
2022-02-26 18:24:08 +13:00
return ;
}
if ( ! act . Enabled )
return ;
var curTime = GameTiming . CurTime ;
if ( act . Cooldown . HasValue & & act . Cooldown . Value . End > curTime )
return ;
2022-04-14 16:17:34 +12:00
BaseActionEvent ? performEvent = null ;
2022-02-26 18:24:08 +13:00
// Validate request by checking action blockers and the like:
2022-09-04 17:21:14 -07:00
var name = Loc . GetString ( act . DisplayName ) ;
2022-02-26 18:24:08 +13:00
switch ( act )
{
case EntityTargetAction entityAction :
2023-01-19 03:56:45 +01:00
if ( ev . EntityTarget is not { Valid : true } entityTarget )
2022-02-26 18:24:08 +13:00
{
2023-06-27 23:56:52 +10:00
Log . Error ( $"Attempted to perform an entity-targeted action without a target! Action: {entityAction.DisplayName}" ) ;
2022-02-26 18:24:08 +13:00
return ;
}
2023-05-01 04:29:18 -04:00
var targetWorldPos = _transformSystem . GetWorldPosition ( entityTarget ) ;
_rotateToFaceSystem . TryFaceCoordinates ( user , targetWorldPos ) ;
2022-02-26 18:24:08 +13:00
if ( ! ValidateEntityTarget ( user , entityTarget , entityAction ) )
return ;
if ( act . Provider = = null )
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2023-06-27 23:56:52 +10:00
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}." ) ;
}
2022-02-26 18:24:08 +13:00
else
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2023-06-27 23:56:52 +10:00
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {ToPrettyString(entityTarget):target}." ) ;
}
2022-02-26 18:24:08 +13:00
if ( entityAction . Event ! = null )
{
entityAction . Event . Target = entityTarget ;
performEvent = entityAction . Event ;
}
break ;
case WorldTargetAction worldAction :
2023-05-01 04:29:18 -04:00
if ( ev . EntityCoordinatesTarget is not { } entityCoordinatesTarget )
2022-02-26 18:24:08 +13:00
{
2023-06-27 23:56:52 +10:00
Log . Error ( $"Attempted to perform a world-targeted action without a target! Action: {worldAction.DisplayName}" ) ;
2022-02-26 18:24:08 +13:00
return ;
}
2022-12-06 18:03:20 -05:00
_rotateToFaceSystem . TryFaceCoordinates ( user , entityCoordinatesTarget . Position ) ;
2022-02-26 18:24:08 +13:00
2022-12-06 18:03:20 -05:00
if ( ! ValidateWorldTarget ( user , entityCoordinatesTarget , worldAction ) )
2022-02-26 18:24:08 +13:00
return ;
if ( act . Provider = = null )
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2022-12-06 18:03:20 -05:00
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {entityCoordinatesTarget:target}." ) ;
2023-06-27 23:56:52 +10:00
}
2022-02-26 18:24:08 +13:00
else
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2022-12-06 18:03:20 -05:00
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {entityCoordinatesTarget:target}." ) ;
2023-06-27 23:56:52 +10:00
}
2022-02-26 18:24:08 +13:00
if ( worldAction . Event ! = null )
{
2022-12-06 18:03:20 -05:00
worldAction . Event . Target = entityCoordinatesTarget ;
2022-02-26 18:24:08 +13:00
performEvent = worldAction . Event ;
}
break ;
case InstantAction instantAction :
if ( act . CheckCanInteract & & ! _actionBlockerSystem . CanInteract ( user , null ) )
return ;
if ( act . Provider = = null )
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2022-02-26 18:24:08 +13:00
$"{ToPrettyString(user):user} is performing the {name:action} action." ) ;
2023-06-27 23:56:52 +10:00
}
2022-02-26 18:24:08 +13:00
else
2023-06-27 23:56:52 +10:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2022-02-26 18:24:08 +13:00
$"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(act.Provider.Value):provider}." ) ;
2023-06-27 23:56:52 +10:00
}
2022-02-26 18:24:08 +13:00
performEvent = instantAction . Event ;
break ;
}
if ( performEvent ! = null )
performEvent . Performer = user ;
// All checks passed. Perform the action!
2023-01-02 13:01:40 +13:00
PerformAction ( user , component , act , performEvent , curTime ) ;
2022-02-26 18:24:08 +13:00
}
public bool ValidateEntityTarget ( EntityUid user , EntityUid target , EntityTargetAction action )
{
if ( ! target . IsValid ( ) | | Deleted ( target ) )
return false ;
if ( action . Whitelist ! = null & & ! action . Whitelist . IsValid ( target , EntityManager ) )
return false ;
if ( action . CheckCanInteract & & ! _actionBlockerSystem . CanInteract ( user , target ) )
return false ;
if ( user = = target )
return action . CanTargetSelf ;
if ( ! action . CheckCanAccess )
{
// even if we don't check for obstructions, we may still need to check the range.
var xform = Transform ( user ) ;
var targetXform = Transform ( target ) ;
if ( xform . MapID ! = targetXform . MapID )
return false ;
if ( action . Range < = 0 )
return true ;
2023-07-08 14:08:32 +10:00
var distance = ( _transformSystem . GetWorldPosition ( xform ) - _transformSystem . GetWorldPosition ( targetXform ) ) . Length ( ) ;
2023-05-01 04:29:18 -04:00
return distance < = action . Range ;
2022-02-26 18:24:08 +13:00
}
if ( _interactionSystem . InRangeUnobstructed ( user , target , range : action . Range )
& & _containerSystem . IsInSameOrParentContainer ( user , target ) )
{
return true ;
}
return _interactionSystem . CanAccessViaStorage ( user , target ) ;
}
2022-12-06 18:03:20 -05:00
public bool ValidateWorldTarget ( EntityUid user , EntityCoordinates coords , WorldTargetAction action )
2022-02-26 18:24:08 +13:00
{
if ( action . CheckCanInteract & & ! _actionBlockerSystem . CanInteract ( user , null ) )
return false ;
if ( ! action . CheckCanAccess )
{
// even if we don't check for obstructions, we may still need to check the range.
var xform = Transform ( user ) ;
2022-12-06 18:03:20 -05:00
if ( xform . MapID ! = coords . GetMapId ( EntityManager ) )
2022-02-26 18:24:08 +13:00
return false ;
if ( action . Range < = 0 )
return true ;
2023-05-01 04:29:18 -04:00
return coords . InRange ( EntityManager , _transformSystem , Transform ( user ) . Coordinates , action . Range ) ;
2022-02-26 18:24:08 +13:00
}
return _interactionSystem . InRangeUnobstructed ( user , coords , range : action . Range ) ;
}
2023-01-02 13:01:40 +13:00
public void PerformAction ( EntityUid performer , ActionsComponent ? component , ActionType action , BaseActionEvent ? actionEvent , TimeSpan curTime , bool predicted = true )
2022-02-26 18:24:08 +13:00
{
var handled = false ;
var toggledBefore = action . Toggled ;
if ( actionEvent ! = null )
{
// This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
actionEvent . Handled = false ;
if ( action . Provider = = null )
2023-01-02 13:01:40 +13:00
RaiseLocalEvent ( performer , ( object ) actionEvent , broadcast : true ) ;
2022-02-26 18:24:08 +13:00
else
RaiseLocalEvent ( action . Provider . Value , ( object ) actionEvent , broadcast : true ) ;
handled = actionEvent . Handled ;
}
2023-04-26 16:04:44 +12:00
_audio . PlayPredicted ( action . Sound , performer , predicted ? performer : null ) ;
handled | = action . Sound ! = null ;
2022-02-26 18:24:08 +13:00
if ( ! handled )
return ; // no interaction occurred.
// reduce charges, start cooldown, and mark as dirty (if required).
var dirty = toggledBefore = = action . Toggled ;
if ( action . Charges ! = null )
{
dirty = true ;
action . Charges - - ;
if ( action . Charges = = 0 )
action . Enabled = false ;
}
action . Cooldown = null ;
if ( action . UseDelay ! = null )
{
dirty = true ;
action . Cooldown = ( curTime , curTime + action . UseDelay . Value ) ;
}
2023-01-02 13:01:40 +13:00
if ( dirty & & component ! = null )
2022-02-26 18:24:08 +13:00
Dirty ( component ) ;
}
#endregion
#region AddRemoveActions
/// <summary>
/// Add an action to an action component. If the entity has no action component, this will give them one.
/// </summary>
/// <param name="uid">Entity to receive the actions</param>
/// <param name="action">The action to add</param>
/// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public virtual void AddAction ( EntityUid uid , ActionType action , EntityUid ? provider , ActionsComponent ? comp = null , bool dirty = true )
{
2022-04-14 16:17:34 +12:00
// Because action classes have state data, e.g. cooldowns and uses-remaining, people should not be adding prototypes directly
if ( action is IPrototype )
{
2023-06-27 23:56:52 +10:00
Log . Error ( "Attempted to directly add a prototype action. You need to clone a prototype in order to use it." ) ;
2022-04-14 16:17:34 +12:00
return ;
}
2022-02-26 18:24:08 +13:00
comp ? ? = EnsureComp < ActionsComponent > ( uid ) ;
action . Provider = provider ;
2023-04-25 07:29:47 +12:00
action . AttachedEntity = uid ;
2022-02-26 18:24:08 +13:00
AddActionInternal ( comp , action ) ;
2023-04-25 07:29:47 +12:00
if ( dirty )
2022-02-26 18:24:08 +13:00
Dirty ( comp ) ;
}
protected virtual void AddActionInternal ( ActionsComponent comp , ActionType action )
{
comp . Actions . Add ( action ) ;
}
/// <summary>
/// Add actions to an action component. If the entity has no action component, this will give them one.
/// </summary>
/// <param name="uid">Entity to receive the actions</param>
/// <param name="actions">The actions to add</param>
/// <param name="provider">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public void AddActions ( EntityUid uid , IEnumerable < ActionType > actions , EntityUid ? provider , ActionsComponent ? comp = null , bool dirty = true )
{
comp ? ? = EnsureComp < ActionsComponent > ( uid ) ;
2023-05-01 04:29:18 -04:00
var allClientExclusive = true ;
2023-04-23 21:38:52 +02:00
2022-02-26 18:24:08 +13:00
foreach ( var action in actions )
{
AddAction ( uid , action , provider , comp , false ) ;
2023-04-23 21:38:52 +02:00
allClientExclusive = allClientExclusive & & action . ClientExclusive ;
2022-02-26 18:24:08 +13:00
}
2023-04-23 21:38:52 +02:00
if ( dirty & & ! allClientExclusive )
2022-02-26 18:24:08 +13:00
Dirty ( comp ) ;
}
/// <summary>
/// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
/// </summary>
public void RemoveProvidedActions ( EntityUid uid , EntityUid provider , ActionsComponent ? comp = null )
{
if ( ! Resolve ( uid , ref comp , false ) )
return ;
2023-04-25 07:29:47 +12:00
foreach ( var act in comp . Actions . ToArray ( ) )
{
if ( act . Provider = = provider )
RemoveAction ( uid , act , comp , dirty : false ) ;
}
Dirty ( comp ) ;
2022-02-26 18:24:08 +13:00
}
2023-04-25 07:29:47 +12:00
public virtual void RemoveAction ( EntityUid uid , ActionType action , ActionsComponent ? comp = null , bool dirty = true )
2022-02-26 18:24:08 +13:00
{
if ( ! Resolve ( uid , ref comp , false ) )
return ;
2023-04-25 07:29:47 +12:00
comp . Actions . Remove ( action ) ;
action . AttachedEntity = null ;
2022-02-26 18:24:08 +13:00
if ( dirty )
Dirty ( comp ) ;
}
#endregion
#region EquipHandlers
private void OnDidEquip ( EntityUid uid , ActionsComponent component , DidEquipEvent args )
{
2022-04-14 16:17:34 +12:00
var ev = new GetItemActionsEvent ( args . SlotFlags ) ;
2023-05-01 04:29:18 -04:00
RaiseLocalEvent ( args . Equipment , ev ) ;
2022-02-26 18:24:08 +13:00
if ( ev . Actions . Count = = 0 )
return ;
AddActions ( args . Equipee , ev . Actions , args . Equipment , component ) ;
}
private void OnHandEquipped ( EntityUid uid , ActionsComponent component , DidEquipHandEvent args )
{
2022-04-14 16:17:34 +12:00
var ev = new GetItemActionsEvent ( ) ;
2023-05-01 04:29:18 -04:00
RaiseLocalEvent ( args . Equipped , ev ) ;
2022-02-26 18:24:08 +13:00
if ( ev . Actions . Count = = 0 )
return ;
AddActions ( args . User , ev . Actions , args . Equipped , component ) ;
}
private void OnDidUnequip ( EntityUid uid , ActionsComponent component , DidUnequipEvent args )
{
RemoveProvidedActions ( uid , args . Equipment , component ) ;
}
private void OnHandUnequipped ( EntityUid uid , ActionsComponent component , DidUnequipHandEvent args )
{
RemoveProvidedActions ( uid , args . Unequipped , component ) ;
}
#endregion
}