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 Content.Shared.Popups ;
using Robust.Shared.Audio ;
using Robust.Shared.Containers ;
using Robust.Shared.GameStates ;
using Robust.Shared.Map ;
using Robust.Shared.Player ;
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 ! ;
[Dependency] private readonly SharedPopupSystem _popupSystem = default ! ;
2022-10-04 14:24:19 +11:00
[Dependency] private readonly SharedAudioSystem _audio = 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 )
{
if ( args . SenderSession . AttachedEntity is not EntityUid user )
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
{
2022-09-04 17:21:14 -07:00
Logger . Error ( $"Attempted to perform an entity-targeted action without a target! Action: {entityAction.DisplayName}" ) ;
2022-02-26 18:24:08 +13:00
return ;
}
_rotateToFaceSystem . TryFaceCoordinates ( user , Transform ( entityTarget ) . WorldPosition ) ;
if ( ! ValidateEntityTarget ( user , entityTarget , entityAction ) )
return ;
if ( act . Provider = = null )
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 targeted at {ToPrettyString(entityTarget):target}." ) ;
else
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}) targeted at {ToPrettyString(entityTarget):target}." ) ;
if ( entityAction . Event ! = null )
{
entityAction . Event . Target = entityTarget ;
performEvent = entityAction . Event ;
}
break ;
case WorldTargetAction worldAction :
2022-12-06 18:03:20 -05:00
if ( ev . EntityCoordinatesTarget is not EntityCoordinates entityCoordinatesTarget )
2022-02-26 18:24:08 +13:00
{
2022-12-06 18:03:20 -05:00
Logger . 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 )
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}." ) ;
2022-02-26 18:24:08 +13:00
else
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}." ) ;
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 )
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." ) ;
else
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}." ) ;
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 ;
return ( xform . WorldPosition - targetXform . WorldPosition ) . Length < = action . Range ;
}
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 ;
2022-12-06 18:03:20 -05:00
return coords . InRange ( EntityManager , 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 ;
}
// Execute convenience functionality (pop-ups, sound, speech)
2023-01-02 13:01:40 +13:00
handled | = PerformBasicActions ( performer , action , predicted ) ;
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 ) ;
}
/// <summary>
/// Execute convenience functionality for actions (pop-ups, sound, speech)
/// </summary>
2023-01-02 13:01:40 +13:00
protected virtual bool PerformBasicActions ( EntityUid performer , ActionType action , bool predicted )
2022-02-26 18:24:08 +13:00
{
if ( action . Sound = = null & & string . IsNullOrWhiteSpace ( action . Popup ) )
return false ;
2023-01-02 13:01:40 +13:00
var filter = predicted ? Filter . PvsExcept ( performer ) : Filter . Pvs ( performer ) ;
2022-02-26 18:24:08 +13:00
2022-11-22 13:49:48 +13:00
_audio . Play ( action . Sound , filter , performer , true , action . AudioParams ) ;
2022-02-26 18:24:08 +13:00
if ( string . IsNullOrWhiteSpace ( action . Popup ) )
return true ;
var msg = ( ! action . Toggled | | string . IsNullOrWhiteSpace ( action . PopupToggleSuffix ) )
? Loc . GetString ( action . Popup )
: Loc . GetString ( action . Popup + action . PopupToggleSuffix ) ;
2022-12-19 10:41:47 +13:00
_popupSystem . PopupEntity ( msg , performer , filter , true ) ;
2022-02-26 18:24:08 +13:00
return true ;
}
#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 )
{
Logger . Error ( "Attempted to directly add a prototype action. You need to clone a prototype in order to use it." ) ;
return ;
}
2022-02-26 18:24:08 +13:00
comp ? ? = EnsureComp < ActionsComponent > ( uid ) ;
action . Provider = provider ;
action . AttachedEntity = comp . Owner ;
AddActionInternal ( comp , action ) ;
// for client-exclusive actions, the client shouldn't mark the comp as dirty. Otherwise that just leads to
// unnecessary prediction resetting and state handling.
2023-04-23 21:38:52 +02:00
if ( dirty & & ! action . ClientExclusive )
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-04-23 21:38:52 +02:00
bool allClientExclusive = true ;
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 ;
var provided = comp . Actions . Where ( act = > act . Provider = = provider ) . ToList ( ) ;
2022-06-29 15:11:09 +12:00
if ( provided . Count > 0 )
RemoveActions ( uid , provided , comp ) ;
2022-02-26 18:24:08 +13:00
}
public virtual void RemoveActions ( EntityUid uid , IEnumerable < ActionType > actions , ActionsComponent ? comp = null , bool dirty = true )
{
if ( ! Resolve ( uid , ref comp , false ) )
return ;
foreach ( var action in actions )
{
comp . Actions . Remove ( action ) ;
action . AttachedEntity = null ;
}
if ( dirty )
Dirty ( comp ) ;
}
public void RemoveAction ( EntityUid uid , ActionType action , ActionsComponent ? comp = null )
= > RemoveActions ( uid , new [ ] { action } , 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 ) ;
2022-02-26 18:24:08 +13:00
RaiseLocalEvent ( args . Equipment , ev , false ) ;
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 ( ) ;
2022-02-26 18:24:08 +13:00
RaiseLocalEvent ( args . Equipped , ev , false ) ;
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
}