using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Actions.Components; using Content.Shared.Actions.Events; 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.Mind; using Content.Shared.Rejuvenate; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Actions; public abstract class SharedActionsSystem : EntitySystem { [Dependency] protected readonly IGameTiming GameTiming = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActionContainerSystem _actionContainer = default!; [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; [Dependency] private readonly RotateToFaceSystem _rotateToFace = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; private EntityQuery _actionQuery; private EntityQuery _actionsQuery; private EntityQuery _mindQuery; public override void Initialize() { base.Initialize(); _actionQuery = GetEntityQuery(); _actionsQuery = GetEntityQuery(); _mindQuery = GetEntityQuery(); SubscribeLocalEvent(OnActionMapInit); SubscribeLocalEvent(OnActionShutdown); SubscribeLocalEvent(OnActionCompChange); SubscribeLocalEvent(OnRelayActionCompChange); SubscribeLocalEvent(OnDidEquip); SubscribeLocalEvent(OnHandEquipped); SubscribeLocalEvent(OnDidUnequip); SubscribeLocalEvent(OnHandUnequipped); SubscribeLocalEvent(OnRejuventate); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnValidate); SubscribeLocalEvent(OnInstantValidate); SubscribeLocalEvent(OnEntityValidate); SubscribeLocalEvent(OnWorldValidate); SubscribeLocalEvent(OnInstantGetEvent); SubscribeLocalEvent(OnEntityGetEvent); SubscribeLocalEvent(OnWorldGetEvent); SubscribeLocalEvent(OnInstantSetEvent); SubscribeLocalEvent(OnEntitySetEvent); SubscribeLocalEvent(OnWorldSetEvent); SubscribeLocalEvent(OnEntitySetTarget); SubscribeLocalEvent(OnWorldSetTarget); SubscribeAllEvent(OnActionRequest); } private void OnActionMapInit(Entity ent, ref MapInitEvent args) { var comp = ent.Comp; comp.OriginalIconColor = comp.IconColor; DirtyField(ent, ent.Comp, nameof(ActionComponent.OriginalIconColor)); } private void OnActionShutdown(Entity ent, ref ComponentShutdown args) { if (ent.Comp.AttachedEntity is {} user && !TerminatingOrDeleted(user)) RemoveAction(user, (ent, ent)); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { foreach (var actionId in ent.Comp.Actions) { RemoveAction((ent, ent), actionId); } } private void OnGetState(Entity ent, ref ComponentGetState args) { args.State = new ActionsComponentState(GetNetEntitySet(ent.Comp.Actions)); } /// /// Resolving an action's , only returning a value if it exists and has it. /// public Entity? GetAction(Entity? action, bool logError = true) { if (action is not {} ent || Deleted(ent)) return null; if (!_actionQuery.Resolve(ent, ref ent.Comp, logError)) return null; return (ent, ent.Comp); } public void SetCooldown(Entity? action, TimeSpan start, TimeSpan end) { if (GetAction(action) is not {} ent) return; ent.Comp.Cooldown = new ActionCooldown { Start = start, End = end }; DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown)); } public void RemoveCooldown(Entity? action) { if (GetAction(action) is not {} ent) return; ent.Comp.Cooldown = null; DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown)); } /// /// Starts a cooldown starting now, lasting for cooldown seconds. /// public void SetCooldown(Entity? action, TimeSpan cooldown) { var start = GameTiming.CurTime; SetCooldown(action, start, start + cooldown); } public void ClearCooldown(Entity? action) { if (GetAction(action) is not {} ent) return; if (ent.Comp.Cooldown is not {} cooldown) return; ent.Comp.Cooldown = new ActionCooldown { Start = cooldown.Start, End = GameTiming.CurTime }; DirtyField(ent, ent.Comp, nameof(ActionComponent.Cooldown)); } /// /// Sets the cooldown for this action only if it is bigger than the one it already has. /// public void SetIfBiggerCooldown(Entity? action, TimeSpan cooldown) { if (GetAction(action) is not {} ent || cooldown < TimeSpan.Zero) return; var start = GameTiming.CurTime; var end = start + cooldown; if (ent.Comp.Cooldown?.End > end) return; SetCooldown((ent, ent), start, end); } /// /// Set an action's cooldown to its use delay, if it has one. /// If there is no set use delay this does nothing. /// public void StartUseDelay(Entity? action) { if (GetAction(action) is not {} ent || ent.Comp.UseDelay is not {} delay) return; SetCooldown((ent, ent), delay); } public void SetUseDelay(Entity? action, TimeSpan? delay) { if (GetAction(action) is not {} ent || ent.Comp.UseDelay == delay) return; ent.Comp.UseDelay = delay; UpdateAction(ent); DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay)); } public void ReduceUseDelay(Entity? action, TimeSpan? lowerDelay) { if (GetAction(action) is not {} ent) return; if (ent.Comp.UseDelay != null && lowerDelay != null) ent.Comp.UseDelay -= lowerDelay; if (ent.Comp.UseDelay < TimeSpan.Zero) ent.Comp.UseDelay = null; UpdateAction(ent); DirtyField(ent, ent.Comp, nameof(ActionComponent.UseDelay)); } private void OnRejuventate(Entity ent, ref RejuvenateEvent args) { foreach (var act in ent.Comp.Actions) { ClearCooldown(act); } } #region ComponentStateManagement public virtual void UpdateAction(Entity ent) { // See client-side code. } public void SetToggled(Entity? action, bool toggled) { if (GetAction(action) is not {} ent || ent.Comp.Toggled == toggled) return; ent.Comp.Toggled = toggled; UpdateAction(ent); DirtyField(ent, ent.Comp, nameof(ActionComponent.Toggled)); } public void SetEnabled(Entity? action, bool enabled) { if (GetAction(action) is not {} ent || ent.Comp.Enabled == enabled) return; ent.Comp.Enabled = enabled; UpdateAction(ent); DirtyField(ent, ent.Comp, nameof(ActionComponent.Enabled)); } #endregion #region Execution /// /// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it /// will raise the relevant /// private void OnActionRequest(RequestPerformActionEvent ev, EntitySessionEventArgs args) { if (args.SenderSession.AttachedEntity is not { } user) return; if (!_actionsQuery.TryComp(user, out var component)) return; var actionEnt = GetEntity(ev.Action); if (!TryComp(actionEnt, out MetaDataComponent? metaData)) return; var name = Name(actionEnt, metaData); // Does the user actually have the requested action? if (!component.Actions.Contains(actionEnt)) { _adminLogger.Add(LogType.Action, $"{ToPrettyString(user):user} attempted to perform an action that they do not have: {name}."); return; } if (GetAction(actionEnt) is not {} action) return; DebugTools.Assert(action.Comp.AttachedEntity == user); if (!action.Comp.Enabled) return; var curTime = GameTiming.CurTime; if (IsCooldownActive(action, curTime)) return; // check for action use prevention // TODO: make code below use this event with a dedicated component var attemptEv = new ActionAttemptEvent(user); RaiseLocalEvent(action, ref attemptEv); if (attemptEv.Cancelled) return; // Validate request by checking action blockers and the like var provider = action.Comp.Container ?? user; var validateEv = new ActionValidateEvent() { Input = ev, User = user, Provider = provider }; RaiseLocalEvent(action, ref validateEv); if (validateEv.Invalid) return; // All checks passed. Perform the action! PerformAction((user, component), action); } private void OnValidate(Entity ent, ref ActionValidateEvent args) { if ((ent.Comp.CheckConsciousness && !_actionBlocker.CanConsciouslyPerformAction(args.User)) || (ent.Comp.CheckCanInteract && !_actionBlocker.CanInteract(args.User, null))) args.Invalid = true; } private void OnInstantValidate(Entity ent, ref ActionValidateEvent args) { _adminLogger.Add(LogType.Action, $"{ToPrettyString(args.User):user} is performing the {Name(ent):action} action provided by {ToPrettyString(args.Provider):provider}."); } private void OnEntityValidate(Entity ent, ref ActionValidateEvent args) { // let WorldTargetAction handle it if (ent.Comp.Event is not {} ev) { DebugTools.Assert(HasComp(ent), $"Entity-world targeting action {ToPrettyString(ent)} requires WorldTargetActionComponent"); return; } if (args.Input.EntityTarget is not {} netTarget) { args.Invalid = true; return; } var user = args.User; var target = GetEntity(netTarget); var targetWorldPos = _transform.GetWorldPosition(target); if (ent.Comp.RotateOnUse) _rotateToFace.TryFaceCoordinates(user, targetWorldPos); if (!ValidateEntityTarget(user, target, ent)) return; _adminLogger.Add(LogType.Action, $"{ToPrettyString(user):user} is performing the {Name(ent):action} action (provided by {ToPrettyString(args.Provider):provider}) targeted at {ToPrettyString(target):target}."); ev.Target = target; } private void OnWorldValidate(Entity ent, ref ActionValidateEvent args) { if (args.Input.EntityCoordinatesTarget is not { } netTarget) { args.Invalid = true; return; } var user = args.User; var target = GetCoordinates(netTarget); if (ent.Comp.RotateOnUse) _rotateToFace.TryFaceCoordinates(user, _transform.ToMapCoordinates(target).Position); if (!ValidateWorldTarget(user, target, ent)) return; // if the client specified an entity it needs to be valid var targetEntity = GetEntity(args.Input.EntityTarget); if (targetEntity != null && ( !TryComp(ent, out var entTarget) || !ValidateEntityTarget(user, targetEntity.Value, (ent, entTarget)))) { args.Invalid = true; return; } _adminLogger.Add(LogType.Action, $"{ToPrettyString(user):user} is performing the {Name(ent):action} action (provided by {args.Provider}) targeting {targetEntity} at {target:target}."); if (ent.Comp.Event is {} ev) { ev.Target = target; ev.Entity = targetEntity; } } public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity ent) { var (uid, comp) = ent; if (!target.IsValid() || Deleted(target)) return false; if (_whitelist.IsWhitelistFail(comp.Whitelist, target)) return false; if (_whitelist.IsBlacklistPass(comp.Blacklist, target)) return false; if (_actionQuery.Comp(uid).CheckCanInteract && !_actionBlocker.CanInteract(user, target)) return false; if (user == target) return comp.CanTargetSelf; var targetAction = Comp(uid); // not using the ValidateBaseTarget logic since its raycast fails if the target is e.g. a wall if (targetAction.CheckCanAccess) return _interaction.InRangeAndAccessible(user, target, range: targetAction.Range); // if not just checking pure range, let stored entities be targeted by actions // if it's out of range it probably isn't stored anyway... return _interaction.CanAccessViaStorage(user, target); } public bool ValidateWorldTarget(EntityUid user, EntityCoordinates target, Entity ent) { var targetAction = Comp(ent); return ValidateBaseTarget(user, target, (ent, targetAction)); } private bool ValidateBaseTarget(EntityUid user, EntityCoordinates coords, Entity ent) { var comp = ent.Comp; if (comp.CheckCanAccess) return _interaction.InRangeUnobstructed(user, coords, range: comp.Range); // even if we don't check for obstructions, we may still need to check the range. var xform = Transform(user); if (xform.MapID != _transform.GetMapId(coords)) return false; if (comp.Range <= 0) return true; return _transform.InRange(coords, xform.Coordinates, comp.Range); } private void OnInstantGetEvent(Entity ent, ref ActionGetEventEvent args) { if (ent.Comp.Event is {} ev) args.Event = ev; } private void OnEntityGetEvent(Entity ent, ref ActionGetEventEvent args) { if (ent.Comp.Event is {} ev) args.Event = ev; } private void OnWorldGetEvent(Entity ent, ref ActionGetEventEvent args) { if (ent.Comp.Event is {} ev) args.Event = ev; } private void OnInstantSetEvent(Entity ent, ref ActionSetEventEvent args) { if (args.Event is InstantActionEvent ev) { ent.Comp.Event = ev; args.Handled = true; } } private void OnEntitySetEvent(Entity ent, ref ActionSetEventEvent args) { if (args.Event is EntityTargetActionEvent ev) { ent.Comp.Event = ev; args.Handled = true; } } private void OnWorldSetEvent(Entity ent, ref ActionSetEventEvent args) { if (args.Event is WorldTargetActionEvent ev) { ent.Comp.Event = ev; args.Handled = true; } } private void OnEntitySetTarget(Entity ent, ref ActionSetTargetEvent args) { if (ent.Comp.Event is {} ev) { ev.Target = args.Target; args.Handled = true; } } private void OnWorldSetTarget(Entity ent, ref ActionSetTargetEvent args) { if (ent.Comp.Event is {} ev) { ev.Target = Transform(args.Target).Coordinates; // only set Entity if the action also has EntityTargetAction ev.Entity = HasComp(ent) ? args.Target : null; args.Handled = true; } } /// /// Perform an action, bypassing validation checks. /// /// The entity performing the action /// The action being performed /// An event override to perform. If null, uses /// If false, prevents playing the action's sound on the client public void PerformAction(Entity performer, Entity action, BaseActionEvent? actionEvent = null, bool predicted = true) { var handled = false; var toggledBefore = action.Comp.Toggled; // Note that attached entity and attached container are allowed to be null here. if (action.Comp.AttachedEntity != null && action.Comp.AttachedEntity != performer) { Log.Error($"{ToPrettyString(performer)} is attempting to perform an action {ToPrettyString(action)} that is attached to another entity {ToPrettyString(action.Comp.AttachedEntity)}"); return; } actionEvent ??= GetEvent(action); if (actionEvent is not {} ev) return; ev.Performer = performer; // This here is required because of client-side prediction (RaisePredictiveEvent results in event re-use). ev.Handled = false; var target = performer.Owner; ev.Performer = performer; ev.Action = action; if (!action.Comp.RaiseOnUser && action.Comp.Container is {} container && !_mindQuery.HasComp(container)) target = container; if (action.Comp.RaiseOnAction) target = action; RaiseLocalEvent(target, (object) ev, broadcast: true); handled = ev.Handled; if (!handled) return; // no interaction occurred. // play sound, reduce charges, start cooldown if (ev?.Toggle == true) SetToggled((action, action), !action.Comp.Toggled); _audio.PlayPredicted(action.Comp.Sound, performer, predicted ? performer : null); // TODO: move to ActionCooldown ActionPerformedEvent? RemoveCooldown((action, action)); StartUseDelay((action, action)); UpdateAction(action); var performed = new ActionPerformedEvent(performer); RaiseLocalEvent(action, ref performed); } #endregion #region AddRemoveActions public EntityUid? AddAction(EntityUid performer, [ForbidLiteral] string? actionPrototypeId, EntityUid container = default, ActionsComponent? component = null) { EntityUid? actionId = null; AddAction(performer, ref actionId, out _, actionPrototypeId, container, component); return actionId; } /// /// Adds an action to an action holder. If the given entity does not exist, it will attempt to spawn one. /// If the holder has no actions component, this will give them one. /// /// Entity to receive the actions /// Action entity to add /// The 's action component of /// The action entity prototype id to use if is invalid. /// The entity that contains/enables this action (e.g., flashlight). public bool AddAction(EntityUid performer, [NotNullWhen(true)] ref EntityUid? actionId, [ForbidLiteral] string? actionPrototypeId, EntityUid container = default, ActionsComponent? component = null) { return AddAction(performer, ref actionId, out _, actionPrototypeId, container, component); } /// public bool AddAction(EntityUid performer, [NotNullWhen(true)] ref EntityUid? actionId, [NotNullWhen(true)] out ActionComponent? action, [ForbidLiteral] string? actionPrototypeId, EntityUid container = default, ActionsComponent? component = null) { if (!container.IsValid()) container = performer; if (!_actionContainer.EnsureAction(container, ref actionId, out action, actionPrototypeId)) return false; return AddActionDirect((performer, component), (actionId.Value, action)); } /// /// Adds a pre-existing action. /// public bool AddAction(Entity performer, Entity action, Entity container) { if (GetAction(action) is not {} ent) return false; if (ent.Comp.Container != container.Owner || !Resolve(container, ref container.Comp) || !container.Comp.Container.Contains(ent)) { Log.Error($"Attempted to add an action with an invalid container: {ToPrettyString(ent)}"); return false; } return AddActionDirect(performer, (ent, ent)); } /// /// Adds a pre-existing action. This also bypasses the requirement that the given action must be stored in a /// valid action container. /// public bool AddActionDirect(Entity performer, Entity? action) { if (GetAction(action) is not {} ent) return false; DebugTools.Assert(ent.Comp.Container == null || (TryComp(ent.Comp.Container, out ActionsContainerComponent? containerComp) && containerComp.Container.Contains(ent))); if (ent.Comp.AttachedEntity is {} user) RemoveAction(user, (ent, ent)); // TODO: make this an event bruh if (ent.Comp.StartDelay && ent.Comp.UseDelay != null) SetCooldown((ent, ent), ent.Comp.UseDelay.Value); DebugTools.AssertOwner(performer, performer.Comp); performer.Comp ??= EnsureComp(performer); ent.Comp.AttachedEntity = performer; DirtyField(ent, ent.Comp, nameof(ActionComponent.AttachedEntity)); performer.Comp.Actions.Add(ent); Dirty(performer, performer.Comp); ActionAdded((performer, performer.Comp), (ent, ent.Comp)); return true; } /// /// This method gets called after a new action got added. /// protected virtual void ActionAdded(Entity performer, Entity action) { // See client-side system for UI code. } /// /// Grant pre-existing actions. If the entity has no action component, this will give them one. /// /// Entity to receive the actions /// The actions to add /// The entity that enables these actions (e.g., flashlight). May be null (innate actions). public void GrantActions(Entity performer, IEnumerable actions, Entity container) { if (!Resolve(container, ref container.Comp)) return; DebugTools.AssertOwner(performer, performer.Comp); performer.Comp ??= EnsureComp(performer); foreach (var actionId in actions) { AddAction(performer, actionId, container); } } /// /// Grants all actions currently contained in some action-container. If the target entity has no action /// component, this will give them one. /// /// Entity to receive the actions /// The entity that contains thee actions. public void GrantContainedActions(Entity performer, Entity container) { if (!Resolve(container, ref container.Comp)) return; performer.Comp ??= EnsureComp(performer); foreach (var actionId in container.Comp.Container.ContainedEntities) { if (GetAction(actionId) is {} action) AddActionDirect(performer, (action, action)); } } /// /// Grants the provided action from the container to the target entity. If the target entity has no action /// component, this will give them one. /// /// /// /// public void GrantContainedAction(Entity performer, Entity container, EntityUid actionId) { if (!Resolve(container, ref container.Comp)) return; performer.Comp ??= EnsureComp(performer); AddActionDirect(performer, actionId); } public IEnumerable> GetActions(EntityUid holderId, ActionsComponent? actions = null) { if (!Resolve(holderId, ref actions, false)) yield break; foreach (var actionId in actions.Actions) { if (GetAction(actionId) is not {} ent) continue; yield return ent; } } /// /// Remove any actions that were enabled by some other entity. Useful when unequiping items that grant actions. /// public void RemoveProvidedActions(EntityUid performer, EntityUid container, ActionsComponent? comp = null) { if (!Resolve(performer, ref comp, false)) return; foreach (var actionId in comp.Actions.ToArray()) { if (GetAction(actionId) is not {} ent) return; if (ent.Comp.Container == container) RemoveAction((performer, comp), (ent, ent)); } } /// /// Removes a single provided action provided by another entity. /// public void RemoveProvidedAction(EntityUid performer, EntityUid container, EntityUid actionId, ActionsComponent? comp = null) { if (!_actionsQuery.Resolve(performer, ref comp, false) || GetAction(actionId) is not {} ent) return; if (ent.Comp.Container == container) RemoveAction((performer, comp), (ent, ent)); } /// /// Removes an action from its container, if it still exists. /// public void RemoveAction(Entity? action) { if (GetAction(action) is not {} ent || ent.Comp.AttachedEntity is not {} actions) return; if (!_actionsQuery.TryComp(actions, out var comp)) return; RemoveAction((actions, comp), (ent, ent)); } public void RemoveAction(Entity performer, Entity? action) { if (GetAction(action) is not {} ent) return; if (ent.Comp.AttachedEntity != performer.Owner) { DebugTools.Assert(!Resolve(performer, ref performer.Comp, false) || performer.Comp.LifeStage >= ComponentLifeStage.Stopping || !performer.Comp.Actions.Contains(ent.Owner)); if (!GameTiming.ApplyingState) Log.Error($"Attempted to remove an action {ToPrettyString(ent)} from an entity that it was never attached to: {ToPrettyString(performer)}. Trace: {Environment.StackTrace}"); return; } if (!_actionsQuery.Resolve(performer, ref performer.Comp, false)) { DebugTools.Assert(performer == null || TerminatingOrDeleted(performer)); ent.Comp.AttachedEntity = null; // TODO: should this delete the action since it's now orphaned? return; } performer.Comp.Actions.Remove(ent.Owner); Dirty(performer, performer.Comp); ent.Comp.AttachedEntity = null; DirtyField(ent, ent.Comp, nameof(ActionComponent.AttachedEntity)); ActionRemoved((performer, performer.Comp), ent); if (ent.Comp.Temporary) QueueDel(ent); } /// /// This method gets called after an action got removed. /// protected virtual void ActionRemoved(Entity performer, Entity action) { // See client-side system for UI code. } public bool ValidAction(Entity ent, bool canReach = true) { var (uid, comp) = ent; if (!comp.Enabled) return false; var curTime = GameTiming.CurTime; if (comp.Cooldown.HasValue && comp.Cooldown.Value.End > curTime) return false; // TODO: use event for this return canReach || Comp(ent)?.CheckCanAccess == false; } #endregion private void OnRelayActionCompChange(Entity ent, ref RelayedActionComponentChangeEvent args) { if (args.Handled) return; var ev = new AttemptRelayActionComponentChangeEvent(); RaiseLocalEvent(ent.Owner, ref ev); var target = ev.Target ?? ent.Owner; args.Handled = true; args.Toggle = true; if (!args.Action.Comp.Toggled) { EntityManager.AddComponents(target, args.Components); } else { EntityManager.RemoveComponents(target, args.Components); } } private void OnActionCompChange(Entity ent, ref ActionComponentChangeEvent args) { if (args.Handled) return; args.Handled = true; args.Toggle = true; var target = ent.Owner; if (!args.Action.Comp.Toggled) { EntityManager.AddComponents(target, args.Components); } else { EntityManager.RemoveComponents(target, args.Components); } } #region EquipHandlers private void OnDidEquip(Entity ent, ref DidEquipEvent args) { if (GameTiming.ApplyingState) return; var ev = new GetItemActionsEvent(_actionContainer, args.Equipee, args.Equipment, args.SlotFlags); RaiseLocalEvent(args.Equipment, ev); if (ev.Actions.Count == 0) return; GrantActions((ent, ent), ev.Actions, args.Equipment); } private void OnHandEquipped(Entity ent, ref DidEquipHandEvent args) { if (GameTiming.ApplyingState) return; var ev = new GetItemActionsEvent(_actionContainer, args.User, args.Equipped); RaiseLocalEvent(args.Equipped, ev); if (ev.Actions.Count == 0) return; GrantActions((ent, ent), ev.Actions, args.Equipped); } private void OnDidUnequip(EntityUid uid, ActionsComponent component, DidUnequipEvent args) { if (GameTiming.ApplyingState) return; RemoveProvidedActions(uid, args.Equipment, component); } private void OnHandUnequipped(EntityUid uid, ActionsComponent component, DidUnequipHandEvent args) { if (GameTiming.ApplyingState) return; RemoveProvidedActions(uid, args.Unequipped, component); } #endregion public void SetEntityIcon(Entity ent, EntityUid? icon) { if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.EntityIcon == icon) return; ent.Comp.EntityIcon = icon; DirtyField(ent, ent.Comp, nameof(ActionComponent.EntIcon)); } public void SetIcon(Entity ent, SpriteSpecifier? icon) { if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.Icon == icon) return; ent.Comp.Icon = icon; DirtyField(ent, ent.Comp, nameof(ActionComponent.Icon)); } public void SetIconOn(Entity ent, SpriteSpecifier? iconOn) { if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IconOn == iconOn) return; ent.Comp.IconOn = iconOn; DirtyField(ent, ent.Comp, nameof(ActionComponent.IconOn)); } public void SetIconColor(Entity ent, Color color) { if (!_actionQuery.Resolve(ent, ref ent.Comp) || ent.Comp.IconColor == color) return; ent.Comp.IconColor = color; DirtyField(ent, ent.Comp, nameof(ActionComponent.IconColor)); } /// /// Set the event of an action. /// Since the event isn't required to be serializable this is not networked. /// Only use this if it's predicted or for a clientside action. /// public void SetEvent(EntityUid uid, BaseActionEvent ev) { // now this is meta var setEv = new ActionSetEventEvent(ev); RaiseLocalEvent(uid, ref setEv); if (!setEv.Handled) Log.Error($"Tried to set event of {ToPrettyString(uid):action} but nothing handled it!"); } public BaseActionEvent? GetEvent(EntityUid uid) { DebugTools.Assert(_actionQuery.HasComp(uid), $"Entity {ToPrettyString(uid)} is missing ActionComponent"); var ev = new ActionGetEventEvent(); RaiseLocalEvent(uid, ref ev); return ev.Event; } public bool SetEventTarget(EntityUid uid, EntityUid target) { DebugTools.Assert(_actionQuery.HasComp(uid), $"Entity {ToPrettyString(uid)} is missing ActionComponent"); var ev = new ActionSetTargetEvent(target); RaiseLocalEvent(uid, ref ev); return ev.Handled; } /// /// Checks if the action has a cooldown and if it's still active /// public bool IsCooldownActive(ActionComponent action, TimeSpan? curTime = null) { // TODO: Check for charge recovery timer return action.Cooldown.HasValue && action.Cooldown.Value.End > curTime; } /// /// Marks the action as temporary. /// Temporary actions get deleted upon being removed from an entity. /// public void SetTemporary(Entity ent, bool temporary) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Temporary = temporary; Dirty(ent); } }