2023-09-08 18:16:05 -07:00
using System.Diagnostics.CodeAnalysis ;
using System.Linq ;
2022-02-26 18:24:08 +13:00
using Content.Shared.ActionBlocker ;
2023-09-08 18:16:05 -07:00
using Content.Shared.Actions.Events ;
2022-02-26 18:24:08 +13:00
using Content.Shared.Administration.Logs ;
using Content.Shared.Database ;
using Content.Shared.Hands ;
using Content.Shared.Interaction ;
using Content.Shared.Inventory.Events ;
2023-11-03 19:55:32 -04:00
using Content.Shared.Mind ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2022-02-26 18:24:08 +13:00
using Robust.Shared.Containers ;
using Robust.Shared.GameStates ;
using Robust.Shared.Map ;
using Robust.Shared.Timing ;
2023-09-09 16:14:17 -07:00
using Robust.Shared.Utility ;
2023-12-05 16:00:02 -05:00
using Content.Shared.Rejuvenate ;
2022-02-26 18:24:08 +13:00
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 ! ;
2023-09-23 04:49:39 -04:00
[Dependency] private readonly ActionContainerSystem _actionContainer = default ! ;
2022-02-26 18:24:08 +13:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2024-02-19 21:08:41 -05:00
SubscribeLocalEvent < InstantActionComponent , MapInitEvent > ( OnActionMapInit ) ;
SubscribeLocalEvent < EntityTargetActionComponent , MapInitEvent > ( OnActionMapInit ) ;
SubscribeLocalEvent < WorldTargetActionComponent , MapInitEvent > ( OnActionMapInit ) ;
SubscribeLocalEvent < InstantActionComponent , ComponentShutdown > ( OnActionShutdown ) ;
SubscribeLocalEvent < EntityTargetActionComponent , ComponentShutdown > ( OnActionShutdown ) ;
SubscribeLocalEvent < WorldTargetActionComponent , ComponentShutdown > ( OnActionShutdown ) ;
2023-12-15 04:41:44 -05:00
2022-02-26 18:24:08 +13:00
SubscribeLocalEvent < ActionsComponent , DidEquipEvent > ( OnDidEquip ) ;
SubscribeLocalEvent < ActionsComponent , DidEquipHandEvent > ( OnHandEquipped ) ;
SubscribeLocalEvent < ActionsComponent , DidUnequipEvent > ( OnDidUnequip ) ;
SubscribeLocalEvent < ActionsComponent , DidUnequipHandEvent > ( OnHandUnequipped ) ;
2023-12-05 16:00:02 -05:00
SubscribeLocalEvent < ActionsComponent , RejuvenateEvent > ( OnRejuventate ) ;
2022-02-26 18:24:08 +13:00
2023-10-29 19:10:30 +11:00
SubscribeLocalEvent < ActionsComponent , ComponentShutdown > ( OnShutdown ) ;
2023-09-08 18:16:05 -07:00
SubscribeLocalEvent < ActionsComponent , ComponentGetState > ( OnActionsGetState ) ;
SubscribeLocalEvent < InstantActionComponent , ComponentGetState > ( OnInstantGetState ) ;
SubscribeLocalEvent < EntityTargetActionComponent , ComponentGetState > ( OnEntityTargetGetState ) ;
SubscribeLocalEvent < WorldTargetActionComponent , ComponentGetState > ( OnWorldTargetGetState ) ;
SubscribeLocalEvent < InstantActionComponent , GetActionDataEvent > ( OnGetActionData ) ;
SubscribeLocalEvent < EntityTargetActionComponent , GetActionDataEvent > ( OnGetActionData ) ;
SubscribeLocalEvent < WorldTargetActionComponent , GetActionDataEvent > ( OnGetActionData ) ;
2022-02-26 18:24:08 +13:00
SubscribeAllEvent < RequestPerformActionEvent > ( OnActionRequest ) ;
}
2024-02-19 21:08:41 -05:00
private void OnActionMapInit ( EntityUid uid , BaseActionComponent component , MapInitEvent args )
{
if ( component . Charges = = null )
return ;
component . MaxCharges ? ? = component . Charges . Value ;
Dirty ( uid , component ) ;
}
private void OnActionShutdown ( EntityUid uid , BaseActionComponent component , ComponentShutdown args )
2023-12-15 04:41:44 -05:00
{
2024-02-19 21:08:41 -05:00
if ( component . AttachedEntity ! = null & & ! TerminatingOrDeleted ( component . AttachedEntity . Value ) )
RemoveAction ( component . AttachedEntity . Value , uid , action : component ) ;
2023-12-15 04:41:44 -05:00
}
2023-10-29 19:10:30 +11:00
private void OnShutdown ( EntityUid uid , ActionsComponent component , ComponentShutdown args )
{
foreach ( var act in component . Actions )
{
RemoveAction ( uid , act , component ) ;
}
}
2023-09-08 18:16:05 -07:00
private void OnInstantGetState ( EntityUid uid , InstantActionComponent component , ref ComponentGetState args )
{
2023-09-11 09:42:41 +10:00
args . State = new InstantActionComponentState ( component , EntityManager ) ;
2023-09-08 18:16:05 -07:00
}
private void OnEntityTargetGetState ( EntityUid uid , EntityTargetActionComponent component , ref ComponentGetState args )
{
2023-09-11 09:42:41 +10:00
args . State = new EntityTargetActionComponentState ( component , EntityManager ) ;
2023-09-08 18:16:05 -07:00
}
private void OnWorldTargetGetState ( EntityUid uid , WorldTargetActionComponent component , ref ComponentGetState args )
{
2023-09-11 09:42:41 +10:00
args . State = new WorldTargetActionComponentState ( component , EntityManager ) ;
2023-09-08 18:16:05 -07:00
}
private void OnGetActionData < T > ( EntityUid uid , T component , ref GetActionDataEvent args ) where T : BaseActionComponent
{
args . Action = component ;
}
2023-09-23 04:49:39 -04:00
public bool TryGetActionData (
[NotNullWhen(true)] EntityUid ? uid ,
[NotNullWhen(true)] out BaseActionComponent ? result ,
bool logError = true )
2023-09-08 18:16:05 -07:00
{
2023-09-23 04:49:39 -04:00
result = null ;
if ( ! Exists ( uid ) )
return false ;
2023-09-08 18:16:05 -07:00
var ev = new GetActionDataEvent ( ) ;
2023-09-23 04:49:39 -04:00
RaiseLocalEvent ( uid . Value , ref ev ) ;
result = ev . Action ;
2023-09-08 18:16:05 -07:00
2023-09-23 04:49:39 -04:00
if ( result ! = null )
return true ;
2023-09-08 18:16:05 -07:00
2023-11-10 22:46:42 -08:00
if ( logError )
Log . Error ( $"Failed to get action from action entity: {ToPrettyString(uid.Value)}" ) ;
2023-09-23 04:49:39 -04:00
return false ;
2023-09-08 18:16:05 -07:00
}
2023-09-23 04:49:39 -04:00
public bool ResolveActionData (
[NotNullWhen(true)] EntityUid ? uid ,
[NotNullWhen(true)] ref BaseActionComponent ? result ,
bool logError = true )
2023-09-08 18:16:05 -07:00
{
2023-09-23 04:49:39 -04:00
if ( result ! = null )
{
2023-10-19 12:34:31 -07:00
DebugTools . AssertOwner ( uid , result ) ;
2023-09-23 04:49:39 -04:00
return true ;
}
2023-09-08 18:16:05 -07:00
2023-09-23 04:49:39 -04:00
return TryGetActionData ( uid , out result , logError ) ;
2023-09-09 16:14:17 -07:00
}
2023-09-08 18:16:05 -07:00
public void SetCooldown ( EntityUid ? actionId , TimeSpan start , TimeSpan end )
{
if ( actionId = = null )
return ;
2023-09-23 04:49:39 -04:00
if ( ! TryGetActionData ( actionId , out var action ) )
2023-09-08 18:16:05 -07:00
return ;
action . Cooldown = ( start , end ) ;
Dirty ( actionId . Value , action ) ;
}
2023-11-07 17:24:43 -08:00
public void SetCooldown ( EntityUid ? actionId , TimeSpan cooldown )
{
var start = GameTiming . CurTime ;
SetCooldown ( actionId , start , start + cooldown ) ;
}
public void ClearCooldown ( EntityUid ? actionId )
{
if ( actionId = = null )
return ;
if ( ! TryGetActionData ( actionId , out var action ) )
return ;
if ( action . Cooldown is not { } cooldown )
return ;
action . Cooldown = ( cooldown . Start , GameTiming . CurTime ) ;
Dirty ( actionId . Value , action ) ;
}
2023-09-22 16:01:05 -04:00
public void StartUseDelay ( EntityUid ? actionId )
{
if ( actionId = = null )
return ;
2023-09-23 03:09:07 -07:00
if ( ! TryGetActionData ( actionId , out var action ) | | action . UseDelay = = null )
2023-09-22 16:01:05 -04:00
return ;
action . Cooldown = ( GameTiming . CurTime , GameTiming . CurTime + action . UseDelay . Value ) ;
Dirty ( actionId . Value , action ) ;
}
2023-12-15 04:41:44 -05:00
public void SetUseDelay ( EntityUid ? actionId , TimeSpan ? delay )
{
if ( ! TryGetActionData ( actionId , out var action ) | | action . UseDelay = = delay )
return ;
action . UseDelay = delay ;
UpdateAction ( actionId , action ) ;
Dirty ( actionId . Value , action ) ;
}
public void ReduceUseDelay ( EntityUid ? actionId , TimeSpan ? lowerDelay )
{
if ( ! TryGetActionData ( actionId , out var action ) )
return ;
if ( action . UseDelay ! = null & & lowerDelay ! = null )
action . UseDelay = action . UseDelay - lowerDelay ;
if ( action . UseDelay < TimeSpan . Zero )
action . UseDelay = null ;
UpdateAction ( actionId , action ) ;
Dirty ( actionId . Value , action ) ;
}
2023-12-05 16:00:02 -05:00
private void OnRejuventate ( EntityUid uid , ActionsComponent component , RejuvenateEvent args )
{
foreach ( var act in component . Actions )
{
ClearCooldown ( act ) ;
}
}
2022-02-26 18:24:08 +13:00
#region ComponentStateManagement
2023-09-23 04:49:39 -04:00
protected virtual void UpdateAction ( EntityUid ? actionId , BaseActionComponent ? action = null )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
// See client-side code.
2022-02-26 18:24:08 +13:00
}
2023-09-08 18:16:05 -07:00
public void SetToggled ( EntityUid ? actionId , bool toggled )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
if ( ! TryGetActionData ( actionId , out var action ) | |
action . Toggled = = toggled )
{
2022-02-26 18:24:08 +13:00
return ;
2023-09-08 18:16:05 -07:00
}
2022-02-26 18:24:08 +13:00
action . Toggled = toggled ;
2023-09-23 04:49:39 -04:00
UpdateAction ( actionId , action ) ;
2023-09-08 18:16:05 -07:00
Dirty ( actionId . Value , action ) ;
2022-02-26 18:24:08 +13:00
}
2023-09-08 18:16:05 -07:00
public void SetEnabled ( EntityUid ? actionId , bool enabled )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
if ( ! TryGetActionData ( actionId , out var action ) | |
action . Enabled = = enabled )
{
2022-02-26 18:24:08 +13:00
return ;
2023-09-08 18:16:05 -07:00
}
2022-02-26 18:24:08 +13:00
action . Enabled = enabled ;
2023-09-23 04:49:39 -04:00
UpdateAction ( actionId , action ) ;
2023-09-08 18:16:05 -07:00
Dirty ( actionId . Value , action ) ;
2022-02-26 18:24:08 +13:00
}
2023-09-08 18:16:05 -07:00
public void SetCharges ( EntityUid ? actionId , int? charges )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
if ( ! TryGetActionData ( actionId , out var action ) | |
action . Charges = = charges )
{
2022-02-26 18:24:08 +13:00
return ;
2023-09-08 18:16:05 -07:00
}
2022-02-26 18:24:08 +13:00
action . Charges = charges ;
2023-09-23 04:49:39 -04:00
UpdateAction ( actionId , action ) ;
2023-09-08 18:16:05 -07:00
Dirty ( actionId . Value , action ) ;
}
2023-12-15 04:41:44 -05:00
public int? GetCharges ( EntityUid ? actionId )
{
if ( ! TryGetActionData ( actionId , out var action ) )
return null ;
return action . Charges ;
}
public void AddCharges ( EntityUid ? actionId , int addCharges )
{
if ( ! TryGetActionData ( actionId , out var action ) | | action . Charges = = null | | addCharges < 1 )
return ;
action . Charges + = addCharges ;
UpdateAction ( actionId , action ) ;
Dirty ( actionId . Value , action ) ;
}
public void RemoveCharges ( EntityUid ? actionId , int? removeCharges )
{
if ( ! TryGetActionData ( actionId , out var action ) | | action . Charges = = null )
return ;
if ( removeCharges = = null )
action . Charges = removeCharges ;
else
action . Charges - = removeCharges ;
if ( action . Charges is < 0 )
action . Charges = null ;
UpdateAction ( actionId , action ) ;
Dirty ( actionId . Value , action ) ;
}
public void ResetCharges ( EntityUid ? actionId )
{
if ( ! TryGetActionData ( actionId , out var action ) )
return ;
action . Charges = action . MaxCharges ;
UpdateAction ( actionId , action ) ;
Dirty ( actionId . Value , action ) ;
}
2023-09-08 18:16:05 -07:00
private void OnActionsGetState ( EntityUid uid , ActionsComponent component , ref ComponentGetState args )
{
2023-09-11 09:42:41 +10:00
args . State = new ActionsComponentState ( GetNetEntitySet ( component . Actions ) ) ;
2022-02-26 18:24:08 +13:00
}
#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 ;
2023-09-11 09:42:41 +10:00
var actionEnt = GetEntity ( ev . Action ) ;
if ( ! TryComp ( actionEnt , out MetaDataComponent ? metaData ) )
2023-09-08 18:16:05 -07:00
return ;
2023-09-11 09:42:41 +10:00
var name = Name ( actionEnt , metaData ) ;
2023-09-08 18:16:05 -07:00
2022-02-26 18:24:08 +13:00
// Does the user actually have the requested action?
2023-09-11 09:42:41 +10:00
if ( ! component . Actions . Contains ( actionEnt ) )
2022-02-26 18:24:08 +13:00
{
2022-05-28 23:41:17 -07:00
_adminLogger . Add ( LogType . Action ,
2023-09-08 18:16:05 -07:00
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}." ) ;
2022-02-26 18:24:08 +13:00
return ;
}
2023-09-23 04:49:39 -04:00
if ( ! TryGetActionData ( actionEnt , out var action ) )
return ;
DebugTools . Assert ( action . AttachedEntity = = user ) ;
if ( ! action . Enabled )
2022-02-26 18:24:08 +13:00
return ;
var curTime = GameTiming . CurTime ;
2023-12-15 04:41:44 -05:00
// TODO: Check for charge recovery timer
2023-09-08 18:16:05 -07:00
if ( action . Cooldown . HasValue & & action . Cooldown . Value . End > curTime )
2022-02-26 18:24:08 +13:00
return ;
2023-12-15 04:41:44 -05:00
// TODO: Replace with individual charge recovery when we have the visuals to aid it
if ( action is { Charges : < 1 , RenewCharges : true } )
ResetCharges ( actionEnt ) ;
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:
2023-09-08 18:16:05 -07:00
switch ( action )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
case EntityTargetActionComponent entityAction :
2023-09-11 09:42:41 +10:00
if ( ev . EntityTarget is not { Valid : true } netTarget )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
Log . Error ( $"Attempted to perform an entity-targeted action without a target! Action: {name}" ) ;
2022-02-26 18:24:08 +13:00
return ;
}
2023-09-11 09:42:41 +10:00
var entityTarget = GetEntity ( netTarget ) ;
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 ;
2023-09-23 04:49:39 -04:00
_adminLogger . Add ( LogType . Action ,
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):provider}) targeted at {ToPrettyString(entityTarget):target}." ) ;
2022-02-26 18:24:08 +13:00
if ( entityAction . Event ! = null )
{
entityAction . Event . Target = entityTarget ;
2023-09-11 09:42:41 +10:00
Dirty ( actionEnt , entityAction ) ;
2022-02-26 18:24:08 +13:00
performEvent = entityAction . Event ;
}
break ;
2023-09-08 18:16:05 -07:00
case WorldTargetActionComponent worldAction :
2023-09-11 09:42:41 +10:00
if ( ev . EntityCoordinatesTarget is not { } netCoordinatesTarget )
2022-02-26 18:24:08 +13:00
{
2023-09-08 18:16:05 -07:00
Log . Error ( $"Attempted to perform a world-targeted action without a target! Action: {name}" ) ;
2022-02-26 18:24:08 +13:00
return ;
}
2023-09-11 09:42:41 +10:00
var entityCoordinatesTarget = GetCoordinates ( netCoordinatesTarget ) ;
2023-11-12 00:50:35 -08:00
_rotateToFaceSystem . TryFaceCoordinates ( user , entityCoordinatesTarget . ToMapPos ( EntityManager , _transformSystem ) ) ;
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 ;
2023-09-23 04:49:39 -04:00
_adminLogger . Add ( LogType . Action ,
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(action.Container ?? user):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 ;
2023-09-11 09:42:41 +10:00
Dirty ( actionEnt , worldAction ) ;
2022-02-26 18:24:08 +13:00
performEvent = worldAction . Event ;
}
break ;
2023-09-08 18:16:05 -07:00
case InstantActionComponent instantAction :
if ( action . CheckCanInteract & & ! _actionBlockerSystem . CanInteract ( user , null ) )
2022-02-26 18:24:08 +13:00
return ;
2023-09-23 04:49:39 -04:00
_adminLogger . Add ( LogType . Action ,
$"{ToPrettyString(user):user} is performing the {name:action} action provided by {ToPrettyString(action.Container ?? user):provider}." ) ;
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-09-11 09:42:41 +10:00
PerformAction ( user , component , actionEnt , action , performEvent , curTime ) ;
2022-02-26 18:24:08 +13:00
}
2023-09-08 18:16:05 -07:00
public bool ValidateEntityTarget ( EntityUid user , EntityUid target , EntityTargetActionComponent action )
2022-02-26 18:24:08 +13:00
{
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 ) ;
}
2023-09-08 18:16:05 -07:00
public bool ValidateWorldTarget ( EntityUid user , EntityCoordinates coords , WorldTargetActionComponent 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-09-08 18:16:05 -07:00
public void PerformAction ( EntityUid performer , ActionsComponent ? component , EntityUid actionId , BaseActionComponent action , BaseActionEvent ? actionEvent , TimeSpan curTime , bool predicted = true )
2022-02-26 18:24:08 +13:00
{
var handled = false ;
var toggledBefore = action . Toggled ;
2023-11-03 19:55:32 -04:00
// Note that attached entity and attached container are allowed to be null here.
2023-09-23 04:49:39 -04:00
if ( action . AttachedEntity ! = null & & action . AttachedEntity ! = performer )
{
Log . Error ( $"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(actionId)} that is attached to another entity {ToPrettyString(action.AttachedEntity.Value)}" ) ;
return ;
}
2022-02-26 18:24:08 +13:00
if ( actionEvent ! = null )
{
// This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use).
actionEvent . Handled = false ;
2023-11-03 19:55:32 -04:00
var target = performer ;
if ( ! action . RaiseOnUser & & action . Container ! = null & & ! HasComp < MindComponent > ( action . Container ) )
target = action . Container . Value ;
RaiseLocalEvent ( target , ( object ) actionEvent , broadcast : true ) ;
2022-02-26 18:24:08 +13:00
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 - - ;
2023-12-15 04:41:44 -05:00
if ( action is { Charges : 0 , RenewCharges : false } )
2022-02-26 18:24:08 +13:00
action . Enabled = false ;
}
action . Cooldown = null ;
2023-12-15 04:41:44 -05:00
if ( action is { UseDelay : not null , Charges : null or < 1 } )
2022-02-26 18:24:08 +13:00
{
dirty = true ;
action . Cooldown = ( curTime , curTime + action . UseDelay . Value ) ;
}
2023-09-08 18:16:05 -07:00
Dirty ( actionId , action ) ;
2023-01-02 13:01:40 +13:00
if ( dirty & & component ! = null )
2023-09-08 18:16:05 -07:00
Dirty ( performer , component ) ;
2022-02-26 18:24:08 +13:00
}
#endregion
#region AddRemoveActions
2023-09-23 04:49:39 -04:00
public EntityUid ? AddAction ( EntityUid performer ,
string? actionPrototypeId ,
EntityUid container = default ,
ActionsComponent ? component = null )
{
EntityUid ? actionId = null ;
AddAction ( performer , ref actionId , out _ , actionPrototypeId , container , component ) ;
return actionId ;
}
2022-02-26 18:24:08 +13:00
/// <summary>
2023-09-23 04:49:39 -04:00
/// Adds an action to an action holder. If the given entity does not exist, it will attempt to spawn one.
2023-09-08 18:16:05 -07:00
/// If the holder has no actions component, this will give them one.
2022-02-26 18:24:08 +13:00
/// </summary>
2023-09-23 04:49:39 -04:00
/// <param name="performer">Entity to receive the actions</param>
/// <param name="actionId">Action entity to add</param>
/// <param name="component">The <see cref="performer"/>'s action component of </param>
/// <param name="actionPrototypeId">The action entity prototype id to use if <see cref="actionId"/> is invalid.</param>
2024-01-14 10:52:07 +03:00
/// <param name="container">The entity that contains/enables this action (e.g., flashlight).</param>
2023-09-23 04:49:39 -04:00
public bool AddAction ( EntityUid performer ,
[NotNullWhen(true)] ref EntityUid ? actionId ,
string? actionPrototypeId ,
EntityUid container = default ,
ActionsComponent ? component = null )
{
return AddAction ( performer , ref actionId , out _ , actionPrototypeId , container , component ) ;
}
/// <inheritdoc cref="AddAction(Robust.Shared.GameObjects.EntityUid,ref System.Nullable{Robust.Shared.GameObjects.EntityUid},string?,Robust.Shared.GameObjects.EntityUid,Content.Shared.Actions.ActionsComponent?)"/>
public bool AddAction ( EntityUid performer ,
[NotNullWhen(true)] ref EntityUid ? actionId ,
[NotNullWhen(true)] out BaseActionComponent ? action ,
string? actionPrototypeId ,
EntityUid container = default ,
ActionsComponent ? component = null )
{
if ( ! container . IsValid ( ) )
container = performer ;
if ( ! _actionContainer . EnsureAction ( container , ref actionId , out action , actionPrototypeId ) )
return false ;
2023-09-08 18:16:05 -07:00
2023-09-23 04:49:39 -04:00
return AddActionDirect ( performer , actionId . Value , component , action ) ;
2023-09-08 18:16:05 -07:00
}
/// <summary>
2023-09-23 04:49:39 -04:00
/// Adds a pre-existing action.
2023-09-08 18:16:05 -07:00
/// </summary>
2023-09-23 04:49:39 -04:00
public bool AddAction ( EntityUid performer ,
EntityUid actionId ,
EntityUid container ,
ActionsComponent ? comp = null ,
BaseActionComponent ? action = null ,
ActionsContainerComponent ? containerComp = null
)
{
if ( ! ResolveActionData ( actionId , ref action ) )
return false ;
if ( action . Container ! = container
| | ! Resolve ( container , ref containerComp )
| | ! containerComp . Container . Contains ( actionId ) )
2022-04-14 16:17:34 +12:00
{
2023-09-23 04:49:39 -04:00
Log . Error ( $"Attempted to add an action with an invalid container: {ToPrettyString(actionId)}" ) ;
return false ;
2022-04-14 16:17:34 +12:00
}
2023-09-23 04:49:39 -04:00
return AddActionDirect ( performer , actionId , comp , action ) ;
}
2023-09-08 18:16:05 -07:00
2023-09-23 04:49:39 -04:00
/// <summary>
/// Adds a pre-existing action. This also bypasses the requirement that the given action must be stored in a
/// valid action container.
/// </summary>
public bool AddActionDirect ( EntityUid performer ,
EntityUid actionId ,
ActionsComponent ? comp = null ,
BaseActionComponent ? action = null )
{
if ( ! ResolveActionData ( actionId , ref action ) )
return false ;
DebugTools . Assert ( action . Container = = null | |
( TryComp ( action . Container , out ActionsContainerComponent ? containerComp )
& & containerComp . Container . Contains ( actionId ) ) ) ;
2022-02-26 18:24:08 +13:00
2023-10-24 11:53:27 +11:00
if ( action . AttachedEntity ! = null )
RemoveAction ( action . AttachedEntity . Value , actionId , action : action ) ;
2023-10-19 12:34:31 -07:00
DebugTools . AssertOwner ( performer , comp ) ;
2023-09-23 04:49:39 -04:00
comp ? ? = EnsureComp < ActionsComponent > ( performer ) ;
action . AttachedEntity = performer ;
comp . Actions . Add ( actionId ) ;
Dirty ( actionId , action ) ;
Dirty ( performer , comp ) ;
ActionAdded ( performer , actionId , comp , action ) ;
return true ;
2022-02-26 18:24:08 +13:00
}
2023-09-23 04:49:39 -04:00
/// <summary>
/// This method gets called after a new action got added.
/// </summary>
protected virtual void ActionAdded ( EntityUid performer , EntityUid actionId , ActionsComponent comp , BaseActionComponent action )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
// See client-side system for UI code.
2022-02-26 18:24:08 +13:00
}
/// <summary>
2023-09-23 04:49:39 -04:00
/// Grant pre-existing actions. If the entity has no action component, this will give them one.
2022-02-26 18:24:08 +13:00
/// </summary>
2023-09-23 04:49:39 -04:00
/// <param name="performer">Entity to receive the actions</param>
2022-02-26 18:24:08 +13:00
/// <param name="actions">The actions to add</param>
2023-09-23 04:49:39 -04:00
/// <param name="container">The entity that enables these actions (e.g., flashlight). May be null (innate actions).</param>
public void GrantActions ( EntityUid performer , IEnumerable < EntityUid > actions , EntityUid container , ActionsComponent ? comp = null , ActionsContainerComponent ? containerComp = null )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
if ( ! Resolve ( container , ref containerComp ) )
return ;
2022-02-26 18:24:08 +13:00
2023-10-19 12:34:31 -07:00
DebugTools . AssertOwner ( performer , comp ) ;
2023-09-23 04:49:39 -04:00
comp ? ? = EnsureComp < ActionsComponent > ( performer ) ;
2023-04-23 21:38:52 +02:00
2023-09-08 18:16:05 -07:00
foreach ( var actionId in actions )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
AddAction ( performer , actionId , container , comp , containerComp : containerComp ) ;
2022-02-26 18:24:08 +13:00
}
2023-09-08 18:16:05 -07:00
}
2023-10-24 11:53:27 +11:00
/// <summary>
/// Grants all actions currently contained in some action-container. If the target entity has no action
/// component, this will give them one.
/// </summary>
/// <param name="performer">Entity to receive the actions</param>
/// <param name="container">The entity that contains thee actions.</param>
public void GrantContainedActions ( Entity < ActionsComponent ? > performer , Entity < ActionsContainerComponent ? > container )
{
if ( ! Resolve ( container , ref container . Comp ) )
return ;
performer . Comp ? ? = EnsureComp < ActionsComponent > ( performer ) ;
foreach ( var actionId in container . Comp . Container . ContainedEntities )
{
if ( TryGetActionData ( actionId , out var action ) )
AddActionDirect ( performer , actionId , performer . Comp , action ) ;
}
}
2024-01-15 01:37:38 -05:00
/// <summary>
/// Grants the provided action from the container to the target entity. If the target entity has no action
/// component, this will give them one.
/// </summary>
/// <param name="performer"></param>
/// <param name="container"></param>
/// <param name="actionId"></param>
public void GrantContainedAction ( Entity < ActionsComponent ? > performer , Entity < ActionsContainerComponent ? > container , EntityUid actionId )
{
if ( ! Resolve ( container , ref container . Comp ) )
return ;
performer . Comp ? ? = EnsureComp < ActionsComponent > ( performer ) ;
if ( TryGetActionData ( actionId , out var action ) )
AddActionDirect ( performer , actionId , performer . Comp , action ) ;
}
2023-09-09 16:14:17 -07:00
public IEnumerable < ( EntityUid Id , BaseActionComponent Comp ) > GetActions ( EntityUid holderId , ActionsComponent ? actions = null )
2023-09-08 18:16:05 -07:00
{
2023-09-09 16:14:17 -07:00
if ( ! Resolve ( holderId , ref actions , false ) )
2023-09-08 18:16:05 -07:00
yield break ;
2023-09-09 16:14:17 -07:00
foreach ( var actionId in actions . Actions )
2023-09-08 18:16:05 -07:00
{
if ( ! TryGetActionData ( actionId , out var action ) )
continue ;
yield return ( actionId , action ) ;
}
2022-02-26 18:24:08 +13:00
}
/// <summary>
/// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions.
/// </summary>
2023-09-23 04:49:39 -04:00
public void RemoveProvidedActions ( EntityUid performer , EntityUid container , ActionsComponent ? comp = null )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
if ( ! Resolve ( performer , ref comp , false ) )
2023-09-08 18:16:05 -07:00
return ;
2023-09-23 04:49:39 -04:00
foreach ( var actionId in comp . Actions . ToArray ( ) )
2023-04-25 07:29:47 +12:00
{
2023-09-23 04:49:39 -04:00
if ( ! TryGetActionData ( actionId , out var action ) )
return ;
2023-09-08 18:16:05 -07:00
2023-09-23 04:49:39 -04:00
if ( action . Container = = container )
RemoveAction ( performer , actionId , comp ) ;
}
2022-02-26 18:24:08 +13:00
}
2024-01-15 01:37:38 -05:00
/// <summary>
/// Removes a single provided action provided by another entity.
/// </summary>
public void RemoveProvidedAction ( EntityUid performer , EntityUid container , EntityUid actionId , ActionsComponent ? comp = null )
{
if ( ! Resolve ( performer , ref comp , false ) | | ! TryGetActionData ( actionId , out var action ) )
return ;
if ( action . Container = = container )
RemoveAction ( performer , actionId , comp ) ;
}
2023-09-23 04:49:39 -04:00
public void RemoveAction ( EntityUid ? actionId )
2022-02-26 18:24:08 +13:00
{
2023-09-23 04:49:39 -04:00
if ( actionId = = null )
2022-02-26 18:24:08 +13:00
return ;
2023-09-23 04:49:39 -04:00
if ( ! TryGetActionData ( actionId , out var action ) )
return ;
2022-02-26 18:24:08 +13:00
2023-09-23 04:49:39 -04:00
if ( ! TryComp ( action . AttachedEntity , out ActionsComponent ? comp ) )
return ;
2023-09-09 16:14:17 -07:00
2023-09-23 04:49:39 -04:00
RemoveAction ( action . AttachedEntity . Value , actionId , comp , action ) ;
2023-09-08 18:16:05 -07:00
}
2023-09-23 04:49:39 -04:00
public void RemoveAction ( EntityUid performer , EntityUid ? actionId , ActionsComponent ? comp = null , BaseActionComponent ? action = null )
2023-09-08 18:16:05 -07:00
{
2023-09-23 04:49:39 -04:00
if ( actionId = = null )
2023-09-08 18:16:05 -07:00
return ;
2023-09-23 04:49:39 -04:00
if ( ! ResolveActionData ( actionId , ref action ) )
return ;
2023-09-08 18:16:05 -07:00
2023-10-12 04:50:10 +11:00
if ( action . AttachedEntity ! = performer )
{
2023-11-11 17:45:46 +11:00
DebugTools . Assert ( ! Resolve ( performer , ref comp , false )
| | comp . LifeStage > = ComponentLifeStage . Stopping
| | ! comp . Actions . Contains ( actionId . Value ) ) ;
2023-10-29 19:10:30 +11:00
if ( ! GameTiming . ApplyingState )
Log . Error ( $"Attempted to remove an action {ToPrettyString(actionId)} from an entity that it was never attached to: {ToPrettyString(performer)}" ) ;
2023-10-12 04:50:10 +11:00
return ;
}
2023-09-23 04:49:39 -04:00
if ( ! Resolve ( performer , ref comp , false ) )
{
2023-10-12 04:50:10 +11:00
DebugTools . Assert ( action . AttachedEntity = = null | | TerminatingOrDeleted ( action . AttachedEntity . Value ) ) ;
action . AttachedEntity = null ;
2023-09-09 16:14:17 -07:00
return ;
2023-09-23 04:49:39 -04:00
}
2023-09-09 16:14:17 -07:00
2023-09-23 04:49:39 -04:00
if ( action . AttachedEntity = = null )
2023-09-08 18:16:05 -07:00
{
2023-09-23 04:49:39 -04:00
// action was already removed?
DebugTools . Assert ( ! comp . Actions . Contains ( actionId . Value ) | | GameTiming . ApplyingState ) ;
return ;
2023-09-08 18:16:05 -07:00
}
2023-09-23 04:49:39 -04:00
comp . Actions . Remove ( actionId . Value ) ;
action . AttachedEntity = null ;
Dirty ( actionId . Value , action ) ;
Dirty ( performer , comp ) ;
ActionRemoved ( performer , actionId . Value , comp , action ) ;
if ( action . Temporary )
QueueDel ( actionId . Value ) ;
}
/// <summary>
/// This method gets called after an action got removed.
/// </summary>
protected virtual void ActionRemoved ( EntityUid performer , EntityUid actionId , ActionsComponent comp , BaseActionComponent action )
{
// See client-side system for UI code.
2022-02-26 18:24:08 +13:00
}
#endregion
#region EquipHandlers
private void OnDidEquip ( EntityUid uid , ActionsComponent component , DidEquipEvent args )
{
2023-09-23 04:49:39 -04:00
if ( GameTiming . ApplyingState )
return ;
var ev = new GetItemActionsEvent ( _actionContainer , args . Equipee , args . Equipment , 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 ;
2023-09-23 04:49:39 -04:00
GrantActions ( args . Equipee , ev . Actions , args . Equipment , component ) ;
2022-02-26 18:24:08 +13:00
}
private void OnHandEquipped ( EntityUid uid , ActionsComponent component , DidEquipHandEvent args )
{
2023-09-23 04:49:39 -04:00
if ( GameTiming . ApplyingState )
return ;
var ev = new GetItemActionsEvent ( _actionContainer , args . User , args . Equipped ) ;
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 ;
2023-09-23 04:49:39 -04:00
GrantActions ( args . User , ev . Actions , args . Equipped , component ) ;
2022-02-26 18:24:08 +13:00
}
private void OnDidUnequip ( EntityUid uid , ActionsComponent component , DidUnequipEvent args )
{
2023-09-23 04:49:39 -04:00
if ( GameTiming . ApplyingState )
return ;
2022-02-26 18:24:08 +13:00
RemoveProvidedActions ( uid , args . Equipment , component ) ;
}
private void OnHandUnequipped ( EntityUid uid , ActionsComponent component , DidUnequipHandEvent args )
{
2023-09-23 04:49:39 -04:00
if ( GameTiming . ApplyingState )
return ;
2022-02-26 18:24:08 +13:00
RemoveProvidedActions ( uid , args . Unequipped , component ) ;
}
#endregion
2023-09-23 04:49:39 -04:00
public void SetEntityIcon ( EntityUid uid , EntityUid ? icon , BaseActionComponent ? action = null )
{
if ( ! Resolve ( uid , ref action ) )
return ;
action . EntityIcon = icon ;
Dirty ( uid , action ) ;
}
2022-02-26 18:24:08 +13:00
}