Patched Actions Rework (#6899)
* Rejig Actions * fix merge errors * lambda-b-gon * fix PAI, add innate actions * Revert "fix PAI, add innate actions" This reverts commit 4b501ac083e979e31ebd98d7b98077e0dbdd344b. * Just fix by making nullable. if only require: true actually did something somehow. * Make AddActions() ensure an actions component and misc comments * misc cleanup * Limit range even when not checking for obstructions * remove old guardian code * rename function and make EntityUid nullable * fix magboot bug * fix action search menu * make targeting toggle all equivalent actions * fix combat popups (enabling <-> disabling) * fix networking * Allow action locking * prevent telepathy
This commit is contained in:
11
Content.Client/Actions/ActionEvents.cs
Normal file
11
Content.Client/Actions/ActionEvents.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
|
||||
namespace Content.Client.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slow.
|
||||
/// </summary>
|
||||
public sealed class FillActionSlotEvent : EntityEventArgs
|
||||
{
|
||||
public ActionType? Action;
|
||||
}
|
||||
@@ -1,20 +1,64 @@
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Construction;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Outline;
|
||||
using Content.Client.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Utility;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ActionsSystem : EntitySystem
|
||||
public sealed class ActionsSystem : SharedActionsSystem
|
||||
{
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly ISerializationManager _serializationManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
|
||||
[Dependency] private readonly TargetOutlineSystem _targetOutline = default!;
|
||||
|
||||
// TODO Redo assignments, including allowing permanent user configurable slot assignments.
|
||||
/// <summary>
|
||||
/// Current assignments for all hotbars / slots for this entity.
|
||||
/// </summary>
|
||||
public ActionAssignments Assignments = new(Hotbars, Slots);
|
||||
|
||||
public const byte Hotbars = 9;
|
||||
public const byte Slots = 10;
|
||||
|
||||
public bool UIDirty;
|
||||
|
||||
public ActionsUI? Ui;
|
||||
private EntityUid? _highlightedEntity;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -64,12 +108,207 @@ namespace Content.Client.Actions
|
||||
HandleChangeHotbarKeybind(8))
|
||||
// when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to
|
||||
// take priority before any other systems handle the click.
|
||||
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse),
|
||||
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
|
||||
typeof(ConstructionSystem), typeof(DragDropSystem))
|
||||
.BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
|
||||
.Register<ActionsSystem>();
|
||||
|
||||
SubscribeLocalEvent<ClientActionsComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached());
|
||||
SubscribeLocalEvent<ClientActionsComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached());
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleState);
|
||||
}
|
||||
|
||||
protected override void Dirty(ActionType action)
|
||||
{
|
||||
// Should only ever receive component states for attached player's component.
|
||||
// --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players.
|
||||
if (action.AttachedEntity != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
base.Dirty(action);
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
// Client only needs to care about local player.
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (args.Current is not ActionsComponentState state)
|
||||
return;
|
||||
|
||||
var serverActions = new SortedSet<ActionType>(state.Actions);
|
||||
|
||||
foreach (var act in component.Actions.ToList())
|
||||
{
|
||||
if (act.ClientExclusive)
|
||||
continue;
|
||||
|
||||
if (!serverActions.TryGetValue(act, out var serverAct))
|
||||
{
|
||||
component.Actions.Remove(act);
|
||||
if (act.AutoRemove && !(Ui?.Locked ?? false))
|
||||
Assignments.Remove(act);
|
||||
continue;
|
||||
}
|
||||
|
||||
act.CopyFrom(serverAct);
|
||||
serverActions.Remove(serverAct);
|
||||
|
||||
if (act is EntityTargetAction entAct)
|
||||
{
|
||||
entAct.Whitelist?.UpdateRegistrations();
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that remains is a new action
|
||||
foreach (var newAct in serverActions)
|
||||
{
|
||||
if (newAct is EntityTargetAction entAct)
|
||||
entAct.Whitelist?.UpdateRegistrations();
|
||||
|
||||
// We create a new action, not just sorting a reference to the state's action.
|
||||
component.Actions.Add((ActionType) newAct.Clone());
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the item slot (inventory or hand) that contains this item
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void HighlightItemSlot(EntityUid item)
|
||||
{
|
||||
StopHighlightingItemSlot();
|
||||
|
||||
_highlightedEntity = item;
|
||||
_itemSlotManager.HighlightEntity(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops highlighting any item slots we are currently highlighting.
|
||||
/// </summary>H
|
||||
public void StopHighlightingItemSlot()
|
||||
{
|
||||
if (_highlightedEntity == null)
|
||||
return;
|
||||
|
||||
_itemSlotManager.UnHighlightEntity(_highlightedEntity.Value);
|
||||
_highlightedEntity = null;
|
||||
}
|
||||
|
||||
protected override void AddActionInternal(ActionsComponent comp, ActionType action)
|
||||
{
|
||||
// Sometimes the client receives actions from the server, before predicting that newly added components will add
|
||||
// their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
|
||||
// action.Toggled), we will remove duplicates:
|
||||
if (comp.Actions.TryGetValue(action, out var existing))
|
||||
{
|
||||
comp.Actions.Remove(existing);
|
||||
Assignments.Replace(existing, action);
|
||||
}
|
||||
|
||||
comp.Actions.Add(action);
|
||||
}
|
||||
|
||||
public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
base.AddAction(uid, action, provider, comp, dirty);
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void RemoveActions(EntityUid uid, IEnumerable<ActionType> actions, ActionsComponent? comp = null, bool dirty = true)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
base.RemoveActions(uid, actions, comp, dirty);
|
||||
|
||||
foreach (var act in actions)
|
||||
{
|
||||
if (act.AutoRemove && !(Ui?.Locked ?? false))
|
||||
Assignments.Remove(act);
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
// avoid updating GUI when doing predictions & resetting state.
|
||||
if (UIDirty)
|
||||
{
|
||||
UIDirty = false;
|
||||
UpdateUI();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed hotbar (and menu) based on current state of actions.
|
||||
/// </summary>
|
||||
public void UpdateUI()
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
foreach (var action in Ui.Component.Actions)
|
||||
{
|
||||
if (action.AutoPopulate && !Assignments.Assignments.ContainsKey(action))
|
||||
Assignments.AutoPopulate(action, Ui.SelectedHotbar, false);
|
||||
}
|
||||
|
||||
// get rid of actions that are no longer available to the user
|
||||
foreach (var (action, index) in Assignments.Assignments.ToList())
|
||||
{
|
||||
if (index.Count == 0)
|
||||
{
|
||||
Assignments.Assignments.Remove(action);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action.AutoRemove && !Ui.Locked && !Ui.Component.Actions.Contains(action))
|
||||
Assignments.ClearSlot(index[0].Hotbar, index[0].Slot, false);
|
||||
}
|
||||
|
||||
Assignments.PreventAutoPopulate.RemoveWhere(action => !Ui.Component.Actions.Contains(action));
|
||||
|
||||
Ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
Ui?.HandleHotbarKeybind(slot, args);
|
||||
}
|
||||
|
||||
public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
Ui?.HandleChangeHotbarKeybind(hotbar, args);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
if (Ui == null) return;
|
||||
_uiManager.StateRoot.RemoveChild(Ui);
|
||||
Ui = null;
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
Assignments = new(Hotbars, Slots);
|
||||
Ui = new ActionsUI(this, component);
|
||||
_uiManager.StateRoot.AddChild(Ui);
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -85,9 +324,9 @@ namespace Content.Client.Actions
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (playerEntity == null ||
|
||||
!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
|
||||
!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
|
||||
|
||||
actionsComponent.HandleHotbarKeybind(slot, args);
|
||||
HandleHotbarKeybind(slot, args);
|
||||
return true;
|
||||
}, false);
|
||||
}
|
||||
@@ -98,28 +337,363 @@ namespace Content.Client.Actions
|
||||
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
if (!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
|
||||
actionsComponent.HandleChangeHotbarKeybind(hotbar, args);
|
||||
HandleChangeHotbarKeybind(hotbar, args);
|
||||
return true;
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
|
||||
return actionsComponent.TargetingOnUse(args);
|
||||
}
|
||||
|
||||
private void ToggleActionsMenu()
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return;
|
||||
Ui?.ToggleActionsMenu();
|
||||
}
|
||||
|
||||
actionsComponent.ToggleActionsMenu();
|
||||
/// <summary>
|
||||
/// A action slot was pressed. This either performs the action or toggles the targeting mode.
|
||||
/// </summary>
|
||||
internal void OnSlotPressed(ActionSlot slot)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
|
||||
return;
|
||||
|
||||
if (slot.Action.Provider != null && Deleted(slot.Action.Provider))
|
||||
return;
|
||||
|
||||
if (slot.Action is not InstantAction instantAction)
|
||||
{
|
||||
// for target actions, we go into "select target" mode, we don't
|
||||
// message the server until we actually pick our target.
|
||||
|
||||
// if we're clicking the same thing we're already targeting for, then we simply cancel
|
||||
// targeting
|
||||
Ui.ToggleTargeting(slot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot.Action.ClientExclusive)
|
||||
{
|
||||
if (instantAction.Event != null)
|
||||
instantAction.Event.Performer = user;
|
||||
|
||||
PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new RequestPerformActionEvent(instantAction);
|
||||
EntityManager.RaisePredictiveEvent(request);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TargetingCancel(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (Ui?.SelectingTargetFor?.Action == null)
|
||||
return false;
|
||||
|
||||
Ui.StopTargeting();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
|
||||
/// </summary>
|
||||
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (Ui?.SelectingTargetFor?.Action is not TargetedAction action)
|
||||
return false;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
|
||||
return false;
|
||||
|
||||
if (!TryComp(user, out ActionsComponent? comp))
|
||||
return false;
|
||||
|
||||
// Is the action currently valid?
|
||||
if (!action.Enabled
|
||||
|| action.Charges != null && action.Charges == 0
|
||||
|| action.Cooldown.HasValue && action.Cooldown.Value.End > GameTiming.CurTime)
|
||||
{
|
||||
// The user is targeting with this action, but it is not valid. Maybe mark this click as
|
||||
// handled and prevent further interactions.
|
||||
return !action.InteractOnMiss;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case WorldTargetAction mapTarget:
|
||||
return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss;
|
||||
|
||||
case EntityTargetAction entTarget:
|
||||
return TargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss;
|
||||
|
||||
default:
|
||||
Logger.Error($"Unknown targeting action: {action.GetType()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp)
|
||||
{
|
||||
var coords = args.Coordinates.ToMap(EntityManager);
|
||||
|
||||
if (!ValidateWorldTarget(user, coords, action))
|
||||
{
|
||||
// Invalid target.
|
||||
if (action.DeselectOnMiss)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (action.Event != null)
|
||||
{
|
||||
action.Event.Target = coords;
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
|
||||
|
||||
if (!action.Repeat)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TargetEntity(in PointerInputCmdHandler.PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp)
|
||||
{
|
||||
if (!ValidateEntityTarget(user, args.EntityUid, action))
|
||||
{
|
||||
if (action.DeselectOnMiss)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (action.Event != null)
|
||||
{
|
||||
action.Event.Target = args.EntityUid;
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
|
||||
|
||||
if (!action.Repeat)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action)
|
||||
{
|
||||
var performedAction = action.Sound != null
|
||||
|| !string.IsNullOrWhiteSpace(action.UserPopup)
|
||||
|| !string.IsNullOrWhiteSpace(action.Popup);
|
||||
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return performedAction;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action.UserPopup))
|
||||
{
|
||||
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
|
||||
? Loc.GetString(action.UserPopup)
|
||||
: Loc.GetString(action.UserPopup + action.PopupToggleSuffix);
|
||||
|
||||
_popupSystem.PopupEntity(msg, user);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(action.Popup))
|
||||
{
|
||||
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
|
||||
? Loc.GetString(action.Popup)
|
||||
: Loc.GetString(action.Popup + action.PopupToggleSuffix);
|
||||
|
||||
_popupSystem.PopupEntity(msg, user);
|
||||
}
|
||||
|
||||
if (action.Sound != null)
|
||||
SoundSystem.Play(Filter.Local(), action.Sound.GetSound(), user, action.AudioParams);
|
||||
|
||||
return performedAction;
|
||||
}
|
||||
|
||||
internal void StopTargeting()
|
||||
{
|
||||
_targetOutline.Disable();
|
||||
_interactionOutline.SetEnabled(true);
|
||||
|
||||
if (!_overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay) || handOverlay == null)
|
||||
return;
|
||||
|
||||
handOverlay.IconOverride = null;
|
||||
handOverlay.EntityOverride = null;
|
||||
}
|
||||
|
||||
internal void StartTargeting(TargetedAction action)
|
||||
{
|
||||
// override "held-item" overlay
|
||||
if (action.TargetingIndicator
|
||||
&& _overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay)
|
||||
&& handOverlay != null)
|
||||
{
|
||||
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
|
||||
{
|
||||
handOverlay.EntityOverride = action.Provider;
|
||||
}
|
||||
else if (action.Toggled && action.IconOn != null)
|
||||
handOverlay.IconOverride = action.IconOn.Frame0();
|
||||
else if (action.Icon != null)
|
||||
handOverlay.IconOverride = action.Icon.Frame0();
|
||||
}
|
||||
|
||||
// TODO: allow world-targets to check valid positions. E.g., maybe:
|
||||
// - Draw a red/green ghost entity
|
||||
// - Add a yes/no checkmark where the HandItemOverlay usually is
|
||||
|
||||
// Highlight valid entity targets
|
||||
if (action is not EntityTargetAction entityAction)
|
||||
return;
|
||||
|
||||
Func<EntityUid, bool>? predicate = null;
|
||||
|
||||
if (!entityAction.CanTargetSelf)
|
||||
predicate = e => e != entityAction.AttachedEntity;
|
||||
|
||||
var range = entityAction.CheckCanAccess ? action.Range : -1;
|
||||
|
||||
_interactionOutline.SetEnabled(false);
|
||||
_targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
|
||||
}
|
||||
|
||||
internal void TryFillSlot(byte hotbar, byte index)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
var fillEvent = new FillActionSlotEvent();
|
||||
RaiseLocalEvent(Ui.Component.Owner, fillEvent, broadcast: true);
|
||||
|
||||
if (fillEvent.Action == null)
|
||||
return;
|
||||
|
||||
fillEvent.Action.ClientExclusive = true;
|
||||
fillEvent.Action.Temporary = true;
|
||||
|
||||
Ui.Component.Actions.Add(fillEvent.Action);
|
||||
Assignments.AssignSlot(hotbar, index, fillEvent.Action);
|
||||
|
||||
Ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void SaveActionAssignments(string path)
|
||||
{
|
||||
// Disabled until YamlMappingFix's sandbox issues are resolved.
|
||||
|
||||
/*
|
||||
// Currently only tested with temporary innate actions (i.e., mapping actions). No guarantee it works with
|
||||
// other actions. If its meant to be used for full game state saving/loading, the entity that provides
|
||||
// actions needs to keep the same uid.
|
||||
|
||||
var sequence = new SequenceDataNode();
|
||||
|
||||
foreach (var (action, assigns) in Assignments.Assignments)
|
||||
{
|
||||
var slot = new MappingDataNode();
|
||||
slot.Add("action", _serializationManager.WriteValue(action));
|
||||
slot.Add("assignments", _serializationManager.WriteValue(assigns));
|
||||
sequence.Add(slot);
|
||||
}
|
||||
|
||||
using var writer = _resourceManager.UserData.OpenWriteText(new ResourcePath(path).ToRootedPath());
|
||||
var stream = new YamlStream { new(sequence.ToSequenceNode()) };
|
||||
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load actions and their toolbar assignments from a file.
|
||||
/// </summary>
|
||||
public void LoadActionAssignments(string path, bool userData)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
var file = new ResourcePath(path).ToRootedPath();
|
||||
TextReader reader = userData
|
||||
? _resourceManager.UserData.OpenText(file)
|
||||
: _resourceManager.ContentFileReadText(file);
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
|
||||
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
|
||||
return;
|
||||
|
||||
foreach (var (action, assigns) in Assignments.Assignments)
|
||||
{
|
||||
foreach (var index in assigns)
|
||||
{
|
||||
Assignments.ClearSlot(index.Hotbar, index.Slot, true);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in sequence.Sequence)
|
||||
{
|
||||
if (entry is not MappingDataNode map)
|
||||
continue;
|
||||
|
||||
if (!map.TryGet("action", out var actionNode))
|
||||
continue;
|
||||
|
||||
var action = _serializationManager.ReadValueCast<ActionType>(typeof(ActionType), actionNode);
|
||||
if (action == null)
|
||||
continue;
|
||||
|
||||
if (Ui.Component.Actions.TryGetValue(action, out var existingAction))
|
||||
{
|
||||
existingAction.CopyFrom(action);
|
||||
action = existingAction;
|
||||
}
|
||||
else
|
||||
Ui.Component.Actions.Add(action);
|
||||
|
||||
if (!map.TryGet("assignments", out var assignmentNode))
|
||||
continue;
|
||||
|
||||
var assignments = _serializationManager.ReadValueCast<List<(byte Hotbar, byte Slot)>>(typeof(List<(byte Hotbar, byte Slot)>), assignmentNode);
|
||||
if (assignments == null)
|
||||
continue;
|
||||
|
||||
foreach (var index in assignments)
|
||||
{
|
||||
Assignments.AssignSlot(index.Hotbar, index.Slot, action);
|
||||
}
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
public struct ActionAssignment : IEquatable<ActionAssignment>
|
||||
{
|
||||
private readonly ActionType _actionType;
|
||||
private readonly ItemActionType _itemActionType;
|
||||
private readonly EntityUid _item;
|
||||
public Assignment Assignment { get; private init; }
|
||||
|
||||
private ActionAssignment(Assignment assignment, ActionType actionType, ItemActionType itemActionType, EntityUid item)
|
||||
{
|
||||
Assignment = assignment;
|
||||
_actionType = actionType;
|
||||
_itemActionType = itemActionType;
|
||||
_item = item;
|
||||
}
|
||||
|
||||
/// <param name="actionType">the action type, if our Assignment is Assignment.Action</param>
|
||||
/// <returns>true only if our Assignment is Assignment.Action</returns>
|
||||
public bool TryGetAction(out ActionType actionType)
|
||||
{
|
||||
actionType = _actionType;
|
||||
return Assignment == Assignment.Action;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithoutItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithoutItem</returns>
|
||||
public bool TryGetItemActionWithoutItem(out ItemActionType itemActionType)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
return Assignment == Assignment.ItemActionWithoutItem;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <param name="item">the item UID providing the action, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithItem</returns>
|
||||
public bool TryGetItemActionWithItem(out ItemActionType itemActionType, out EntityUid item)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
item = _item;
|
||||
return Assignment == Assignment.ItemActionWithItem;
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ActionType actionType)
|
||||
{
|
||||
return new(Assignment.Action, actionType, default, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType)
|
||||
{
|
||||
return new(Assignment.ItemActionWithoutItem, default, actionType, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
return new(Assignment.ItemActionWithItem, default, actionType, item);
|
||||
}
|
||||
|
||||
public bool Equals(ActionAssignment other)
|
||||
{
|
||||
return _actionType == other._actionType && _itemActionType == other._itemActionType && Equals(_item, other._item);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ActionAssignment other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_actionType, _itemActionType, _item);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_actionType)}: {_actionType}, {nameof(_itemActionType)}: {_itemActionType}, {nameof(_item)}: {_item}, {nameof(Assignment)}: {Assignment}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum Assignment : byte
|
||||
{
|
||||
Action,
|
||||
ItemActionWithoutItem,
|
||||
ItemActionWithItem
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Actions.Assignments
|
||||
@@ -9,6 +7,7 @@ namespace Content.Client.Actions.Assignments
|
||||
/// <summary>
|
||||
/// Tracks and manages the hotbar assignments for actions.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed class ActionAssignments
|
||||
{
|
||||
// the slots and assignments fields hold client's assignments (what action goes in what slot),
|
||||
@@ -19,7 +18,7 @@ namespace Content.Client.Actions.Assignments
|
||||
/// x = hotbar number, y = slot of that hotbar (index 0 corresponds to the one labeled "1",
|
||||
/// index 9 corresponds to the one labeled "0"). Essentially the inverse of _assignments.
|
||||
/// </summary>
|
||||
private readonly ActionAssignment?[,] _slots;
|
||||
private readonly ActionType?[,] _slots;
|
||||
|
||||
/// <summary>
|
||||
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
|
||||
@@ -28,14 +27,14 @@ namespace Content.Client.Actions.Assignments
|
||||
/// it can still be assigned to a slot. Essentially the inverse of _slots.
|
||||
/// There will be no entry if there is no assignment (no empty lists in this dict)
|
||||
/// </summary>
|
||||
private readonly Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>> _assignments;
|
||||
[DataField("assignments")]
|
||||
public readonly Dictionary<ActionType, List<(byte Hotbar, byte Slot)>> Assignments = new();
|
||||
|
||||
/// <summary>
|
||||
/// Actions which have been manually cleared by the user, thus should not
|
||||
/// auto-populate.
|
||||
/// </summary>
|
||||
private readonly HashSet<ActionType> _preventAutoPopulate = new();
|
||||
private readonly Dictionary<EntityUid, HashSet<ItemActionType>> _preventAutoPopulateItem = new();
|
||||
public readonly SortedSet<ActionType> PreventAutoPopulate = new();
|
||||
|
||||
private readonly byte _numHotbars;
|
||||
private readonly byte _numSlots;
|
||||
@@ -44,105 +43,25 @@ namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
_numHotbars = numHotbars;
|
||||
_numSlots = numSlots;
|
||||
_assignments = new Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>>();
|
||||
_slots = new ActionAssignment?[numHotbars, numSlots];
|
||||
_slots = new ActionType?[numHotbars, numSlots];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the assignments based on the current states of all the actions.
|
||||
/// Newly-granted actions or item actions which don't have an assignment will be assigned a slot
|
||||
/// automatically (unless they've been manually cleared). Item-based actions
|
||||
/// which no longer have an associated state will be decoupled from their item.
|
||||
/// </summary>
|
||||
public void Reconcile(byte currentHotbar, IReadOnlyDictionary<ActionType, ActionState> actionStates,
|
||||
IReadOnlyDictionary<EntityUid, Dictionary<ItemActionType, ActionState>> itemActionStates,
|
||||
bool actionMenuLocked)
|
||||
public bool Remove(ActionType action) => Replace(action, null);
|
||||
|
||||
internal bool Replace(ActionType action, ActionType? newAction)
|
||||
{
|
||||
// if we've been granted any actions which have no assignment to any hotbar, we must auto-populate them
|
||||
// into the hotbar so the user knows about them.
|
||||
// We fill their current hotbar first, rolling over to the next open slot on the next hotbar.
|
||||
foreach (var actionState in actionStates)
|
||||
if (!Assignments.Remove(action, out var assigns))
|
||||
return false;
|
||||
|
||||
if (newAction != null)
|
||||
Assignments[newAction] = assigns;
|
||||
|
||||
foreach (var (bar, slot) in assigns)
|
||||
{
|
||||
var assignment = ActionAssignment.For(actionState.Key);
|
||||
if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
_slots[bar, slot] = newAction;
|
||||
}
|
||||
|
||||
foreach (var (item, itemStates) in itemActionStates)
|
||||
{
|
||||
foreach (var itemActionState in itemStates)
|
||||
{
|
||||
// unlike regular actions, we DO actually show user their new item action even when it's disabled.
|
||||
// this allows them to instantly see when an action may be possible that is provided by an item but
|
||||
// something is preventing it
|
||||
// Note that we are checking if there is an explicit assignment for this item action + item,
|
||||
// we will determine during auto-population if we should tie the item to an existing "item action only"
|
||||
// assignment
|
||||
var assignment = ActionAssignment.For(itemActionState.Key, item);
|
||||
if (!_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to figure out which current item action assignments we had
|
||||
// which once had an associated item but have been revoked (based on our newly provided action states)
|
||||
// so we can dissociate them from the item. If the provided action states do not
|
||||
// have a state for this action type + item, we can assume that the action has been revoked for that item.
|
||||
var assignmentsWithoutItem = new List<KeyValuePair<ActionAssignment, List<(byte Hotbar, byte Slot)>>>();
|
||||
foreach (var assignmentEntry in _assignments)
|
||||
{
|
||||
if (!assignmentEntry.Key.TryGetItemActionWithItem(out var actionType, out var item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// we have this assignment currently tied to an item,
|
||||
// check if it no longer has an associated item in our dict of states
|
||||
if (itemActionStates.TryGetValue(item, out var states))
|
||||
{
|
||||
if (states.ContainsKey(actionType))
|
||||
{
|
||||
// we have a state for this item + action type so we won't
|
||||
// remove the item from the assignment
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assignmentsWithoutItem.Add(assignmentEntry);
|
||||
}
|
||||
|
||||
// reassign without the item for each assignment we found that no longer has an associated item
|
||||
foreach (var (assignment, slots) in assignmentsWithoutItem)
|
||||
{
|
||||
foreach (var (hotbar, slot) in slots)
|
||||
{
|
||||
if (!assignment.TryGetItemActionWithItem(out var actionType, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actionMenuLocked)
|
||||
{
|
||||
AssignSlot(hotbar, slot, ActionAssignment.For(actionType));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSlot(hotbar, slot, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally, we must find items which have no action states at all in our newly provided states so
|
||||
// we can assume their item was unequipped and reset them to allow auto-population.
|
||||
var itemsWithoutState = _preventAutoPopulateItem.Keys.Where(item => !itemActionStates.ContainsKey(item));
|
||||
foreach (var toRemove in itemsWithoutState)
|
||||
{
|
||||
_preventAutoPopulateItem.Remove(toRemove);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -151,18 +70,18 @@ namespace Content.Client.Actions.Assignments
|
||||
/// <param name="hotbar">hotbar whose slot is being assigned</param>
|
||||
/// <param name="slot">slot of the hotbar to assign to (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
|
||||
/// <param name="actionType">action to assign to the slot</param>
|
||||
public void AssignSlot(byte hotbar, byte slot, ActionAssignment actionType)
|
||||
public void AssignSlot(byte hotbar, byte slot, ActionType actionType)
|
||||
{
|
||||
ClearSlot(hotbar, slot, false);
|
||||
_slots[hotbar, slot] = actionType;
|
||||
if (_assignments.TryGetValue(actionType, out var slotList))
|
||||
if (Assignments.TryGetValue(actionType, out var slotList))
|
||||
{
|
||||
slotList.Add((hotbar, slot));
|
||||
}
|
||||
else
|
||||
{
|
||||
var newList = new List<(byte Hotbar, byte Slot)> { (hotbar, slot) };
|
||||
_assignments[actionType] = newList;
|
||||
Assignments[actionType] = newList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,40 +102,21 @@ namespace Content.Client.Actions.Assignments
|
||||
// (keeping in mind something can be assigned multiple slots)
|
||||
var currentAction = _slots[hotbar, slot];
|
||||
|
||||
if (!currentAction.HasValue)
|
||||
{
|
||||
if (currentAction == null)
|
||||
return;
|
||||
}
|
||||
|
||||
if (preventAutoPopulate)
|
||||
{
|
||||
var assignment = currentAction.Value;
|
||||
PreventAutoPopulate.Add(currentAction);
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
_preventAutoPopulate.Add(actionType);
|
||||
}
|
||||
else if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
if (!_preventAutoPopulateItem.TryGetValue(item, out var actionTypes))
|
||||
{
|
||||
actionTypes = new HashSet<ItemActionType>();
|
||||
_preventAutoPopulateItem[item] = actionTypes;
|
||||
}
|
||||
|
||||
actionTypes.Add(itemActionType);
|
||||
}
|
||||
}
|
||||
|
||||
var assignmentList = _assignments[currentAction.Value];
|
||||
var assignmentList = Assignments[currentAction];
|
||||
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
|
||||
if (!assignmentList.Any())
|
||||
{
|
||||
_assignments.Remove(currentAction.Value);
|
||||
Assignments.Remove(currentAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
_assignments[currentAction.Value] = assignmentList;
|
||||
Assignments[currentAction] = assignmentList;
|
||||
}
|
||||
|
||||
_slots[hotbar, slot] = null;
|
||||
@@ -231,45 +131,10 @@ namespace Content.Client.Actions.Assignments
|
||||
/// regardless of whether this assignment has been prevented from auto population
|
||||
/// via ClearSlot's preventAutoPopulate parameter. If false, will have no effect
|
||||
/// if this assignment has been prevented from auto population.</param>
|
||||
public void AutoPopulate(ActionAssignment toAssign, byte currentHotbar, bool force = true)
|
||||
public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true)
|
||||
{
|
||||
if (ShouldPreventAutoPopulate(toAssign, force))
|
||||
{
|
||||
if (!force && PreventAutoPopulate.Contains(toAssign))
|
||||
return;
|
||||
}
|
||||
|
||||
// if the assignment to make is an item action with an associated item,
|
||||
// then first look for currently assigned item actions without an item, to replace with this
|
||||
// assignment
|
||||
if (toAssign.TryGetItemActionWithItem(out var actionType, out var _))
|
||||
{
|
||||
if (_assignments.TryGetValue(ActionAssignment.For(actionType),
|
||||
out var possibilities))
|
||||
{
|
||||
// use the closest assignment to current hotbar
|
||||
byte hotbar = 0;
|
||||
byte slot = 0;
|
||||
var minCost = int.MaxValue;
|
||||
foreach (var possibility in possibilities)
|
||||
{
|
||||
var cost = possibility.Slot + _numSlots * (currentHotbar >= possibility.Hotbar
|
||||
? currentHotbar - possibility.Hotbar
|
||||
: _numHotbars - currentHotbar + possibility.Hotbar);
|
||||
if (cost < minCost)
|
||||
{
|
||||
hotbar = possibility.Hotbar;
|
||||
slot = possibility.Slot;
|
||||
minCost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
if (minCost != int.MaxValue)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++)
|
||||
{
|
||||
@@ -277,21 +142,10 @@ namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars);
|
||||
var slotAssignment = _slots[hotbar, slot];
|
||||
if (slotAssignment.HasValue)
|
||||
{
|
||||
// if the assignment in this slot is an item action without an associated item,
|
||||
// then tie it to the current item if we are trying to auto populate an item action.
|
||||
if (toAssign.Assignment == Assignment.ItemActionWithItem &&
|
||||
slotAssignment.Value.Assignment == Assignment.ItemActionWithoutItem)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotAssignment != null)
|
||||
continue;
|
||||
}
|
||||
|
||||
// slot's empty, assign
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
@@ -299,36 +153,15 @@ namespace Content.Client.Actions.Assignments
|
||||
// there was no empty slot
|
||||
}
|
||||
|
||||
private bool ShouldPreventAutoPopulate(ActionAssignment assignment, bool force)
|
||||
{
|
||||
if (force)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
return _preventAutoPopulate.Contains(actionType);
|
||||
}
|
||||
|
||||
if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
return _preventAutoPopulateItem.TryGetValue(item,
|
||||
out var itemActionTypes) && itemActionTypes.Contains(itemActionType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assignment to the indicated slot if there is one.
|
||||
/// </summary>
|
||||
public ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
|
||||
public ActionType? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
|
||||
|
||||
/// <returns>true if we have the assignment assigned to some slot</returns>
|
||||
public bool HasAssignment(ActionAssignment assignment)
|
||||
public bool HasAssignment(ActionType assignment)
|
||||
{
|
||||
return _assignments.ContainsKey(assignment);
|
||||
return Assignments.ContainsKey(assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Actions
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedActionsComponent))]
|
||||
public sealed class ClientActionsComponent : SharedActionsComponent
|
||||
{
|
||||
public const byte Hotbars = 9;
|
||||
public const byte Slots = 10;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
|
||||
private ActionsUI? _ui;
|
||||
private EntityUid _highlightedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Current assignments for all hotbars / slots for this entity.
|
||||
/// </summary>
|
||||
public ActionAssignments Assignments { get; } = new(Hotbars, Slots);
|
||||
|
||||
/// <summary>
|
||||
/// Allows calculating if we need to act due to this component being controlled by the current mob
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
PlayerDetached();
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void PlayerAttached()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ui = new ActionsUI(this);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void PlayerDetached()
|
||||
{
|
||||
if (_ui == null) return;
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.RemoveChild(_ui);
|
||||
_ui = null;
|
||||
}
|
||||
|
||||
public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
_ui?.HandleHotbarKeybind(slot, args);
|
||||
}
|
||||
|
||||
public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
_ui?.HandleChangeHotbarKeybind(hotbar, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed hotbar (and menu) based on current state of actions.
|
||||
/// </summary>
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assignments.Reconcile(_ui.SelectedHotbar, ActionStates(), ItemActionStates(), _ui.Locked);
|
||||
|
||||
_ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void AttemptAction(ActionSlot slot)
|
||||
{
|
||||
|
||||
var attempt = slot.ActionAttempt();
|
||||
if (attempt == null) return;
|
||||
|
||||
switch (attempt.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.Instant:
|
||||
// for instant actions, we immediately tell the server we're doing it
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformInstantActionMessage());
|
||||
#pragma warning restore 618
|
||||
break;
|
||||
case BehaviorType.Toggle:
|
||||
// for toggle actions, we immediately tell the server we're toggling it.
|
||||
if (attempt.TryGetActionState(this, out var actionState))
|
||||
{
|
||||
// TODO: At the moment we always predict that the toggle will work clientside,
|
||||
// even if it sometimes may not (it will be reset by the server if wrong).
|
||||
attempt.ToggleAction(this, !actionState.ToggledOn);
|
||||
slot.ToggledOn = !actionState.ToggledOn;
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformToggleActionMessage(!actionState.ToggledOn));
|
||||
#pragma warning restore 618
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "attempted to toggle action {0} which has" +
|
||||
" unknown state", attempt);
|
||||
}
|
||||
|
||||
break;
|
||||
case BehaviorType.TargetPoint:
|
||||
case BehaviorType.TargetEntity:
|
||||
// for target actions, we go into "select target" mode, we don't
|
||||
// message the server until we actually pick our target.
|
||||
|
||||
// if we're clicking the same thing we're already targeting for, then we simply cancel
|
||||
// targeting
|
||||
_ui?.ToggleTargeting(slot);
|
||||
break;
|
||||
case BehaviorType.None:
|
||||
break;
|
||||
default:
|
||||
Logger.ErrorS("action", "unhandled action press for action {0}",
|
||||
attempt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles clicks when selecting the target for an action. Only has an effect when currently
|
||||
/// selecting a target.
|
||||
/// </summary>
|
||||
public bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
// not currently predicted
|
||||
if (EntitySystem.Get<InputSystem>().Predicted) return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (_ui?.SelectingTargetFor?.Action == null ||
|
||||
(!_ui.SelectingTargetFor.Action.IsTargetAction)) return false;
|
||||
|
||||
// do nothing if we know it's on cooldown
|
||||
if (_ui.SelectingTargetFor.IsOnCooldown) return false;
|
||||
|
||||
var attempt = _ui.SelectingTargetFor.ActionAttempt();
|
||||
if (attempt == null)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (_ui.SelectingTargetFor.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.TargetPoint:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformTargetPointActionMessage(args));
|
||||
#pragma warning restore 618
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// target the currently hovered entity, if there is one
|
||||
case BehaviorType.TargetEntity when args.EntityUid != EntityUid.Invalid:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformTargetEntityActionMessage(args));
|
||||
#pragma warning restore 618
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// we are supposed to target an entity but we didn't click it
|
||||
case BehaviorType.TargetEntity when args.EntityUid == EntityUid.Invalid:
|
||||
{
|
||||
if (attempt.Action.DeselectWhenEntityNotClicked)
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AfterActionChanged()
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the item slot (inventory or hand) that contains this item
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void HighlightItemSlot(EntityUid item)
|
||||
{
|
||||
StopHighlightingItemSlots();
|
||||
|
||||
_highlightedEntity = item;
|
||||
_itemSlotManager.HighlightEntity(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops highlighting any item slots we are currently highlighting.
|
||||
/// </summary>H
|
||||
public void StopHighlightingItemSlots()
|
||||
{
|
||||
if (_highlightedEntity == default)
|
||||
return;
|
||||
|
||||
_itemSlotManager.UnHighlightEntity(_highlightedEntity);
|
||||
_highlightedEntity = default;
|
||||
}
|
||||
|
||||
public void ToggleActionsMenu()
|
||||
{
|
||||
_ui?.ToggleActionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -89,7 +91,7 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
|
||||
$"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
|
||||
$"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]"));
|
||||
_cooldownLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
@@ -29,28 +23,35 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public sealed class ActionMenu : DefaultWindow
|
||||
{
|
||||
private const string ItemTag = "item";
|
||||
private const string NotItemTag = "not item";
|
||||
private const string InstantActionTag = "instant";
|
||||
private const string ToggleActionTag = "toggle";
|
||||
private const string TargetActionTag = "target";
|
||||
private const string AllActionsTag = "all";
|
||||
private const string GrantedActionsTag = "granted";
|
||||
// Pre-defined global filters that can be used to select actions based on their properties (as opposed to their
|
||||
// own yaml-defined filters).
|
||||
// TODO LOC STRINGs
|
||||
private const string AllFilter = "all";
|
||||
private const string ItemFilter = "item";
|
||||
private const string InnateFilter = "innate";
|
||||
private const string EnabledFilter = "enabled";
|
||||
private const string InstantFilter = "instant";
|
||||
private const string TargetedFilter = "targeted";
|
||||
|
||||
private readonly string[] _filters =
|
||||
{
|
||||
AllFilter,
|
||||
ItemFilter,
|
||||
InnateFilter,
|
||||
EnabledFilter,
|
||||
InstantFilter,
|
||||
TargetedFilter
|
||||
};
|
||||
|
||||
private const int MinSearchLength = 3;
|
||||
private static readonly Regex NonAlphanumeric = new Regex(@"\W", RegexOptions.Compiled);
|
||||
private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled);
|
||||
private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty<BaseActionPrototype>();
|
||||
|
||||
/// <summary>
|
||||
/// Is an action currently being dragged from this window?
|
||||
/// </summary>
|
||||
public bool IsDragging => _dragDropHelper.IsDragging;
|
||||
|
||||
// parallel list of actions currently selectable in itemList
|
||||
private BaseActionPrototype[] _actionList = new BaseActionPrototype[0];
|
||||
|
||||
private readonly ActionManager _actionManager;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private readonly ActionsUI _actionsUI;
|
||||
private readonly LineEdit _searchBar;
|
||||
private readonly MultiselectOptionButton<string> _filterButton;
|
||||
@@ -60,17 +61,16 @@ namespace Content.Client.Actions.UI
|
||||
private readonly TextureRect _dragShadow;
|
||||
private readonly IGameHud _gameHud;
|
||||
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
|
||||
private readonly IEntityManager _entMan;
|
||||
|
||||
|
||||
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
|
||||
public ActionMenu(ActionsUI actionsUI)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
Title = Loc.GetString("ui-actionmenu-title");
|
||||
MinSize = (300, 300);
|
||||
MinSize = (320, 300);
|
||||
|
||||
Contents.AddChild(new BoxContainer
|
||||
{
|
||||
@@ -115,27 +115,14 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
});
|
||||
|
||||
// populate filters from search tags
|
||||
var filterTags = new List<string>();
|
||||
foreach (var action in _actionManager.EnumerateActions())
|
||||
{
|
||||
filterTags.AddRange(action.Filters);
|
||||
}
|
||||
|
||||
// special one to filter to only include item actions
|
||||
filterTags.Add(ItemTag);
|
||||
filterTags.Add(NotItemTag);
|
||||
filterTags.Add(InstantActionTag);
|
||||
filterTags.Add(ToggleActionTag);
|
||||
filterTags.Add(TargetActionTag);
|
||||
filterTags.Add(AllActionsTag);
|
||||
filterTags.Add(GrantedActionsTag);
|
||||
|
||||
foreach (var tag in filterTags.Distinct().OrderBy(tag => tag))
|
||||
foreach (var tag in _filters)
|
||||
{
|
||||
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
|
||||
}
|
||||
|
||||
// default to showing all actions.
|
||||
_filterButton.SelectKey(AllFilter);
|
||||
|
||||
UpdateFilterLabel();
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
@@ -157,13 +144,6 @@ namespace Content.Client.Actions.UI
|
||||
_searchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected += OnFilterItemSelected;
|
||||
_gameHud.ActionsButtonDown = true;
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (ActionMenuItem) actionMenuControl;
|
||||
actionMenuItem.OnButtonDown += OnItemButtonDown;
|
||||
actionMenuItem.OnButtonUp += OnItemButtonUp;
|
||||
actionMenuItem.OnPressed += OnItemPressed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
@@ -199,7 +179,7 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private bool OnBeginActionDrag()
|
||||
{
|
||||
_dragShadow.Texture = _dragDropHelper.Dragged!.Action.Icon.Frame0();
|
||||
_dragShadow.Texture = _dragDropHelper.Dragged?.Action?.Icon?.Frame0();
|
||||
// don't make visible until frameupdate, otherwise it'll flicker
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32));
|
||||
return true;
|
||||
@@ -244,47 +224,7 @@ namespace Content.Client.Actions.UI
|
||||
return;
|
||||
}
|
||||
|
||||
// drag and drop
|
||||
switch (_dragDropHelper.Dragged.Action)
|
||||
{
|
||||
// assign the dragged action to the target slot
|
||||
case ActionPrototype actionPrototype:
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(actionPrototype.ActionType));
|
||||
break;
|
||||
case ItemActionPrototype itemActionPrototype:
|
||||
// the action menu doesn't show us if the action has an associated item,
|
||||
// so when we perform the assignment, we should check if we currently have an unassigned state
|
||||
// for this item and assign it tied to that item if so, otherwise assign it "itemless"
|
||||
|
||||
// this is not particularly efficient but we don't maintain an index from
|
||||
// item action type to its action states, and this method should be pretty infrequent so it's probably fine
|
||||
var assigned = false;
|
||||
foreach (var (item, itemStates) in _actionsComponent.ItemActionStates())
|
||||
{
|
||||
foreach (var (actionType, _) in itemStates)
|
||||
{
|
||||
if (actionType != itemActionPrototype.ActionType) continue;
|
||||
var assignment = ActionAssignment.For(actionType, item);
|
||||
if (_actionsComponent.Assignments.HasAssignment(assignment)) continue;
|
||||
// no assignment for this state, assign tied to the item
|
||||
assigned = true;
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, assignment);
|
||||
break;
|
||||
}
|
||||
|
||||
if (assigned)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!assigned)
|
||||
{
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(itemActionPrototype.ActionType));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
@@ -300,19 +240,8 @@ namespace Content.Client.Actions.UI
|
||||
private void OnItemPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not ActionMenuItem actionMenuItem) return;
|
||||
switch (actionMenuItem.Action)
|
||||
{
|
||||
case ActionPrototype actionPrototype:
|
||||
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(actionPrototype.ActionType), _actionsUI.SelectedHotbar);
|
||||
break;
|
||||
case ItemActionPrototype itemActionPrototype:
|
||||
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(itemActionPrototype.ActionType), _actionsUI.SelectedHotbar);
|
||||
break;
|
||||
default:
|
||||
Logger.ErrorS("action", "unexpected action prototype {0}", actionMenuItem.Action);
|
||||
break;
|
||||
}
|
||||
|
||||
_actionsUI.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
@@ -341,7 +270,7 @@ namespace Content.Client.Actions.UI
|
||||
return;
|
||||
}
|
||||
|
||||
var matchingActions = _actionManager.EnumerateActions()
|
||||
var matchingActions = _actionsUI.Component.Actions
|
||||
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
|
||||
|
||||
PopulateActions(matchingActions);
|
||||
@@ -361,7 +290,7 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
|
||||
private bool MatchesSearchCriteria(ActionType action, string standardizedSearch,
|
||||
IReadOnlyList<string> selectedFilterTags)
|
||||
{
|
||||
// check filter tag match first - each action must contain all filter tags currently selected.
|
||||
@@ -377,56 +306,32 @@ namespace Content.Client.Actions.UI
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Standardize(ActionTypeString(action)).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// allows matching by typing spaces between the enum case changes, like "xeno spit" if the
|
||||
// actiontype is "XenoSpit"
|
||||
if (Standardize(ActionTypeString(action), true).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Standardize(action.Name.ToString()).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// search by provider name
|
||||
if (action.Provider == null || action.Provider == _actionsUI.Component.Owner)
|
||||
return false;
|
||||
|
||||
var name = _entMan.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
|
||||
return Standardize(name).Contains(standardizedSearch);
|
||||
}
|
||||
|
||||
private string ActionTypeString(BaseActionPrototype baseActionPrototype)
|
||||
{
|
||||
if (baseActionPrototype is ActionPrototype actionPrototype)
|
||||
{
|
||||
return actionPrototype.ActionType.ToString();
|
||||
}
|
||||
if (baseActionPrototype is ItemActionPrototype itemActionPrototype)
|
||||
{
|
||||
return itemActionPrototype.ActionType.ToString();
|
||||
}
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private bool ActionMatchesFilterTag(BaseActionPrototype action, string tag)
|
||||
private bool ActionMatchesFilterTag(ActionType action, string tag)
|
||||
{
|
||||
return tag switch
|
||||
{
|
||||
AllActionsTag => true,
|
||||
GrantedActionsTag => _actionsComponent.IsGranted(action),
|
||||
ItemTag => action is ItemActionPrototype,
|
||||
NotItemTag => action is ActionPrototype,
|
||||
InstantActionTag => action.BehaviorType == BehaviorType.Instant,
|
||||
TargetActionTag => action.IsTargetAction,
|
||||
ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
|
||||
_ => action.Filters.Contains(tag)
|
||||
EnabledFilter => action.Enabled,
|
||||
ItemFilter => action.Provider != null && action.Provider != _actionsUI.Component.Owner,
|
||||
InnateFilter => action.Provider == null || action.Provider == _actionsUI.Component.Owner,
|
||||
InstantFilter => action is InstantAction,
|
||||
TargetedFilter => action is TargetedAction,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace),
|
||||
/// trimmed, 1 space max per whitespace gap,
|
||||
@@ -472,16 +377,15 @@ namespace Content.Client.Actions.UI
|
||||
return newText.ToString();
|
||||
}
|
||||
|
||||
private void PopulateActions(IEnumerable<BaseActionPrototype> actions)
|
||||
private void PopulateActions(IEnumerable<ActionType> actions)
|
||||
{
|
||||
ClearList();
|
||||
|
||||
_actionList = actions.ToArray();
|
||||
foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var actionItem = new ActionMenuItem(action, OnItemFocusExited);
|
||||
var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited);
|
||||
_resultsGrid.Children.Add(actionItem);
|
||||
actionItem.SetActionState(_actionsComponent.IsGranted(action));
|
||||
actionItem.SetActionState(action.Enabled);
|
||||
actionItem.OnButtonDown += OnItemButtonDown;
|
||||
actionItem.OnButtonUp += OnItemButtonUp;
|
||||
actionItem.OnPressed += OnItemPressed;
|
||||
@@ -496,7 +400,6 @@ namespace Content.Client.Actions.UI
|
||||
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
|
||||
}
|
||||
_resultsGrid.Children.Clear();
|
||||
_actionList = EmptyActionList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -508,8 +411,10 @@ namespace Content.Client.Actions.UI
|
||||
foreach (var actionItem in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = ((ActionMenuItem) actionItem);
|
||||
actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
|
||||
actionMenuItem.SetActionState(actionMenuItem.Action.Enabled);
|
||||
}
|
||||
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
// TODO merge this with action-slot when it gets XAMLd
|
||||
// this has way too much overlap, especially now that they both have the item-sprite icons.
|
||||
|
||||
/// <summary>
|
||||
/// An individual action visible in the action menu.
|
||||
/// </summary>
|
||||
@@ -16,28 +24,165 @@ namespace Content.Client.Actions.UI
|
||||
// quickly explore what each action is
|
||||
private const float CustomTooltipDelay = 0.2f;
|
||||
|
||||
public BaseActionPrototype Action { get; private set; }
|
||||
private readonly TextureRect _bigActionIcon;
|
||||
private readonly TextureRect _smallActionIcon;
|
||||
private readonly SpriteView _smallItemSpriteView;
|
||||
private readonly SpriteView _bigItemSpriteView;
|
||||
|
||||
public ActionType Action;
|
||||
|
||||
private Action<ActionMenuItem> _onControlFocusExited;
|
||||
|
||||
public ActionMenuItem(BaseActionPrototype action, Action<ActionMenuItem> onControlFocusExited)
|
||||
private readonly ActionsUI _actionsUI;
|
||||
|
||||
public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action<ActionMenuItem> onControlFocusExited)
|
||||
{
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
_actionsUI = actionsUI;
|
||||
Action = action;
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
|
||||
MinSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
|
||||
AddChild(new TextureRect
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Texture = action.Icon.Frame0()
|
||||
Visible = false
|
||||
};
|
||||
_bigItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Scale = (2, 2),
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
_smallActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_smallItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
|
||||
// padding to the left of the small icon
|
||||
var paddingBoxItemIcon = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBoxItemIcon.AddChild(new Control()
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
});
|
||||
paddingBoxItemIcon.AddChild(new Control
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_smallActionIcon,
|
||||
_smallItemSpriteView
|
||||
}
|
||||
});
|
||||
AddChild(_bigActionIcon);
|
||||
AddChild(_bigItemSpriteView);
|
||||
AddChild(paddingBoxItemIcon);
|
||||
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
UpdateIcons();
|
||||
}
|
||||
|
||||
|
||||
public void UpdateIcons()
|
||||
{
|
||||
UpdateItemIcon();
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
else
|
||||
SetActionIcon(Action.Icon?.Frame0());
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || Action == null)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
|
||||
{
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Modulate = Action.IconColor;
|
||||
_smallActionIcon.Visible = true;
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Modulate = Action.IconColor;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateItemIcon()
|
||||
{
|
||||
if (Action?.Provider == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite))
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Action.ItemIconStyle)
|
||||
{
|
||||
case ItemActionIconStyle.BigItem:
|
||||
_bigItemSpriteView.Visible = true;
|
||||
_bigItemSpriteView.Sprite = sprite;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
case ItemActionIconStyle.BigAction:
|
||||
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = true;
|
||||
_smallItemSpriteView.Sprite = sprite;
|
||||
break;
|
||||
|
||||
case ItemActionIconStyle.NoItem:
|
||||
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
@@ -48,7 +193,15 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
var tooltip = new ActionAlertTooltip(name, decr);
|
||||
|
||||
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
|
||||
tooltip.Cooldown = Action.Cooldown;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -71,6 +224,5 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
@@ -37,48 +31,7 @@ namespace Content.Client.Actions.UI
|
||||
/// <summary>
|
||||
/// Current action in this slot.
|
||||
/// </summary>
|
||||
public BaseActionPrototype? Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// true if there is an action assigned to the slot
|
||||
/// </summary>
|
||||
public bool HasAssignment => Action != null;
|
||||
|
||||
private bool HasToggleSprite => Action != null && Action.IconOn != SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable when an action is in this slot.
|
||||
/// True if the action is currently shown as enabled, false if action disabled.
|
||||
/// </summary>
|
||||
public bool ActionEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is there an action in the slot that can currently be used?
|
||||
/// Target-basedActions on cooldown can still be selected / deselected if they've been configured as such
|
||||
/// </summary>
|
||||
public bool CanUseAction => Action != null && ActionEnabled &&
|
||||
(!IsOnCooldown || (Action.IsTargetAction && !Action.DeselectOnCooldown));
|
||||
|
||||
/// <summary>
|
||||
/// Item the action is provided by, only valid if Action is an ItemActionPrototype. May be null
|
||||
/// if the item action is not yet tied to an item.
|
||||
/// </summary>
|
||||
public EntityUid? Item { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the action in this slot should be shown as toggled on. Separate from Depressed.
|
||||
/// </summary>
|
||||
public bool ToggledOn
|
||||
{
|
||||
get => _toggledOn;
|
||||
set
|
||||
{
|
||||
if (_toggledOn == value) return;
|
||||
_toggledOn = value;
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
public ActionType? Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
|
||||
@@ -86,25 +39,6 @@ namespace Content.Client.Actions.UI
|
||||
private byte SlotNumber => (byte) (SlotIndex + 1);
|
||||
public byte SlotIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
|
||||
/// </summary>
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown
|
||||
{
|
||||
get => _cooldown;
|
||||
set
|
||||
{
|
||||
_cooldown = value;
|
||||
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
|
||||
{
|
||||
actionAlertTooltip.Cooldown = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
private (TimeSpan Start, TimeSpan End)? _cooldown;
|
||||
|
||||
public bool IsOnCooldown => Cooldown.HasValue && _gameTiming.CurTime < Cooldown.Value.End;
|
||||
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly RichTextLabel _number;
|
||||
private readonly TextureRect _bigActionIcon;
|
||||
@@ -114,8 +48,6 @@ namespace Content.Client.Actions.UI
|
||||
private readonly CooldownGraphic _cooldownGraphic;
|
||||
private readonly ActionsUI _actionsUI;
|
||||
private readonly ActionMenu _actionMenu;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private bool _toggledOn;
|
||||
// whether button is currently pressed down by mouse or keybind down.
|
||||
private bool _depressed;
|
||||
private bool _beingHovered;
|
||||
@@ -124,9 +56,8 @@ namespace Content.Client.Actions.UI
|
||||
/// Creates an action slot for the specified number
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0".</param>
|
||||
public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, ClientActionsComponent actionsComponent, byte slotIndex)
|
||||
public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionMenu = actionMenu;
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
@@ -170,7 +101,8 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
|
||||
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
|
||||
@@ -219,24 +151,24 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
return Action == null ? null :
|
||||
new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
|
||||
}
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
/// <summary>
|
||||
/// Action attempt for performing the action in the slot
|
||||
/// </summary>
|
||||
public IActionAttempt? ActionAttempt()
|
||||
{
|
||||
IActionAttempt? attempt = Action switch
|
||||
string? extra = null;
|
||||
if (Action.Charges != null)
|
||||
{
|
||||
ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
|
||||
ItemActionPrototype itemActionPrototype =>
|
||||
Item.HasValue && IoCManager.Resolve<IEntityManager>().TryGetComponent<ItemActionsComponent?>(Item, out var itemActions) ?
|
||||
new ItemActionAttempt(itemActionPrototype, Item.Value, itemActions) : null,
|
||||
_ => null
|
||||
};
|
||||
return attempt;
|
||||
extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges));
|
||||
}
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
var tooltip = new ActionAlertTooltip(name, decr, extra);
|
||||
|
||||
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
|
||||
tooltip.Cooldown = Action.Cooldown;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
protected override void MouseEntered()
|
||||
@@ -245,9 +177,9 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
if (Action is not ItemActionPrototype) return;
|
||||
if (Item == null) return;
|
||||
_actionsComponent.HighlightItemSlot(Item.Value);
|
||||
|
||||
if (Action?.Provider != null)
|
||||
_actionsUI.System.HighlightItemSlot(Action.Provider.Value);
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
@@ -256,32 +188,55 @@ namespace Content.Client.Actions.UI
|
||||
_beingHovered = false;
|
||||
CancelPress();
|
||||
DrawModeChanged();
|
||||
_actionsComponent.StopHighlightingItemSlots();
|
||||
_actionsUI.System.StopHighlightingItemSlot();
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
if (Action == null)
|
||||
{
|
||||
if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging && !_actionMenu.IsDragging)
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
|
||||
_actionsUI.StopTargeting();
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
// No action for this slot. Maybe the user is trying to add a mapping action?
|
||||
_actionsUI.System.TryFillSlot(_actionsUI.SelectedHotbar, SlotIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// only handle clicks, and can't do anything to this if no assignment
|
||||
if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment)
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
// might turn into a drag or a full press if released
|
||||
Depress(true);
|
||||
_actionsUI.DragDropHelper.MouseDown(this);
|
||||
DrawModeChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIRightClick || _actionsUI.Locked)
|
||||
return;
|
||||
|
||||
// might turn into a drag or a full press if released
|
||||
Depress(true);
|
||||
_actionsUI.DragDropHelper.MouseDown(this);
|
||||
DrawModeChanged();
|
||||
if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging)
|
||||
return;
|
||||
|
||||
// user right clicked on an action slot, so we clear it.
|
||||
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
|
||||
|
||||
// If this was a temporary action, and it is no longer assigned to any slots, then we remove the action
|
||||
// altogether.
|
||||
if (Action.Temporary)
|
||||
{
|
||||
// Theres probably a better way to do this.....
|
||||
DebugTools.Assert(Action.ClientExclusive, "Temporary-actions must be client exclusive");
|
||||
|
||||
if (!_actionsUI.System.Assignments.Assignments.TryGetValue(Action, out var index)
|
||||
|| index.Count == 0)
|
||||
{
|
||||
_actionsUI.Component.Actions.Remove(Action);
|
||||
}
|
||||
}
|
||||
|
||||
_actionsUI.StopTargeting();
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
@@ -299,21 +254,21 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
// finish the drag, swap the 2 slots
|
||||
var fromIdx = SlotIndex;
|
||||
var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var fromAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var toIdx = targetSlot.SlotIndex;
|
||||
var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
|
||||
if (fromIdx == toIdx) return;
|
||||
if (!fromAssignment.HasValue) return;
|
||||
if (fromAssignment == null) return;
|
||||
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
|
||||
if (toAssignment.HasValue)
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment);
|
||||
if (toAssignment != null)
|
||||
{
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
}
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
@@ -348,66 +303,19 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
/// <summary>
|
||||
/// Press this button down. If it was depressed and now set to not depressed, will
|
||||
/// trigger the action. Only has an effect if CanUseAction.
|
||||
/// trigger the action.
|
||||
/// </summary>
|
||||
public void Depress(bool depress)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
if (!CanUseAction) return;
|
||||
|
||||
if (Action == null || !Action.Enabled) return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
// no left-click interaction with it on cooldown or revoked
|
||||
_actionsComponent.AttemptAction(this);
|
||||
_actionsUI.System.OnSlotPressed(this);
|
||||
}
|
||||
_depressed = depress;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the action assigned to this slot.
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
|
||||
public void Assign(ActionPrototype action, bool actionEnabled)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action) return;
|
||||
|
||||
Action = action;
|
||||
Item = default;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = actionEnabled;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the item action assigned to this slot. The action will always be shown as disabled
|
||||
/// until it is tied to a specific item.
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
public void Assign(ItemActionPrototype action)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action && Item == default) return;
|
||||
|
||||
Action = action;
|
||||
Item = default;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -415,18 +323,12 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
/// <param name="item">item the action is provided by</param>
|
||||
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
|
||||
public void Assign(ItemActionPrototype action, EntityUid item, bool actionEnabled)
|
||||
public void Assign(ActionType action)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action && Item == item) return;
|
||||
if (Action != null && Action == action) return;
|
||||
|
||||
Action = action;
|
||||
Item = item;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
@@ -438,12 +340,9 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (!HasAssignment) return;
|
||||
if (Action == null) return;
|
||||
Action = null;
|
||||
Item = default;
|
||||
ToggledOn = false;
|
||||
_depressed = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
@@ -453,12 +352,8 @@ namespace Content.Client.Actions.UI
|
||||
/// <summary>
|
||||
/// Display the action in this slot (if there is one) as enabled
|
||||
/// </summary>
|
||||
public void EnableAction()
|
||||
public void Enable()
|
||||
{
|
||||
if (ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = true;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
@@ -467,11 +362,8 @@ namespace Content.Client.Actions.UI
|
||||
/// Display the action in this slot (if there is one) as disabled.
|
||||
/// The slot is still clickable.
|
||||
/// </summary>
|
||||
public void DisableAction()
|
||||
public void Disable()
|
||||
{
|
||||
if (!ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = false;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
@@ -481,70 +373,56 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
|
||||
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
|
||||
var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
|
||||
var color = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor;
|
||||
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
|
||||
}
|
||||
|
||||
private void UpdateIcons()
|
||||
public void UpdateIcons()
|
||||
{
|
||||
if (!HasAssignment)
|
||||
UpdateItemIcon();
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
SetItemIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasToggleSprite && ToggledOn && Action != null)
|
||||
{
|
||||
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
}
|
||||
else if (Action != null)
|
||||
{
|
||||
SetActionIcon(Action.Icon.Frame0());
|
||||
}
|
||||
|
||||
if (Item != default)
|
||||
{
|
||||
SetItemIcon(IoCManager.Resolve<IEntityManager>().TryGetComponent<ISpriteComponent?>(Item, out var spriteComponent) ? spriteComponent : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetItemIcon(null);
|
||||
}
|
||||
SetActionIcon(Action.Icon?.Frame0());
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || !HasAssignment)
|
||||
if (texture == null || Action == null)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
|
||||
{
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Modulate = Action.IconColor;
|
||||
_smallActionIcon.Visible = true;
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action is ItemActionPrototype {IconStyle: ItemActionIconStyle.BigItem})
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Modulate = Action.IconColor;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetItemIcon(ISpriteComponent? sprite)
|
||||
private void UpdateItemIcon()
|
||||
{
|
||||
if (sprite == null || !HasAssignment)
|
||||
if (Action?.Provider == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite))
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
@@ -553,70 +431,48 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action is ItemActionPrototype actionPrototype)
|
||||
switch (Action.ItemIconStyle)
|
||||
{
|
||||
switch (actionPrototype.IconStyle)
|
||||
{
|
||||
case ItemActionIconStyle.BigItem:
|
||||
{
|
||||
_bigItemSpriteView.Visible = true;
|
||||
_bigItemSpriteView.Sprite = sprite;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
case ItemActionIconStyle.BigAction:
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = true;
|
||||
_smallItemSpriteView.Sprite = sprite;
|
||||
break;
|
||||
}
|
||||
case ItemActionIconStyle.NoItem:
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
case ItemActionIconStyle.BigItem:
|
||||
_bigItemSpriteView.Visible = true;
|
||||
_bigItemSpriteView.Sprite = sprite;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
case ItemActionIconStyle.BigAction:
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = true;
|
||||
_smallItemSpriteView.Sprite = sprite;
|
||||
break;
|
||||
|
||||
case ItemActionIconStyle.NoItem:
|
||||
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawModeChanged()
|
||||
public void DrawModeChanged()
|
||||
{
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered)
|
||||
{
|
||||
if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging ||
|
||||
(HasAssignment && ActionEnabled && !IsOnCooldown))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// always show the normal empty button style if no action in this slot
|
||||
if (!HasAssignment)
|
||||
if (Action == null)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered && (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging || Action.Enabled))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
}
|
||||
|
||||
// it's only depress-able if it's usable, so if we're depressed
|
||||
// show the depressed style
|
||||
if (_depressed)
|
||||
@@ -625,47 +481,50 @@ namespace Content.Client.Actions.UI
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// if it's toggled on, always show the toggled on style (currently same as depressed style)
|
||||
if (ToggledOn)
|
||||
if (Action.Toggled || _actionsUI.SelectingTargetFor == this)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
|
||||
SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal :
|
||||
ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!ActionEnabled)
|
||||
if (!Action.Enabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
if (Action == null || Action.Cooldown == null || !Action.Enabled)
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
var cooldown = Action.Cooldown.Value;
|
||||
var duration = cooldown.End - cooldown.Start;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
var length = duration.TotalSeconds;
|
||||
var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
|
||||
var progress = (curTime - cooldown.Start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5);
|
||||
|
||||
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
|
||||
_cooldownGraphic.Visible = ratio > -1f;
|
||||
if (ratio > -1f)
|
||||
_cooldownGraphic.Visible = true;
|
||||
else
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
Action.Cooldown = null;
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
@@ -27,13 +22,16 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public sealed class ActionsUI : Container
|
||||
{
|
||||
private const float DragDeadZone = 10f;
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private readonly ActionManager _actionManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
internal readonly ActionsSystem System;
|
||||
private readonly IGameHud _gameHud;
|
||||
|
||||
/// <summary>
|
||||
/// The action component of the currently attached entity.
|
||||
/// </summary>
|
||||
public readonly ActionsComponent Component;
|
||||
|
||||
private readonly ActionSlot[] _slots;
|
||||
|
||||
private readonly GridContainer _slotContainer;
|
||||
@@ -75,15 +73,13 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public IEnumerable<ActionSlot> Slots => _slots;
|
||||
|
||||
public ActionsUI(ClientActionsComponent actionsComponent)
|
||||
public ActionsUI(ActionsSystem system, ActionsComponent component)
|
||||
{
|
||||
SetValue(LayoutContainer.DebugProperty, true);
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
System = system;
|
||||
Component = component;
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_menu = new ActionMenu(_actionsComponent, this);
|
||||
_menu = new ActionMenu(this);
|
||||
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain);
|
||||
@@ -196,7 +192,7 @@ namespace Content.Client.Actions.UI
|
||||
_loadoutContainer.AddChild(nextHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
_slots = new ActionSlot[ClientActionsComponent.Slots];
|
||||
_slots = new ActionSlot[ActionsSystem.Slots];
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
@@ -207,14 +203,14 @@ namespace Content.Client.Actions.UI
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
|
||||
for (byte i = 0; i < ActionsSystem.Slots; i++)
|
||||
{
|
||||
var slot = new ActionSlot(this, _menu, actionsComponent, i);
|
||||
var slot = new ActionSlot(this, _menu, i);
|
||||
_slotContainer.AddChild(slot);
|
||||
_slots[i] = slot;
|
||||
}
|
||||
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag, DragDeadZone);
|
||||
|
||||
MinSize = (10, 400);
|
||||
}
|
||||
@@ -285,157 +281,61 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
foreach (var actionSlot in Slots)
|
||||
{
|
||||
var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
if (!assignedActionType.HasValue)
|
||||
var action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
if (SelectingTargetFor == actionSlot)
|
||||
StopTargeting(true);
|
||||
actionSlot.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignedActionType.Value.TryGetAction(out var actionType))
|
||||
if (Component.Actions.TryGetValue(action, out var actualAction))
|
||||
{
|
||||
UpdateActionSlot(actionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else if (assignedActionType.Value.TryGetItemActionWithoutItem(out var itemlessActionType))
|
||||
{
|
||||
UpdateActionSlot(itemlessActionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else if (assignedActionType.Value.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
UpdateActionSlot(item, itemActionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unexpected Assignment type {0}",
|
||||
assignedActionType.Value.Assignment);
|
||||
actionSlot.Clear();
|
||||
UpdateActionSlot(actualAction, actionSlot);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Action not in the actions component, but in the assignment list.
|
||||
// This is either an action that doesn't auto-clear from the menu, or the action menu was locked.
|
||||
// Show the old action, but make sure it is disabled;
|
||||
action.Enabled = false;
|
||||
action.Toggled = false;
|
||||
|
||||
// If we enable the item-sprite, and if the item-sprite has a visual toggle, then the player will be
|
||||
// able to know whether the item is toggled, even if it is not in their LOS (but in PVS). And for things
|
||||
// like PDA sprites, the player can even see whether the action's item is currently inside of their PVS.
|
||||
// SO unless theres some way of "freezing" a sprite-view, we just have to disable it.
|
||||
action.ItemIconStyle = ItemActionIconStyle.NoItem;
|
||||
|
||||
UpdateActionSlot(action, actionSlot);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
|
||||
private void UpdateActionSlot(ActionType action, ActionSlot actionSlot)
|
||||
{
|
||||
if (_actionManager.TryGet(actionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
return;
|
||||
}
|
||||
actionSlot.Assign(action);
|
||||
|
||||
if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled)
|
||||
if (!action.Enabled)
|
||||
{
|
||||
// action is currently disabled
|
||||
|
||||
// just revoked an action we were trying to target with, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
|
||||
actionSlot.DisableAction();
|
||||
actionSlot.Cooldown = null;
|
||||
actionSlot.Disable();
|
||||
}
|
||||
else
|
||||
{
|
||||
// action is currently granted
|
||||
actionSlot.EnableAction();
|
||||
actionSlot.Cooldown = actionState.Cooldown;
|
||||
|
||||
// if we are targeting for this action and it's now on cooldown, stop targeting if we're supposed to
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
actionSlot.Enable();
|
||||
}
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
actionSlot.UpdateIcons();
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(ItemActionType itemlessActionType, ActionSlot actionSlot,
|
||||
ActionAssignment? assignedActionType)
|
||||
{
|
||||
if (_actionManager.TryGet(itemlessActionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
}
|
||||
actionSlot.Cooldown = null;
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(EntityUid item, ItemActionType itemActionType, ActionSlot actionSlot,
|
||||
ActionAssignment? assignedActionType)
|
||||
{
|
||||
if (!_entityManager.EntityExists(item)) return;
|
||||
if (_actionManager.TryGet(itemActionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, item, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_actionsComponent.TryGetItemActionState(itemActionType, item, out var actionState))
|
||||
{
|
||||
// action is no longer tied to an item, this should never happen as we
|
||||
// check this at the start of this method. But just to be safe
|
||||
// we will restore our assignment here to the correct state
|
||||
Logger.ErrorS("action", "coding error, expected actionType {0} to have" +
|
||||
" a state but it didn't", assignedActionType);
|
||||
_actionsComponent.Assignments.AssignSlot(SelectedHotbar, actionSlot.SlotIndex,
|
||||
ActionAssignment.For(itemActionType));
|
||||
actionSlot.Assign(action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actionState.Enabled)
|
||||
{
|
||||
// just disabled an action we were trying to target with, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
|
||||
actionSlot.DisableAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
// action is currently granted
|
||||
actionSlot.EnableAction();
|
||||
|
||||
// if we are targeting with an action now on cooldown, stop targeting if we should
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
SelectingTargetFor.Item == item &&
|
||||
actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
}
|
||||
actionSlot.Cooldown = actionState.Cooldown;
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnHotbarPaginate(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// rather than clicking the arrows themselves, the user can click the hbox so it's more
|
||||
@@ -445,11 +345,11 @@ namespace Content.Client.Actions.UI
|
||||
var rightness = args.RelativePosition.X / _loadoutContainer.Width;
|
||||
if (rightness > 0.5)
|
||||
{
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars));
|
||||
}
|
||||
else
|
||||
{
|
||||
var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
|
||||
var newBar = SelectedHotbar == 0 ? ActionsSystem.Hotbars - 1 : SelectedHotbar - 1;
|
||||
ChangeHotbar((byte) newBar);
|
||||
}
|
||||
}
|
||||
@@ -483,29 +383,35 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
private void StartTargeting(ActionSlot actionSlot)
|
||||
{
|
||||
if (actionSlot.Action == null)
|
||||
return;
|
||||
|
||||
// If we were targeting something else we should stop
|
||||
StopTargeting();
|
||||
|
||||
SelectingTargetFor = actionSlot;
|
||||
|
||||
// show it as toggled on to indicate we are currently selecting a target for it
|
||||
if (!actionSlot.ToggledOn)
|
||||
{
|
||||
actionSlot.ToggledOn = true;
|
||||
}
|
||||
if (actionSlot.Action is TargetedAction targetAction)
|
||||
System.StartTargeting(targetAction);
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out of targeting mode if currently selecting target for an action
|
||||
/// </summary>
|
||||
public void StopTargeting()
|
||||
public void StopTargeting(bool updating = false)
|
||||
{
|
||||
if (SelectingTargetFor == null) return;
|
||||
if (SelectingTargetFor.ToggledOn)
|
||||
{
|
||||
SelectingTargetFor.ToggledOn = false;
|
||||
}
|
||||
if (SelectingTargetFor == null)
|
||||
return;
|
||||
|
||||
SelectingTargetFor = null;
|
||||
System.StopTargeting();
|
||||
|
||||
// Sometimes targeting gets stopped mid-UI update.
|
||||
// in that case, don't need to do a nested UI refresh.
|
||||
if (!updating)
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
|
||||
@@ -513,7 +419,6 @@ namespace Content.Client.Actions.UI
|
||||
ToggleActionsMenu();
|
||||
}
|
||||
|
||||
|
||||
private void OnToggleActionsMenuTopButton(bool open)
|
||||
{
|
||||
if (open == _menu.IsOpen) return;
|
||||
@@ -543,7 +448,7 @@ namespace Content.Client.Actions.UI
|
||||
// only initiate the drag if the slot has an action in it
|
||||
if (Locked || DragDropHelper.Dragged?.Action == null) return false;
|
||||
|
||||
_dragShadow.Texture = DragDropHelper.Dragged.Action.Icon.Frame0();
|
||||
_dragShadow.Texture = DragDropHelper.Dragged.Action.Icon?.Frame0();
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32));
|
||||
DragDropHelper.Dragged.CancelPress();
|
||||
return true;
|
||||
@@ -574,6 +479,7 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
var actionSlot = _slots[slot];
|
||||
actionSlot.Depress(args.State == BoundKeyState.Down);
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Movement.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Clothing
|
||||
{
|
||||
public sealed class MagbootsSystem : EntitySystem
|
||||
public sealed class MagbootsSystem : SharedMagbootsSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -204,7 +204,7 @@ namespace Content.Client.DragDrop
|
||||
}
|
||||
|
||||
HighlightTargets();
|
||||
_outline.Enabled = false;
|
||||
_outline.SetEnabled(false);
|
||||
|
||||
// drag initiated
|
||||
return true;
|
||||
@@ -257,7 +257,7 @@ namespace Content.Client.DragDrop
|
||||
EntityManager.DeleteEntity(_dragShadow);
|
||||
}
|
||||
|
||||
_outline.Enabled = true;
|
||||
_outline.SetEnabled(true);
|
||||
_dragShadow = default;
|
||||
_draggables.Clear();
|
||||
_dragger = default;
|
||||
@@ -353,6 +353,7 @@ namespace Content.Client.DragDrop
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO make this just use TargetOutlineSystem
|
||||
private void HighlightTargets()
|
||||
{
|
||||
if (_dragDropHelper.Dragged == default || Deleted(_dragDropHelper.Dragged) ||
|
||||
@@ -370,7 +371,7 @@ namespace Content.Client.DragDrop
|
||||
RemoveHighlights();
|
||||
|
||||
// find possible targets on screen even if not reachable
|
||||
// TODO: Duplicated in SpriteSystem
|
||||
// TODO: Duplicated in SpriteSystem and TargetOutlineSystem. Should probably be cached somewhere for a frame?
|
||||
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition).Position;
|
||||
var bounds = new Box2(mousePos - 1.5f, mousePos + 1.5f);
|
||||
var pvsEntities = IoCManager.Resolve<IEntityLookup>().GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.IncludeAnchored);
|
||||
|
||||
@@ -101,6 +101,9 @@ namespace Content.Client.Entry
|
||||
prototypes.RegisterIgnore("salvageMap");
|
||||
prototypes.RegisterIgnore("gamePreset");
|
||||
prototypes.RegisterIgnore("gameRule");
|
||||
prototypes.RegisterIgnore("worldSpell");
|
||||
prototypes.RegisterIgnore("entitySpell");
|
||||
prototypes.RegisterIgnore("instantSpell");
|
||||
|
||||
ClientContentIoC.Register();
|
||||
|
||||
@@ -189,7 +192,6 @@ namespace Content.Client.Entry
|
||||
IoCManager.Resolve<IClientPreferencesManager>().Initialize();
|
||||
IoCManager.Resolve<IStationEventManager>().Initialize();
|
||||
IoCManager.Resolve<EuiManager>().Initialize();
|
||||
IoCManager.Resolve<ActionManager>().Initialize();
|
||||
IoCManager.Resolve<IVoteManager>().Initialize();
|
||||
IoCManager.Resolve<IGamePrototypeLoadManager>().Initialize();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Content.Client.Entry
|
||||
"Temperature",
|
||||
"AtmosExposed",
|
||||
"Explosive",
|
||||
"Vocal",
|
||||
"OnUseTimerTrigger",
|
||||
"WarpPoint",
|
||||
"EmitSoundOnUse",
|
||||
|
||||
@@ -5,9 +5,6 @@ using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Hands
|
||||
{
|
||||
@@ -16,11 +13,15 @@ namespace Content.Client.Hands
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
private readonly IRenderTexture _renderBackbuffer;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||
|
||||
public Texture? IconOverride;
|
||||
public EntityUid? EntityOverride;
|
||||
|
||||
public ShowHandItemOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -43,15 +44,25 @@ namespace Content.Client.Hands
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var sys = EntitySystem.Get<HandsSystem>();
|
||||
var handEntity = sys.GetActiveHandEntity();
|
||||
|
||||
if (handEntity == null || !_cfg.GetCVar(CCVars.HudHeldItemShow) || !IoCManager.Resolve<IEntityManager>().HasComponent<ISpriteComponent>(handEntity))
|
||||
if (!_cfg.GetCVar(CCVars.HudHeldItemShow))
|
||||
return;
|
||||
|
||||
var screen = args.ScreenHandle;
|
||||
var halfSize = _renderBackbuffer.Size / 2;
|
||||
var offset = _cfg.GetCVar(CCVars.HudHeldItemOffset);
|
||||
var mousePos = _inputManager.MouseScreenPosition.Position;
|
||||
|
||||
if (IconOverride != null)
|
||||
{
|
||||
screen.DrawTexture(IconOverride, mousePos - IconOverride.Size / 2 + offset, Color.White.WithAlpha(0.75f));
|
||||
return;
|
||||
}
|
||||
|
||||
var handEntity = EntityOverride ?? EntitySystem.Get<HandsSystem>().GetActiveHandEntity();
|
||||
|
||||
if (handEntity == null || !_entMan.HasComponent<ISpriteComponent>(handEntity))
|
||||
return;
|
||||
|
||||
var halfSize = _renderBackbuffer.Size / 2;
|
||||
var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f;
|
||||
|
||||
screen.RenderInRenderTarget(_renderBackbuffer, () =>
|
||||
@@ -59,11 +70,7 @@ namespace Content.Client.Hands
|
||||
screen.DrawEntity(handEntity.Value, halfSize, new Vector2(1f, 1f) * uiScale, Direction.South);
|
||||
}, Color.Transparent);
|
||||
|
||||
var offset = _cfg.GetCVar(CCVars.HudHeldItemOffset);
|
||||
|
||||
var mousePos = _inputManager.MouseScreenPosition.Position;
|
||||
screen.DrawTexture(_renderBackbuffer.Texture, mousePos - halfSize + offset, Color.White.WithAlpha(0.75f));
|
||||
// screen.DrawRect(UIBox2.FromDimensions((offset, offset) + mousePos, (32, 32)), Color.Red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ namespace Content.Client.IoC
|
||||
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
|
||||
IoCManager.Register<IClickMapManager, ClickMapManager>();
|
||||
IoCManager.Register<IStationEventManager, StationEventManager>();
|
||||
IoCManager.Register<ActionManager, ActionManager>();
|
||||
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
|
||||
IoCManager.Register<EuiManager, EuiManager>();
|
||||
IoCManager.Register<IVoteManager, VoteManager>();
|
||||
|
||||
@@ -24,14 +24,74 @@ public sealed class InteractionOutlineSystem : EntitySystem
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public bool Enabled = true;
|
||||
/// <summary>
|
||||
/// Whether to currently draw the outline. The outline may be temporarily disabled by other systems
|
||||
/// </summary>
|
||||
private bool _enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to draw the outline at all. Overrides <see cref="_enabled"/>.
|
||||
/// </summary>
|
||||
private bool _cvarEnabled = true;
|
||||
|
||||
private EntityUid? _lastHoveredEntity;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_configManager.OnValueChanged(CCVars.OutlineEnabled, SetCvarEnabled);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
|
||||
_configManager.UnsubValueChanged(CCVars.OutlineEnabled, SetCvarEnabled);
|
||||
}
|
||||
|
||||
public void SetCvarEnabled(bool cvarEnabled)
|
||||
{
|
||||
_cvarEnabled = cvarEnabled;
|
||||
|
||||
// clear last hover if required:
|
||||
|
||||
if (_cvarEnabled)
|
||||
return;
|
||||
|
||||
if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
|
||||
return;
|
||||
|
||||
if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
|
||||
outline.OnMouseLeave();
|
||||
}
|
||||
|
||||
public void SetEnabled(bool enabled)
|
||||
{
|
||||
if (enabled == _enabled)
|
||||
return;
|
||||
|
||||
_enabled = enabled;
|
||||
|
||||
// clear last hover if required:
|
||||
|
||||
if (enabled)
|
||||
return;
|
||||
|
||||
if (_lastHoveredEntity == null || Deleted(_lastHoveredEntity))
|
||||
return;
|
||||
|
||||
if (TryComp(_lastHoveredEntity, out InteractionOutlineComponent? outline))
|
||||
outline.OnMouseLeave();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_enabled || !_cvarEnabled)
|
||||
return;
|
||||
|
||||
// If there is no local player, there is no session, and therefore nothing to do here.
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
@@ -81,16 +141,6 @@ public sealed class InteractionOutlineSystem : EntitySystem
|
||||
|
||||
InteractionOutlineComponent? outline;
|
||||
|
||||
if (!Enabled || !_configManager.GetCVar(CCVars.OutlineEnabled))
|
||||
{
|
||||
if (entityToClick != null && TryComp(entityToClick, out outline))
|
||||
{
|
||||
outline.OnMouseLeave(); //Prevent outline remains from persisting post command.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (entityToClick == _lastHoveredEntity)
|
||||
{
|
||||
if (entityToClick != null && TryComp(entityToClick, out outline))
|
||||
|
||||
179
Content.Client/Outline/TargetOutlineSystem.cs
Normal file
179
Content.Client/Outline/TargetOutlineSystem.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Outline;
|
||||
|
||||
/// <summary>
|
||||
/// System used to indicate whether an entity is a valid target based on some criteria.
|
||||
/// </summary>
|
||||
public sealed class TargetOutlineSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
private bool _enabled = false;
|
||||
|
||||
/// <summary>
|
||||
/// Whitelist that the target must satisfy.
|
||||
/// </summary>
|
||||
public EntityWhitelist? Whitelist = null;
|
||||
|
||||
/// <summary>
|
||||
/// Predicate the target must satisfy.
|
||||
/// </summary>
|
||||
public Func<EntityUid, bool>? Predicate = null;
|
||||
|
||||
/// <summary>
|
||||
/// Event to raise as targets to check whether they are valid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event will be uncanceled and re-used.
|
||||
/// </remarks>
|
||||
public CancellableEntityEventArgs? ValidationEvent = null;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum range for a target to be valid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a target is further than this distance, they will still be highlighted in a different color.
|
||||
/// </remarks>
|
||||
public float Range = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to check if the player is unobstructed to the target;
|
||||
/// </summary>
|
||||
public bool CheckObstruction = true;
|
||||
|
||||
/// <summary>
|
||||
/// The size of the box around the mouse to use when looking for valid targets.
|
||||
/// </summary>
|
||||
public float LookupSize = 2;
|
||||
|
||||
private const string ShaderTargetValid = "SelectionOutlineInrange";
|
||||
private const string ShaderTargetInvalid = "SelectionOutline";
|
||||
private ShaderInstance? _shaderTargetValid;
|
||||
private ShaderInstance? _shaderTargetInvalid;
|
||||
|
||||
private readonly HashSet<SpriteComponent> _highlightedSprites = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_shaderTargetValid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetValid).Instance();
|
||||
_shaderTargetInvalid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetInvalid).Instance();
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (_enabled == false)
|
||||
return;
|
||||
|
||||
_enabled = false;
|
||||
RemoveHighlights();
|
||||
}
|
||||
|
||||
public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, CancellableEntityEventArgs? validationEvent)
|
||||
{
|
||||
Range = range;
|
||||
CheckObstruction = checkObstructions;
|
||||
Predicate = predicate;
|
||||
Whitelist = whitelist;
|
||||
ValidationEvent = validationEvent;
|
||||
|
||||
_enabled = Predicate != null || Whitelist != null || ValidationEvent != null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
HighlightTargets();
|
||||
}
|
||||
|
||||
private void HighlightTargets()
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } player)
|
||||
return;
|
||||
|
||||
// remove current highlights
|
||||
RemoveHighlights();
|
||||
|
||||
// find possible targets on screen
|
||||
// TODO: Duplicated in SpriteSystem and DragDropSystem. Should probably be cached somewhere for a frame?
|
||||
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition).Position;
|
||||
var bounds = new Box2(mousePos - LookupSize, mousePos + LookupSize);
|
||||
var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentMap, bounds, LookupFlags.Approximate | LookupFlags.IncludeAnchored);
|
||||
|
||||
foreach (var entity in pvsEntities)
|
||||
{
|
||||
if (!TryComp(entity, out SpriteComponent? sprite) || !sprite.Visible)
|
||||
continue;
|
||||
|
||||
// Check the predicate
|
||||
var valid = Predicate?.Invoke(entity) ?? true;
|
||||
|
||||
// check the entity whitelist
|
||||
if (valid && Whitelist != null)
|
||||
valid = Whitelist.IsValid(entity);
|
||||
|
||||
// and check the cancellable event
|
||||
if (valid && ValidationEvent != null)
|
||||
{
|
||||
ValidationEvent.Uncancel();
|
||||
RaiseLocalEvent(entity, (object) ValidationEvent, broadcast: false);
|
||||
valid = !ValidationEvent.Cancelled;
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
// was this previously valid?
|
||||
if (_highlightedSprites.Remove(sprite))
|
||||
{
|
||||
sprite.PostShader = null;
|
||||
sprite.RenderOrder = 0;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Range check
|
||||
if (CheckObstruction)
|
||||
valid = _interactionSystem.InRangeUnobstructed(player, entity, Range);
|
||||
else if (Range >= 0)
|
||||
{
|
||||
var origin = Transform(player).WorldPosition;
|
||||
var target = Transform(entity).WorldPosition;
|
||||
valid = (origin - target).LengthSquared <= Range;
|
||||
}
|
||||
|
||||
// highlight depending on whether its in or out of range
|
||||
sprite.PostShader = valid ? _shaderTargetValid : _shaderTargetInvalid;
|
||||
sprite.RenderOrder = EntityManager.CurrentTick.Value;
|
||||
_highlightedSprites.Add(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveHighlights()
|
||||
{
|
||||
foreach (var sprite in _highlightedSprites)
|
||||
{
|
||||
sprite.PostShader = null;
|
||||
sprite.RenderOrder = 0;
|
||||
}
|
||||
|
||||
_highlightedSprites.Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,389 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Items.Components;
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Item;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(SharedActionsComponent))]
|
||||
[TestOf(typeof(ClientActionsComponent))]
|
||||
[TestOf(typeof(ServerActionsComponent))]
|
||||
[TestOf(typeof(ItemActionsComponent))]
|
||||
public sealed class ActionsComponentTests : ContentIntegrationTest
|
||||
{
|
||||
const string Prototypes = @"
|
||||
- type: entity
|
||||
name: flashlight
|
||||
parent: BaseItem
|
||||
id: TestFlashlight
|
||||
components:
|
||||
- type: HandheldLight
|
||||
- type: ItemActions
|
||||
actions:
|
||||
- actionType: ToggleLight
|
||||
- type: PowerCellSlot
|
||||
- type: Sprite
|
||||
sprite: Objects/Tools/flashlight.rsi
|
||||
layers:
|
||||
- state: flashlight
|
||||
- state: flashlight-overlay
|
||||
shader: unshaded
|
||||
visible: false
|
||||
- type: Item
|
||||
sprite: Objects/Tools/flashlight.rsi
|
||||
HeldPrefix: off
|
||||
- type: PointLight
|
||||
enabled: false
|
||||
radius: 3
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: FlashLightVisualizer
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task GrantsAndRevokesActionsTest()
|
||||
{
|
||||
var (client, server) = await StartConnectedServerClientPair();
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
await client.WaitIdleAsync();
|
||||
|
||||
var cEntities = client.ResolveDependency<IEntityManager>();
|
||||
|
||||
var sEntities = server.ResolveDependency<IEntityManager>();
|
||||
var serverPlayerManager = server.ResolveDependency<IPlayerManager>();
|
||||
var innateActions = new List<ActionType>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var playerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
|
||||
Assert.That(playerEnt, Is.Not.EqualTo(default));
|
||||
var actionsComponent = sEntities.GetComponent<ServerActionsComponent>(playerEnt);
|
||||
|
||||
// player should begin with their innate actions granted
|
||||
innateActions.AddRange(actionsComponent.InnateActions);
|
||||
foreach (var innateAction in actionsComponent.InnateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
Assert.That(innateActions.Count, Is.GreaterThan(0));
|
||||
|
||||
actionsComponent.Grant(ActionType.DebugInstant);
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.HumanScream, out var state) && state.Enabled);
|
||||
});
|
||||
|
||||
// check that client has the actions
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
var clientPlayerMgr = client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
|
||||
var expectedOrder = new List<ActionType>();
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
var local = clientPlayerMgr.LocalPlayer;
|
||||
var controlled = local!.ControlledEntity;
|
||||
var actionsComponent = cEntities.GetComponent<ClientActionsComponent>(controlled!.Value);
|
||||
|
||||
// we should have our innate actions and debug1.
|
||||
foreach (var innateAction in innateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out var state) && state.Enabled);
|
||||
|
||||
// innate actions should've auto-populated into our slots (in non-deterministic order),
|
||||
// but debug1 should be in the last slot
|
||||
var actionsUI =
|
||||
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
|
||||
Assert.That(actionsUI, Is.Not.Null);
|
||||
|
||||
var expectedInnate = new HashSet<ActionType>(innateActions);
|
||||
var expectEmpty = false;
|
||||
expectedOrder.Clear();
|
||||
foreach (var slot in actionsUI.Slots)
|
||||
{
|
||||
if (expectEmpty)
|
||||
{
|
||||
Assert.That(slot.HasAssignment, Is.False);
|
||||
Assert.That(slot.Item, Is.EqualTo(default));
|
||||
Assert.That(slot.Action, Is.Null);
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
continue;
|
||||
}
|
||||
Assert.That(slot.HasAssignment);
|
||||
// all the actions we gave so far are not tied to an item
|
||||
Assert.That(slot.Item, Is.EqualTo(default));
|
||||
Assert.That(slot.Action, Is.Not.Null);
|
||||
Assert.That(slot.ActionEnabled);
|
||||
var asAction = slot.Action as ActionPrototype;
|
||||
Assert.That(asAction, Is.Not.Null);
|
||||
expectedOrder.Add(asAction.ActionType);
|
||||
|
||||
if (expectedInnate.Count != 0)
|
||||
{
|
||||
Assert.That(expectedInnate.Remove(asAction.ActionType));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(asAction.ActionType, Is.EqualTo(ActionType.DebugInstant));
|
||||
Assert.That(slot.Cooldown, Is.Null);
|
||||
expectEmpty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// now revoke the action and check that the client sees it as revoked
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var playerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
|
||||
Assert.That(playerEnt, Is.Not.EqualTo(default));
|
||||
var actionsComponent = sEntities.GetComponent<ServerActionsComponent>(playerEnt);
|
||||
actionsComponent.Revoke(ActionType.DebugInstant);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
var local = clientPlayerMgr.LocalPlayer;
|
||||
var controlled = local!.ControlledEntity;
|
||||
var actionsComponent = cEntities.GetComponent<ClientActionsComponent>(controlled!.Value);
|
||||
|
||||
// we should have our innate actions, but debug1 should be revoked
|
||||
foreach (var innateAction in innateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out _), Is.False);
|
||||
|
||||
// all actions should be in the same order as before, but the slot with DebugInstant should appear
|
||||
// disabled.
|
||||
var actionsUI =
|
||||
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
|
||||
Assert.That(actionsUI, Is.Not.Null);
|
||||
|
||||
var idx = 0;
|
||||
foreach (var slot in actionsUI.Slots)
|
||||
{
|
||||
if (idx < expectedOrder.Count)
|
||||
{
|
||||
var expected = expectedOrder[idx++];
|
||||
Assert.That(slot.HasAssignment);
|
||||
// all the actions we gave so far are not tied to an item
|
||||
Assert.That(slot.Item, Is.EqualTo(default));
|
||||
Assert.That(slot.Action, Is.Not.Null);
|
||||
var asAction = slot.Action as ActionPrototype;
|
||||
Assert.That(asAction, Is.Not.Null);
|
||||
Assert.That(expected, Is.EqualTo(asAction.ActionType));
|
||||
|
||||
if (asAction.ActionType == ActionType.DebugInstant)
|
||||
{
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(slot.ActionEnabled);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(slot.HasAssignment, Is.False);
|
||||
Assert.That(slot.Item, Is.EqualTo(default));
|
||||
Assert.That(slot.Action, Is.Null);
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GrantsAndRevokesItemActions()
|
||||
{
|
||||
var serverOptions = new ServerIntegrationOptions
|
||||
{
|
||||
ExtraPrototypes = Prototypes,
|
||||
CVarOverrides =
|
||||
{
|
||||
{CVars.NetPVS.Name, "false"}
|
||||
}
|
||||
};
|
||||
var clientOptions = new ClientIntegrationOptions { ExtraPrototypes = Prototypes };
|
||||
var (client, server) = await StartConnectedServerClientPair(serverOptions: serverOptions, clientOptions: clientOptions);
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
await client.WaitIdleAsync();
|
||||
|
||||
var clientEntities = client.ResolveDependency<IEntityManager>();
|
||||
|
||||
var serverPlayerManager = server.ResolveDependency<IPlayerManager>();
|
||||
var serverEntManager = server.ResolveDependency<IEntityManager>();
|
||||
var serverGameTiming = server.ResolveDependency<IGameTiming>();
|
||||
|
||||
var cooldown = Cooldowns.SecondsFromNow(30, serverGameTiming);
|
||||
|
||||
ServerActionsComponent serverActionsComponent = null;
|
||||
ClientActionsComponent clientActionsComponent = null;
|
||||
EntityUid serverPlayerEnt = default;
|
||||
EntityUid serverFlashlight = default;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
serverPlayerEnt = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
|
||||
Assert.That(serverPlayerEnt, Is.Not.EqualTo(default));
|
||||
serverActionsComponent = serverEntManager.GetComponent<ServerActionsComponent>(serverPlayerEnt);
|
||||
|
||||
// spawn and give them an item that has actions
|
||||
serverFlashlight = serverEntManager.SpawnEntity("TestFlashlight",
|
||||
new EntityCoordinates(serverPlayerEnt, (0, 0)));
|
||||
Assert.That(serverEntManager.TryGetComponent<ItemActionsComponent>(serverFlashlight, out var itemActions));
|
||||
// we expect this only to have a toggle light action initially
|
||||
var actionConfigs = itemActions.ActionConfigs.ToList();
|
||||
Assert.That(actionConfigs.Count == 1);
|
||||
Assert.That(actionConfigs[0].ActionType == ItemActionType.ToggleLight);
|
||||
Assert.That(actionConfigs[0].Enabled);
|
||||
|
||||
// grant an extra item action, before pickup, initially disabled
|
||||
itemActions.GrantOrUpdate(ItemActionType.DebugToggle, false);
|
||||
serverEntManager.GetComponent<HandsComponent>(serverPlayerEnt).PutInHand(serverEntManager.GetComponent<SharedItemComponent>(serverFlashlight), false);
|
||||
// grant an extra item action, after pickup, with a cooldown
|
||||
itemActions.GrantOrUpdate(ItemActionType.DebugInstant, cooldown: cooldown);
|
||||
|
||||
Assert.That(serverActionsComponent.TryGetItemActionStates(serverFlashlight, out var state));
|
||||
// they should have been granted all 3 actions
|
||||
Assert.That(state.Count == 3);
|
||||
Assert.That(state.TryGetValue(ItemActionType.ToggleLight, out var toggleLightState));
|
||||
Assert.That(toggleLightState.Equals(new ActionState(true)));
|
||||
Assert.That(state.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
|
||||
Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
|
||||
Assert.That(state.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
|
||||
Assert.That(debugToggleState.Equals(new ActionState(false)));
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
// check that client has the actions, and toggle the light on via the action slot it was auto-assigned to
|
||||
var clientPlayerMgr = client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
|
||||
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
|
||||
EntityUid clientFlashlight = default;
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
var local = clientPlayerMgr.LocalPlayer;
|
||||
var controlled = local!.ControlledEntity;
|
||||
clientActionsComponent = clientEntities.GetComponent<ClientActionsComponent>(controlled!.Value);
|
||||
|
||||
var lightEntry = clientActionsComponent.ItemActionStates()
|
||||
.Where(entry => entry.Value.ContainsKey(ItemActionType.ToggleLight))
|
||||
.FirstOrNull();
|
||||
clientFlashlight = lightEntry!.Value.Key;
|
||||
Assert.That(lightEntry, Is.Not.Null);
|
||||
Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.ToggleLight, out var lightState));
|
||||
Assert.That(lightState.Equals(new ActionState(true)));
|
||||
Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
|
||||
Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
|
||||
Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
|
||||
Assert.That(debugToggleState.Equals(new ActionState(false)));
|
||||
|
||||
var actionsUI = clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
|
||||
Assert.That(actionsUI, Is.Not.Null);
|
||||
|
||||
var toggleLightSlot = actionsUI.Slots.FirstOrDefault(slot => slot.Action is ItemActionPrototype
|
||||
{
|
||||
ActionType: ItemActionType.ToggleLight
|
||||
});
|
||||
Assert.That(toggleLightSlot, Is.Not.Null);
|
||||
|
||||
clientActionsComponent.AttemptAction(toggleLightSlot);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
// server should see the action toggled on
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight, out var lightStates));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
|
||||
Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true)));
|
||||
});
|
||||
|
||||
// client should see it toggled on.
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
|
||||
Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true)));
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// drop the item, and the item actions should go away
|
||||
serverEntManager.GetComponent<HandsComponent>(serverPlayerEnt)
|
||||
.TryDropEntity(serverFlashlight, serverEntManager.GetComponent<TransformComponent>(serverPlayerEnt).Coordinates, false);
|
||||
Assert.That(serverActionsComponent.ItemActionStates().ContainsKey(serverFlashlight), Is.False);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
// client should see they have no item actions for that item either.
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(clientActionsComponent.ItemActionStates().ContainsKey(clientFlashlight), Is.False);
|
||||
});
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
// pick the item up again, the states should be back to what they were when dropped,
|
||||
// as the states "stick" with the item
|
||||
serverEntManager.GetComponent<HandsComponent>(serverPlayerEnt).PutInHand(serverEntManager.GetComponent<SharedItemComponent>(serverFlashlight), false);
|
||||
Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight, out var lightStates));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
|
||||
Assert.That(lightState.Equals(new ActionState(true, toggledOn: true)));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
|
||||
Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
|
||||
Assert.That(debugToggleState.Equals(new ActionState(false)));
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
// client should see the actions again, with their states back to what they were
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState));
|
||||
Assert.That(lightState.Equals(new ActionState(true, toggledOn: true)));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState));
|
||||
Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown)));
|
||||
Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState));
|
||||
Assert.That(debugToggleState.Equals(new ActionState(false)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,6 @@
|
||||
using System;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Act
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements behavior when an entity is disarmed.
|
||||
/// </summary>
|
||||
[RequiresExplicitImplementation, Obsolete("Use the directed event instead.")]
|
||||
public interface IDisarmedAct
|
||||
{
|
||||
/// <summary>
|
||||
/// Behavior when the entity is disarmed.
|
||||
/// Return true to prevent the default disarm behavior,
|
||||
/// or rest of IDisarmedAct behaviors that come after this one from happening.
|
||||
/// </summary>
|
||||
bool Disarmed(DisarmedActEvent @event);
|
||||
|
||||
/// <summary>
|
||||
/// Priority for this disarm act.
|
||||
/// Used to determine act execution order.
|
||||
/// </summary>
|
||||
int Priority => 0;
|
||||
}
|
||||
|
||||
public sealed class DisarmedActEvent : HandledEntityEventArgs
|
||||
public sealed class DisarmedEvent : HandledEntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity being disarmed.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
using Content.Server.CombatMode;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class CombatMode : IToggleAction
|
||||
{
|
||||
public bool DoToggleAction(ToggleActionEventArgs args)
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(args.Performer, out CombatModeComponent? combatMode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
args.Performer.PopupMessage(Loc.GetString(args.ToggledOn ? "hud-combat-enabled" : "hud-combat-disabled"));
|
||||
combatMode.IsInCombatMode = args.ToggledOn;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Cooldown;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Just shows a popup message.asd
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class DebugInstant : IInstantAction, IInstantItemAction
|
||||
{
|
||||
[DataField("message")] public string Message { get; [UsedImplicitly] private set; } = "Instant action used.";
|
||||
[DataField("cooldown")] public float Cooldown { get; [UsedImplicitly] private set; }
|
||||
|
||||
public void DoInstantAction(InstantItemActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(Message);
|
||||
if (Cooldown > 0)
|
||||
{
|
||||
args.ItemActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
|
||||
}
|
||||
}
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(Message);
|
||||
args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class DebugTargetEntity : ITargetEntityAction, ITargetEntityItemAction
|
||||
{
|
||||
public void DoTargetEntityAction(TargetEntityItemActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
args.Performer.PopupMessageEveryone(entMan.GetComponent<MetaDataComponent>(args.Item).EntityName + ": Clicked " +
|
||||
entMan.GetComponent<MetaDataComponent>(args.Target).EntityName);
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(TargetEntityActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone("Clicked " + IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(args.Target).EntityName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class DebugTargetPoint : ITargetPointAction, ITargetPointItemAction
|
||||
{
|
||||
public void DoTargetPointAction(TargetPointItemActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(args.Item).EntityName + ": Clicked local position " +
|
||||
args.Target);
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(TargetPointActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone("Clicked local position " +
|
||||
args.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class DebugToggle : IToggleAction, IToggleItemAction
|
||||
{
|
||||
[DataField("messageOn")] public string MessageOn { get; private set; } = "on!";
|
||||
[DataField("messageOff")] public string MessageOff { get; private set; } = "off!";
|
||||
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (args.ToggledOn)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(entMan.GetComponent<MetaDataComponent>(args.Item).EntityName + ": " + MessageOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(entMan.GetComponent<MetaDataComponent>(args.Item).EntityName + ": " +MessageOff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DoToggleAction(ToggleActionEventArgs args)
|
||||
{
|
||||
if (args.ToggledOn)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(MessageOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(MessageOff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.Act;
|
||||
using Content.Server.Actions.Events;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Weapon.Melee;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class DisarmAction : ITargetEntityAction
|
||||
{
|
||||
[DataField("failProb")] private float _failProb = 0.4f;
|
||||
[DataField("pushProb")] private float _pushProb = 0.4f;
|
||||
[DataField("cooldown")] private float _cooldown = 1.5f;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("punchMissSound")]
|
||||
private SoundSpecifier PunchMissSound { get; } = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg");
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("disarmSuccessSound")]
|
||||
private SoundSpecifier DisarmSuccessSound { get; } = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
|
||||
public void DoTargetEntityAction(TargetEntityActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var disarmedActs = entMan.GetComponents<IDisarmedAct>(args.Target).ToArray();
|
||||
var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer);
|
||||
|
||||
entMan.EventBus.RaiseLocalEvent(args.Target, attemptEvent);
|
||||
|
||||
if (attemptEvent.Cancelled)
|
||||
return;
|
||||
|
||||
var sys = EntitySystem.Get<InteractionSystem>();
|
||||
|
||||
if (!sys.InRangeUnobstructed(args.Performer, args.Target)) return;
|
||||
|
||||
if (disarmedActs.Length == 0)
|
||||
{
|
||||
if (entMan.TryGetComponent(args.Performer, out ActorComponent? actor))
|
||||
{
|
||||
// Fall back to a normal interaction with the entity
|
||||
var player = actor.PlayerSession;
|
||||
var coordinates = entMan.GetComponent<TransformComponent>(args.Target).Coordinates;
|
||||
var target = args.Target;
|
||||
sys.HandleUseInteraction(player, coordinates, target);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entMan.TryGetComponent<SharedActionsComponent?>(args.Performer, out var actions)) return;
|
||||
if (args.Target == args.Performer || !EntitySystem.Get<ActionBlockerSystem>().CanAttack(args.Performer)) return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var system = EntitySystem.Get<MeleeWeaponSystem>();
|
||||
|
||||
var diff = entMan.GetComponent<TransformComponent>(args.Target).MapPosition.Position - entMan.GetComponent<TransformComponent>(args.Performer).MapPosition.Position;
|
||||
var angle = Angle.FromWorldVec(diff);
|
||||
|
||||
actions.Cooldown(ActionType.Disarm, Cooldowns.SecondsFromNow(_cooldown));
|
||||
|
||||
if (random.Prob(_failProb))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), PunchMissSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
|
||||
|
||||
args.Performer.PopupMessageOtherClients(Loc.GetString("disarm-action-popup-message-other-clients",
|
||||
("performerName", entMan.GetComponent<MetaDataComponent>(args.Performer).EntityName),
|
||||
("targetName", entMan.GetComponent<MetaDataComponent>(args.Target).EntityName)));
|
||||
args.Performer.PopupMessageCursor(Loc.GetString("disarm-action-popup-message-cursor",
|
||||
("targetName", entMan.GetComponent<MetaDataComponent>(args.Target).EntityName)));
|
||||
system.SendLunge(angle, args.Performer);
|
||||
return;
|
||||
}
|
||||
|
||||
system.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target });
|
||||
|
||||
var eventArgs = new DisarmedActEvent() { Target = args.Target, Source = args.Performer, PushProbability = _pushProb };
|
||||
|
||||
entMan.EventBus.RaiseLocalEvent(args.Target, eventArgs);
|
||||
|
||||
EntitySystem.Get<AdminLogSystem>().Add(LogType.DisarmedAction, LogImpact.Low, $"{entMan.ToPrettyString(args.Performer):user} used disarm on {entMan.ToPrettyString(args.Target):target}");
|
||||
|
||||
// Check if the event has been handled, and if so, do nothing else!
|
||||
if (eventArgs.Handled)
|
||||
return;
|
||||
|
||||
// Sort by priority.
|
||||
Array.Sort(disarmedActs, (a, b) => a.Priority.CompareTo(b.Priority));
|
||||
|
||||
// TODO: Remove this shit.
|
||||
foreach (var disarmedAct in disarmedActs)
|
||||
{
|
||||
if (disarmedAct.Disarmed(eventArgs))
|
||||
return;
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), DisarmSuccessSound.GetSound(), entMan.GetComponent<TransformComponent>(args.Performer).Coordinates, AudioHelpers.WithVariation(0.025f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Cooldown;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Blink lights and scare livings
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class GhostBoo : IInstantAction
|
||||
{
|
||||
[DataField("radius")] private float _radius = 3;
|
||||
[DataField("cooldown")] private float _cooldown = 120;
|
||||
[DataField("maxTargets")] private int _maxTargets = 3;
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entMan.TryGetComponent<SharedActionsComponent?>(args.Performer, out var actions)) return;
|
||||
|
||||
// find all IGhostBooAffected nearby and do boo on them
|
||||
var ents = IoCManager.Resolve<IEntityLookup>().GetEntitiesInRange(args.Performer, _radius);
|
||||
|
||||
var booCounter = 0;
|
||||
foreach (var ent in ents)
|
||||
{
|
||||
var ghostBoo = new GhostBooEvent();
|
||||
entMan.EventBus.RaiseLocalEvent(ent, ghostBoo);
|
||||
|
||||
if (ghostBoo.Handled)
|
||||
booCounter++;
|
||||
|
||||
if (booCounter >= _maxTargets)
|
||||
break;
|
||||
}
|
||||
|
||||
actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using Content.Server.Guardian;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Manifests the guardian saved in the action, using the system
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class ToggleGuardianAction : IInstantAction
|
||||
{
|
||||
[DataField("cooldown")] public float Cooldown { get; [UsedImplicitly] private set; }
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (entManager.TryGetComponent(args.Performer, out GuardianHostComponent? hostComponent) &&
|
||||
hostComponent.HostedGuardian != null)
|
||||
{
|
||||
EntitySystem.Get<GuardianSystem>().ToggleGuardian(hostComponent);
|
||||
args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Performer.PopupMessage(Loc.GetString("guardian-missing-invalid-action"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Cooldown;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Server.GameObjects;
|
||||
using Content.Shared.Instruments;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Pull up MIDI instrument interface for PAIs to "play themselves"
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class PAIMidi : IInstantAction
|
||||
{
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (!entMan.TryGetComponent<ServerUserInterfaceComponent?>(args.Performer, out var serverUi)) return;
|
||||
if (!entMan.TryGetComponent<ActorComponent?>(args.Performer, out var actor)) return;
|
||||
if (!serverUi.TryGetBoundUserInterface(InstrumentUiKey.Key,out var bui)) return;
|
||||
|
||||
bui.Toggle(actor.PlayerSession);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.CharacterAppearance;
|
||||
using Content.Shared.CharacterAppearance.Components;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class ScreamAction : IInstantAction, ISerializationHooks
|
||||
{
|
||||
private const float Variation = 0.125f;
|
||||
private const float Volume = 4f;
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
[DataField("male", required: true)] private SoundSpecifier _male = default!;
|
||||
[DataField("female", required: true)] private SoundSpecifier _female = default!;
|
||||
[DataField("wilhelm", required: true)] private SoundSpecifier _wilhelm = default!;
|
||||
|
||||
/// seconds
|
||||
[DataField("cooldown")] private float _cooldown = 10;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanSpeak(args.Performer)) return;
|
||||
if (!_entMan.TryGetComponent<HumanoidAppearanceComponent?>(args.Performer, out var humanoid)) return;
|
||||
if (!_entMan.TryGetComponent<SharedActionsComponent?>(args.Performer, out var actions)) return;
|
||||
|
||||
if (_random.Prob(.01f))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), _wilhelm.GetSound(), args.Performer, AudioParams.Default.WithVolume(Volume));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (humanoid.Sex)
|
||||
{
|
||||
case Sex.Male:
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), _male.GetSound(), args.Performer, AudioHelpers.WithVariation(Variation).WithVolume(Volume));
|
||||
break;
|
||||
case Sex.Female:
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), _female.GetSound(), args.Performer, AudioHelpers.WithVariation(Variation).WithVolume(Volume));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
|
||||
}
|
||||
}
|
||||
}
|
||||
40
Content.Server/Actions/ActionsSystem.cs
Normal file
40
Content.Server/Actions/ActionsSystem.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ActionsSystem : SharedActionsSystem
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
// need to send state to new player.
|
||||
component.Dirty();
|
||||
}
|
||||
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action)
|
||||
{
|
||||
var result = base.PerformBasicActions(user, action);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action.Speech))
|
||||
{
|
||||
_chatMan.EntitySay(user, Loc.GetString(action.Speech));
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Commands;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Actions.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class CooldownAction : IConsoleCommand
|
||||
{
|
||||
public string Command => "coolaction";
|
||||
public string Description => "Sets a cooldown on an action for a player, defaulting to current player";
|
||||
public string Help => "coolaction <actionType> <seconds> <name or userID, omit for current player>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player as IPlayerSession;
|
||||
if (player?.AttachedEntity is not {} attachedEntity) return;
|
||||
if (args.Length > 2)
|
||||
{
|
||||
var target = args[2];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.WriteError("user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.WriteLine("unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.WriteLine("unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
var cooldownStart = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
if (!uint.TryParse(args[1], out var seconds))
|
||||
{
|
||||
shell.WriteLine("cannot parse seconds: " + args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
var cooldownEnd = cooldownStart.Add(TimeSpan.FromSeconds(seconds));
|
||||
|
||||
actionsComponent.Cooldown(action.ActionType, (cooldownStart, cooldownEnd));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Commands;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Actions.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class GrantAction : IConsoleCommand
|
||||
{
|
||||
public string Command => "grantaction";
|
||||
public string Description => "Grants an action to a player, defaulting to current player";
|
||||
public string Help => "grantaction <actionType> <name or userID, omit for current player>";
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player as IPlayerSession;
|
||||
if (player?.AttachedEntity == null) return;
|
||||
var attachedEntity = player.AttachedEntity.Value;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
var target = args[1];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (attachedEntity == default) return;
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.WriteLine("user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.WriteLine("unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.WriteLine("unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
actionsComponent.Grant(action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Commands;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Actions.Commands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class RevokeAction : IConsoleCommand
|
||||
{
|
||||
public string Command => "revokeaction";
|
||||
public string Description => "Revokes an action from a player, defaulting to current player";
|
||||
public string Help => "revokeaction <actionType> <name or userID, omit for current player>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player as IPlayerSession;
|
||||
if (player?.AttachedEntity == null) return;
|
||||
var attachedEntity = player.AttachedEntity.Value;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
var target = args[1];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
if (attachedEntity == default) return;
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(attachedEntity, out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.WriteLine("user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.WriteLine("unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.WriteLine("unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
actionsComponent.Revoke(action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedActionsComponent))]
|
||||
public sealed class ServerActionsComponent : SharedActionsComponent
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
private float MaxUpdateRange;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_configManager.OnValueChanged(CVars.NetMaxUpdateRange, OnRangeChanged, true);
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_configManager.UnsubValueChanged(CVars.NetMaxUpdateRange, OnRangeChanged);
|
||||
}
|
||||
|
||||
private void OnRangeChanged(float obj)
|
||||
{
|
||||
MaxUpdateRange = obj;
|
||||
}
|
||||
|
||||
[Obsolete("Component Messages are deprecated, use Entity Events instead.")]
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
|
||||
{
|
||||
base.HandleNetworkMessage(message, netChannel, session);
|
||||
|
||||
if (message is not BasePerformActionMessage performActionMessage) return;
|
||||
if (session == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
if (session.AttachedEntity is not {Valid: true} player || player != Owner) return;
|
||||
var attempt = ActionAttempt(performActionMessage, session);
|
||||
if (attempt == null) return;
|
||||
|
||||
if (!attempt.TryGetActionState(this, out var actionState) || !actionState.Enabled)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to use" +
|
||||
" action {1} which is not granted to them", _entities.GetComponent<MetaDataComponent>(player).EntityName,
|
||||
attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionState.IsOnCooldown(GameTiming))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to use" +
|
||||
" action {1} which is on cooldown", _entities.GetComponent<MetaDataComponent>(player).EntityName,
|
||||
attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (performActionMessage.BehaviorType)
|
||||
{
|
||||
case BehaviorType.Instant:
|
||||
attempt.DoInstantAction(player);
|
||||
break;
|
||||
case BehaviorType.Toggle:
|
||||
if (performActionMessage is not IToggleActionMessage toggleMsg) return;
|
||||
if (toggleMsg.ToggleOn == actionState.ToggledOn)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" toggle action {1} to {2}, but it is already toggled {2}", _entities.GetComponent<MetaDataComponent>(player).EntityName,
|
||||
attempt.Action.Name, toggleMsg.ToggleOn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt.DoToggleAction(player, toggleMsg.ToggleOn))
|
||||
{
|
||||
attempt.ToggleAction(this, toggleMsg.ToggleOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if client predicted the toggle will work, need to reset
|
||||
// that prediction
|
||||
Dirty();
|
||||
}
|
||||
break;
|
||||
case BehaviorType.TargetPoint:
|
||||
if (performActionMessage is not ITargetPointActionMessage targetPointMsg) return;
|
||||
if (!CheckRangeAndSetFacing(targetPointMsg.Target, player)) return;
|
||||
attempt.DoTargetPointAction(player, targetPointMsg.Target);
|
||||
break;
|
||||
case BehaviorType.TargetEntity:
|
||||
if (performActionMessage is not ITargetEntityActionMessage targetEntityMsg) return;
|
||||
if (!EntityManager.EntityExists(targetEntityMsg.Target))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform target entity action {1} but could not find entity with " +
|
||||
"provided uid {2}", _entities.GetComponent<MetaDataComponent>(player).EntityName, attempt.Action.Name,
|
||||
targetEntityMsg.Target);
|
||||
return;
|
||||
}
|
||||
if (!CheckRangeAndSetFacing(_entities.GetComponent<TransformComponent>(targetEntityMsg.Target).Coordinates, player)) return;
|
||||
|
||||
attempt.DoTargetEntityAction(player, targetEntityMsg.Target);
|
||||
break;
|
||||
case BehaviorType.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionAttempt? ActionAttempt(BasePerformActionMessage message, ICommonSession session)
|
||||
{
|
||||
IActionAttempt? attempt;
|
||||
var player = session.AttachedEntity;
|
||||
|
||||
switch (message)
|
||||
{
|
||||
case PerformActionMessage performActionMessage:
|
||||
if (!ActionManager.TryGet(performActionMessage.ActionType, out var action))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" unrecognized action {1}", player,
|
||||
performActionMessage.ActionType);
|
||||
return null;
|
||||
}
|
||||
attempt = new ActionAttempt(action);
|
||||
break;
|
||||
case PerformItemActionMessage performItemActionMessage:
|
||||
var type = performItemActionMessage.ActionType;
|
||||
if (!ActionManager.TryGet(type, out var itemAction))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" unrecognized item action {1}",
|
||||
player, type);
|
||||
return null;
|
||||
}
|
||||
|
||||
var item = performItemActionMessage.Item;
|
||||
if (!EntityManager.EntityExists(item))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for unknown item {2}",
|
||||
player, type, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!_entities.TryGetComponent<ItemActionsComponent?>(item, out var actionsComponent))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for item {2} which has no ItemActionsComponent",
|
||||
player, type, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actionsComponent.Holder != player)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for item {2} which they are not holding",
|
||||
player, type, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
attempt = new ItemActionAttempt(itemAction, item, actionsComponent);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.BehaviorType != attempt.Action.BehaviorType)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform action {1} as a {2} behavior, but this action is actually a" +
|
||||
" {3} behavior", player, attempt, message.BehaviorType,
|
||||
attempt.Action.BehaviorType);
|
||||
return null;
|
||||
}
|
||||
|
||||
return attempt;
|
||||
}
|
||||
|
||||
private bool CheckRangeAndSetFacing(EntityCoordinates target, EntityUid player)
|
||||
{
|
||||
// ensure it's within their clickable range
|
||||
var targetWorldPos = target.ToMapPos(EntityManager);
|
||||
var rangeBox = new Box2(_entities.GetComponent<TransformComponent>(player).WorldPosition, _entities.GetComponent<TransformComponent>(player).WorldPosition)
|
||||
.Enlarged(MaxUpdateRange);
|
||||
if (!rangeBox.Contains(targetWorldPos))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform target action further than allowed range",
|
||||
_entities.GetComponent<MetaDataComponent>(player).EntityName);
|
||||
return false;
|
||||
}
|
||||
|
||||
EntitySystem.Get<RotateToFaceSystem>().TryFaceCoordinates(player, targetWorldPos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Actions.Spells
|
||||
{
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class GiveItemSpell : IInstantAction
|
||||
{ //TODO: Needs to be an EntityPrototype for proper validation
|
||||
[ViewVariables] [DataField("castMessage")] public string? CastMessage { get; set; } = default!;
|
||||
[ViewVariables] [DataField("cooldown")] public float CoolDown { get; set; } = 1f;
|
||||
[ViewVariables] [DataField("spellItem", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] public string ItemProto { get; set; } = default!;
|
||||
|
||||
[ViewVariables] [DataField("castSound", required: true)] public SoundSpecifier CastSound { get; set; } = default!;
|
||||
|
||||
//Rubber-band snapping items into player's hands, originally was a workaround, later found it works quite well with stuns
|
||||
//Not sure if needs fixing
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var caster = args.Performer;
|
||||
|
||||
if (!entMan.TryGetComponent(caster, out HandsComponent? handsComponent))
|
||||
{
|
||||
caster.PopupMessage(Loc.GetString("spell-fail-no-hands"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(caster, null)) return;
|
||||
|
||||
// TODO: Nix when we get EntityPrototype serializers
|
||||
if (!IoCManager.Resolve<IPrototypeManager>().HasIndex<EntityPrototype>(ItemProto))
|
||||
{
|
||||
Logger.Error($"Invalid prototype {ItemProto} supplied for {nameof(GiveItemSpell)}");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Look this is shitty and ideally a test would do it
|
||||
var spawnedProto = entMan.SpawnEntity(ItemProto, entMan.GetComponent<TransformComponent>(caster).MapPosition);
|
||||
|
||||
if (!entMan.TryGetComponent(spawnedProto, out SharedItemComponent? itemComponent))
|
||||
{
|
||||
Logger.Error($"Tried to use {nameof(GiveItemSpell)} but prototype has no {nameof(SharedItemComponent)}?");
|
||||
entMan.DeleteEntity(spawnedProto);
|
||||
return;
|
||||
}
|
||||
|
||||
args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(CoolDown));
|
||||
|
||||
if (CastMessage != null)
|
||||
caster.PopupMessageEveryone(CastMessage);
|
||||
|
||||
handsComponent.PutInHandOrDrop(itemComponent);
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(caster), CastSound.GetSound(), caster);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,21 @@
|
||||
using System;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Sound;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Atmos.Components
|
||||
{
|
||||
@@ -90,6 +81,9 @@ namespace Content.Server.Atmos.Components
|
||||
[DataField("tankFragmentScale")]
|
||||
public float TankFragmentScale { get; set; } = 10 * Atmospherics.OneAtmosphere;
|
||||
|
||||
[DataField("toggleAction", required: true)]
|
||||
public InstantAction ToggleAction = new();
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -165,6 +159,7 @@ namespace Content.Server.Atmos.Components
|
||||
var internals = GetInternalsComponent();
|
||||
if (internals == null) return;
|
||||
IsConnected = internals.TryConnectTank(Owner);
|
||||
EntitySystem.Get<SharedActionsSystem>().SetToggled(ToggleAction, IsConnected);
|
||||
UpdateUserInterface();
|
||||
}
|
||||
|
||||
@@ -172,6 +167,7 @@ namespace Content.Server.Atmos.Components
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
IsConnected = false;
|
||||
EntitySystem.Get<SharedActionsSystem>().SetToggled(ToggleAction, false);
|
||||
GetInternalsComponent(owner)?.DisconnectTank();
|
||||
UpdateUserInterface();
|
||||
}
|
||||
@@ -187,9 +183,6 @@ namespace Content.Server.Atmos.Components
|
||||
InternalsConnected = IsConnected,
|
||||
CanConnectInternals = IsFunctional && internals != null
|
||||
});
|
||||
|
||||
if (internals == null || !_entityManager.TryGetComponent<ItemActionsComponent>(Owner, out var itemActions)) return;
|
||||
itemActions.GrantOrUpdate(ItemActionType.ToggleInternals, IsFunctional, IsConnected);
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
||||
@@ -309,22 +302,4 @@ namespace Content.Server.Atmos.Components
|
||||
DisconnectFromInternals(eventArgs.User);
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class ToggleInternalsAction : IToggleItemAction
|
||||
{
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(args.Performer, args.Item))
|
||||
return false;
|
||||
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<GasTankComponent?>(args.Item, out var gasTankComponent)) return false;
|
||||
// no change
|
||||
if (gasTankComponent.IsConnected == args.ToggledOn) return false;
|
||||
gasTankComponent.ToggleInternals();
|
||||
// did we successfully toggle to the desired status?
|
||||
return gasTankComponent.IsConnected == args.ToggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
@@ -20,6 +19,23 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUIVerb);
|
||||
SubscribeLocalEvent<GasTankComponent, GetActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<GasTankComponent, ToggleActionEvent>(OnActionToggle);
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, GasTankComponent component, GetActionsEvent args)
|
||||
{
|
||||
args.Actions.Add(component.ToggleAction);
|
||||
}
|
||||
|
||||
private void OnActionToggle(EntityUid uid, GasTankComponent component, ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
component.ToggleInternals();
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void AddOpenUIVerb(EntityUid uid, GasTankComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects;
|
||||
|
||||
/// <summary>
|
||||
/// Forces someone to do a certain action, if they have it.
|
||||
/// </summary>
|
||||
public sealed class DoAction : ReagentEffect
|
||||
{
|
||||
[DataField("action", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<ActionPrototype>))]
|
||||
public string Action = default!;
|
||||
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out SharedActionsComponent? actions))
|
||||
{
|
||||
if (!IoCManager.Resolve<IPrototypeManager>().TryIndex<ActionPrototype>(Action, out var proto))
|
||||
return;
|
||||
|
||||
if (actions.IsGranted(proto.ActionType))
|
||||
{
|
||||
var attempt = new ActionAttempt(proto);
|
||||
attempt.DoInstantAction(args.SolutionEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Content.Server/Chemistry/ReagentEffects/Scream.cs
Normal file
15
Content.Server/Chemistry/ReagentEffects/Scream.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Content.Server.Speech;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects;
|
||||
|
||||
/// <summary>
|
||||
/// Forces someone to scream their lungs out.
|
||||
/// </summary>
|
||||
public sealed class Scream : ReagentEffect
|
||||
{
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<VocalSystem>().TryScream(args.SolutionEntity);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
@@ -38,8 +36,6 @@ namespace Content.Server.Clothing.Components
|
||||
EntitySystem.Get<MagbootsSystem>().UpdateMagbootEffects(container.Owner, Owner, true, this);
|
||||
}
|
||||
|
||||
if(_entMan.TryGetComponent<ItemActionsComponent>(Owner, out var itemActions))
|
||||
itemActions.Toggle(ItemActionType.ToggleMagboots, On);
|
||||
if (_entMan.TryGetComponent<SharedItemComponent>(Owner, out var item))
|
||||
item.EquippedPrefix = On ? "on" : null;
|
||||
if(_entMan.TryGetComponent<SpriteComponent>(Owner, out var sprite))
|
||||
@@ -49,14 +45,9 @@ namespace Content.Server.Clothing.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void Toggle(EntityUid user)
|
||||
{
|
||||
On = !On;
|
||||
}
|
||||
|
||||
void IActivate.Activate(ActivateEventArgs eventArgs)
|
||||
{
|
||||
Toggle(eventArgs.User);
|
||||
On = !On;
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
@@ -64,18 +55,4 @@ namespace Content.Server.Clothing.Components
|
||||
return new MagbootsComponentState(On);
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class ToggleMagbootsAction : IToggleItemAction
|
||||
{
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<MagbootsComponent?>(args.Item, out var magboots))
|
||||
return false;
|
||||
|
||||
magboots.Toggle(args.Performer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
using Content.Server.Alert;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Clothing.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Movement.EntitySystems;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Server.Clothing
|
||||
{
|
||||
public sealed class MagbootsSystem : EntitySystem
|
||||
public sealed class MagbootsSystem : SharedMagbootsSystem
|
||||
{
|
||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||
|
||||
|
||||
@@ -1,10 +1,82 @@
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Server.Act;
|
||||
using Content.Server.Actions.Events;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Weapon.Melee;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Database;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.CombatMode
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class CombatModeSystem : SharedCombatModeSystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
[Dependency] private readonly MeleeWeaponSystem _meleeWeaponSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly AdminLogSystem _logSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharedCombatModeComponent, DisarmActionEvent>(OnEntityActionPerform);
|
||||
}
|
||||
|
||||
private void OnEntityActionPerform(EntityUid uid, SharedCombatModeComponent component, DisarmActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!_actionBlockerSystem.CanAttack(args.Performer))
|
||||
return;
|
||||
|
||||
var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer);
|
||||
RaiseLocalEvent(args.Target, attemptEvent);
|
||||
if (attemptEvent.Cancelled)
|
||||
return;
|
||||
|
||||
var diff = Transform(args.Target).MapPosition.Position - Transform(args.Performer).MapPosition.Position;
|
||||
var angle = Angle.FromWorldVec(diff);
|
||||
|
||||
var filterAll = Filter.Pvs(args.Performer);
|
||||
var filterOther = filterAll.RemoveWhereAttachedEntity(e => e == args.Performer);
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (_random.Prob(component.DisarmFailChance))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(args.Performer), component.DisarmFailSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
|
||||
|
||||
var targetName = Name(args.Target);
|
||||
|
||||
var msgOther = Loc.GetString(
|
||||
"disarm-action-popup-message-other-clients",
|
||||
("performerName", Name(args.Performer)),
|
||||
("targetName", targetName));
|
||||
|
||||
var msgUser = Loc.GetString("disarm-action-popup-message-cursor", ("targetName", targetName ));
|
||||
|
||||
_popupSystem.PopupEntity(msgOther, args.Performer, filterOther);
|
||||
_popupSystem.PopupEntity(msgUser, args.Performer, Filter.Entities(args.Performer));
|
||||
|
||||
_meleeWeaponSystem.SendLunge(angle, args.Performer);
|
||||
return;
|
||||
}
|
||||
|
||||
_meleeWeaponSystem.SendAnimation("disarm", angle, args.Performer, args.Performer, new[] { args.Target });
|
||||
SoundSystem.Play(filterAll, component.DisarmSuccessSound.GetSound(), args.Performer, AudioHelpers.WithVariation(0.025f));
|
||||
_logSystem.Add(LogType.DisarmedAction, $"{ToPrettyString(args.Performer):user} used disarm on {ToPrettyString(args.Target):target}");
|
||||
|
||||
var eventArgs = new DisarmedEvent() { Target = args.Target, Source = args.Performer, PushProbability = component.DisarmPushChance };
|
||||
RaiseLocalEvent(args.Target, eventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@ namespace Content.Server.Entry
|
||||
else
|
||||
{
|
||||
IoCManager.Resolve<RecipeManager>().Initialize();
|
||||
IoCManager.Resolve<ActionManager>().Initialize();
|
||||
IoCManager.Resolve<BlackboardManager>().Initialize();
|
||||
IoCManager.Resolve<ConsiderationsManager>().Initialize();
|
||||
IoCManager.Resolve<IAdminManager>().Initialize();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Ghost.Components
|
||||
{
|
||||
@@ -9,5 +10,23 @@ namespace Content.Server.Ghost.Components
|
||||
public sealed class GhostComponent : SharedGhostComponent
|
||||
{
|
||||
public TimeSpan TimeOfDeath { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[DataField("booRadius")]
|
||||
public float BooRadius = 3;
|
||||
|
||||
[DataField("booMaxTargets")]
|
||||
public int BooMaxTargets = 3;
|
||||
|
||||
[DataField("action")]
|
||||
public InstantAction Action = new()
|
||||
{
|
||||
UseDelay = TimeSpan.FromSeconds(120),
|
||||
Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/scream.png")),
|
||||
Name = "action-name-boo",
|
||||
Description = "action-description-boo",
|
||||
Event = new BooActionEvent(),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class BooActionEvent : PerformActionEvent { }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Server.Mind.Components;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Visible;
|
||||
using Content.Server.Warps;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Follower;
|
||||
using Content.Shared.Ghost;
|
||||
@@ -15,10 +16,6 @@ using Content.Shared.Movement.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Ghost
|
||||
@@ -30,7 +27,9 @@ namespace Content.Server.Ghost
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly GameTicker _ticker = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly VisibilitySystem _visibilitySystem = default!;
|
||||
[Dependency] private readonly IEntityLookup _lookup = default!;
|
||||
[Dependency] private readonly FollowerSystem _followerSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -51,6 +50,30 @@ namespace Content.Server.Ghost
|
||||
SubscribeNetworkEvent<GhostReturnToBodyRequest>(OnGhostReturnToBodyRequest);
|
||||
SubscribeNetworkEvent<GhostWarpToLocationRequestEvent>(OnGhostWarpToLocationRequest);
|
||||
SubscribeNetworkEvent<GhostWarpToTargetRequestEvent>(OnGhostWarpToTargetRequest);
|
||||
|
||||
SubscribeLocalEvent<GhostComponent, BooActionEvent>(OnActionPerform);
|
||||
}
|
||||
private void OnActionPerform(EntityUid uid, GhostComponent component, BooActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var ents = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius);
|
||||
|
||||
var booCounter = 0;
|
||||
foreach (var ent in ents)
|
||||
{
|
||||
var ghostBoo = new GhostBooEvent();
|
||||
RaiseLocalEvent(ent, ghostBoo);
|
||||
|
||||
if (ghostBoo.Handled)
|
||||
booCounter++;
|
||||
|
||||
if (booCounter >= component.BooMaxTargets)
|
||||
break;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnRelayMoveInput(EntityUid uid, GhostOnMoveComponent component, RelayMoveInputEvent args)
|
||||
@@ -78,6 +101,8 @@ namespace Content.Server.Ghost
|
||||
}
|
||||
|
||||
component.TimeOfDeath = _gameTiming.RealTime;
|
||||
|
||||
_actions.AddAction(uid, component.Action, null);
|
||||
}
|
||||
|
||||
private void OnGhostShutdown(EntityUid uid, GhostComponent component, ComponentShutdown args)
|
||||
@@ -98,6 +123,8 @@ namespace Content.Server.Ghost
|
||||
{
|
||||
eye.VisibilityMask &= ~(uint) VisibilityFlags.Ghost;
|
||||
}
|
||||
|
||||
_actions.RemoveAction(uid, component.Action);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Guardian
|
||||
{
|
||||
@@ -22,5 +23,17 @@ namespace Content.Server.Guardian
|
||||
/// Container which holds the guardian
|
||||
/// </summary>
|
||||
[ViewVariables] public ContainerSlot GuardianContainer = default!;
|
||||
|
||||
[DataField("action")]
|
||||
public InstantAction Action = new()
|
||||
{
|
||||
Name = "action-name-guardian",
|
||||
Description = "action-description-guardian",
|
||||
Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/manifest.png")),
|
||||
UseDelay = TimeSpan.FromSeconds(2),
|
||||
Event = new GuardianToggleActionEvent(),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class GuardianToggleActionEvent : PerformActionEvent { };
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Examine;
|
||||
@@ -13,10 +11,6 @@ using Content.Shared.MobState;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -30,6 +24,7 @@ namespace Content.Server.Guardian
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageSystem = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actionSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -50,9 +45,22 @@ namespace Content.Server.Guardian
|
||||
SubscribeLocalEvent<GuardianHostComponent, MobStateChangedEvent>(OnHostStateChange);
|
||||
SubscribeLocalEvent<GuardianHostComponent, ComponentShutdown>(OnHostShutdown);
|
||||
|
||||
SubscribeLocalEvent<GuardianHostComponent, GuardianToggleActionEvent>(OnPerformAction);
|
||||
|
||||
SubscribeLocalEvent<GuardianComponent, AttackAttemptEvent>(OnGuardianAttackAttempt);
|
||||
}
|
||||
|
||||
private void OnPerformAction(EntityUid uid, GuardianHostComponent component, GuardianToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (component.HostedGuardian != null)
|
||||
ToggleGuardian(component);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnGuardianUnplayer(EntityUid uid, GuardianComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
var host = component.Host;
|
||||
@@ -74,12 +82,14 @@ namespace Content.Server.Guardian
|
||||
private void OnHostInit(EntityUid uid, GuardianHostComponent component, ComponentInit args)
|
||||
{
|
||||
component.GuardianContainer = uid.EnsureContainer<ContainerSlot>("GuardianContainer");
|
||||
_actionSystem.AddAction(uid, component.Action, null);
|
||||
}
|
||||
|
||||
private void OnHostShutdown(EntityUid uid, GuardianHostComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.HostedGuardian == null) return;
|
||||
EntityManager.QueueDeleteEntity(component.HostedGuardian.Value);
|
||||
_actionSystem.RemoveAction(uid, component.Action);
|
||||
}
|
||||
|
||||
private void OnGuardianAttackAttempt(EntityUid uid, GuardianComponent component, AttackAttemptEvent args)
|
||||
@@ -151,9 +161,6 @@ namespace Content.Server.Guardian
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't work without actions
|
||||
EntityManager.EnsureComponent<ServerActionsComponent>(target);
|
||||
|
||||
if (component.Injecting) return;
|
||||
|
||||
component.Injecting = true;
|
||||
@@ -175,8 +182,7 @@ namespace Content.Server.Guardian
|
||||
comp.Used ||
|
||||
!TryComp<HandsComponent>(ev.User, out var hands) ||
|
||||
!hands.IsHolding(comp.Owner) ||
|
||||
HasComp<GuardianHostComponent>(ev.Target) ||
|
||||
!TryComp<SharedActionsComponent>(ev.Target, out var actions))
|
||||
HasComp<GuardianHostComponent>(ev.Target))
|
||||
{
|
||||
comp.Injecting = false;
|
||||
return;
|
||||
@@ -194,8 +200,6 @@ namespace Content.Server.Guardian
|
||||
{
|
||||
guardianComponent.Host = ev.Target;
|
||||
|
||||
// Grant the user the recall action and notify them
|
||||
actions.Grant(ActionType.ManifestGuardian);
|
||||
SoundSystem.Play(Filter.Entities(ev.Target), "/Audio/Effects/guardian_inject.ogg", ev.Target);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("guardian-created"), ev.Target, Filter.Entities(ev.Target));
|
||||
|
||||
@@ -13,27 +13,19 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Pulling.Components;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Hands.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHandsComponent))]
|
||||
#pragma warning disable 618
|
||||
public sealed class HandsComponent : SharedHandsComponent, IBodyPartAdded, IBodyPartRemoved, IDisarmedAct
|
||||
public sealed class HandsComponent : SharedHandsComponent, IBodyPartAdded, IBodyPartRemoved
|
||||
#pragma warning restore 618
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
[DataField("disarmedSound")] SoundSpecifier _disarmedSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
|
||||
|
||||
int IDisarmedAct.Priority => int.MaxValue; // We want this to be the last disarm act to run.
|
||||
|
||||
#region Pull/Disarm
|
||||
|
||||
void IBodyPartAdded.BodyPartAdded(BodyPartAddedEventArgs args)
|
||||
@@ -62,33 +54,10 @@ namespace Content.Server.Hands.Components
|
||||
RemoveHand(args.Slot);
|
||||
}
|
||||
|
||||
bool IDisarmedAct.Disarmed(DisarmedActEvent @event)
|
||||
{
|
||||
if (BreakPulls())
|
||||
return false;
|
||||
|
||||
var source = @event.Source;
|
||||
var target = @event.Target;
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(source), _disarmedSound.GetSound(), source, AudioHelpers.WithVariation(0.025f));
|
||||
|
||||
if (ActiveHand != null && Drop(ActiveHand, false))
|
||||
{
|
||||
source.PopupMessageOtherClients(Loc.GetString("hands-component-disarm-success-others-message", ("disarmer", _entities.GetComponent<MetaDataComponent>(source).EntityName), ("disarmed", _entities.GetComponent<MetaDataComponent>(target).EntityName)));
|
||||
source.PopupMessageCursor(Loc.GetString("hands-component-disarm-success-message", ("disarmed", _entities.GetComponent<MetaDataComponent>(target).EntityName)));
|
||||
}
|
||||
else
|
||||
{
|
||||
source.PopupMessageOtherClients(Loc.GetString("hands-component-shove-success-others-message", ("shover", _entities.GetComponent<MetaDataComponent>(source).EntityName), ("shoved", _entities.GetComponent<MetaDataComponent>(target).EntityName)));
|
||||
source.PopupMessageCursor(Loc.GetString("hands-component-shove-success-message", ("shoved", _entities.GetComponent<MetaDataComponent>(target).EntityName)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool BreakPulls()
|
||||
public bool BreakPulls()
|
||||
{
|
||||
// What is this API??
|
||||
// I just wanted to do actions not deal with this shit...
|
||||
if (!_entities.TryGetComponent(Owner, out SharedPullerComponent? puller)
|
||||
|| puller.Pulling is not {Valid: true} pulling || !_entities.TryGetComponent(puller.Pulling.Value, out SharedPullableComponent? pullable))
|
||||
return false;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Act;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stack;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.Strip;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Server.Throwing;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Database;
|
||||
@@ -18,6 +21,7 @@ using Content.Shared.Popups;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input.Binding;
|
||||
@@ -38,6 +42,7 @@ namespace Content.Server.Hands.Systems
|
||||
[Dependency] private readonly AdminLogSystem _logSystem = default!;
|
||||
[Dependency] private readonly StrippableSystem _strippableSystem = default!;
|
||||
[Dependency] private readonly SharedHandVirtualItemSystem _virtualSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -48,6 +53,7 @@ namespace Content.Server.Hands.Systems
|
||||
SubscribeNetworkEvent<ClientInteractUsingInHandMsg>(HandleInteractUsingInHand);
|
||||
SubscribeNetworkEvent<UseInHandMsg>(HandleUseInHand);
|
||||
SubscribeNetworkEvent<MoveItemFromHandMsg>(HandleMoveItemFromHand);
|
||||
SubscribeLocalEvent<HandsComponent, DisarmedEvent>(OnDisarmed, before: new[] { typeof(StunSystem) });
|
||||
|
||||
SubscribeLocalEvent<HandsComponent, PullAttemptMessage>(HandlePullAttempt);
|
||||
SubscribeLocalEvent<HandsComponent, PullStartedMessage>(HandlePullStarted);
|
||||
@@ -76,6 +82,26 @@ namespace Content.Server.Hands.Systems
|
||||
args.State = new HandsComponentState(hands.Hands, hands.ActiveHand);
|
||||
}
|
||||
|
||||
private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent args)
|
||||
{
|
||||
if (args.Handled || component.BreakPulls())
|
||||
return;
|
||||
|
||||
if (component.ActiveHand == null || !component.Drop(component.ActiveHand, false))
|
||||
return;
|
||||
|
||||
var targetName = Name(args.Target);
|
||||
|
||||
var msgOther = Loc.GetString("hands-component-disarm-success-others-message", ("disarmer", Name(args.Source)), ("disarmed", targetName));
|
||||
var msgUser = Loc.GetString("hands-component-disarm-success-message", ("disarmed", targetName));
|
||||
|
||||
var filter = Filter.Pvs(args.Source).RemoveWhereAttachedEntity(e => e == args.Source);
|
||||
_popupSystem.PopupEntity(msgOther, args.Source, filter);
|
||||
_popupSystem.PopupEntity(msgUser, args.Source, Filter.Entities(args.Source));
|
||||
|
||||
args.Handled = true; // no shove/stun.
|
||||
}
|
||||
|
||||
#region EntityInsertRemove
|
||||
public override void RemoveHeldEntityFromHand(EntityUid uid, Hand hand, SharedHandsComponent? hands = null)
|
||||
{
|
||||
|
||||
@@ -35,7 +35,6 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
|
||||
IoCManager.Register<IServerDbManager, ServerDbManager>();
|
||||
IoCManager.Register<RecipeManager, RecipeManager>();
|
||||
IoCManager.Register<ActionManager, ActionManager>();
|
||||
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
|
||||
IoCManager.Register<BlackboardManager, BlackboardManager>();
|
||||
IoCManager.Register<ConsiderationsManager, ConsiderationsManager>();
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Threading.Tasks;
|
||||
using Content.Server.Clothing.Components;
|
||||
using Content.Server.Light.EntitySystems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Light.Component;
|
||||
@@ -48,17 +47,4 @@ namespace Content.Server.Light.Components
|
||||
/// </summary>
|
||||
public byte? LastLevel;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[DataDefinition]
|
||||
public sealed class ToggleLightAction : IToggleItemAction
|
||||
{
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(args.Performer, args.Item)) return false;
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<HandheldLightComponent?>(args.Item, out var lightComponent)) return false;
|
||||
if (lightComponent.Activated == args.ToggledOn) return false;
|
||||
return EntitySystem.Get<HandheldLightSystem>().ToggleStatus(args.Performer, lightComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Light.Components
|
||||
{
|
||||
@@ -16,5 +14,8 @@ namespace Content.Server.Light.Components
|
||||
public SoundSpecifier ToggleSound = new SoundPathSpecifier("/Audio/Items/flashlight_pda.ogg");
|
||||
|
||||
[ViewVariables] public bool LightOn = false;
|
||||
|
||||
[DataField("toggleAction", required: true)]
|
||||
public InstantAction ToggleAction = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.Clothing.Components;
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Light.Component;
|
||||
using Content.Shared.Rounding;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Verbs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -30,6 +24,7 @@ namespace Content.Server.Light.EntitySystems
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly ActionsSystem _actionSystem = default!;
|
||||
|
||||
// TODO: Ideally you'd be able to subscribe to power stuff to get events at certain percentages.. or something?
|
||||
// But for now this will be better anyway.
|
||||
@@ -47,6 +42,27 @@ namespace Content.Server.Light.EntitySystems
|
||||
SubscribeLocalEvent<HandheldLightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerb);
|
||||
|
||||
SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(OnActivate);
|
||||
|
||||
SubscribeLocalEvent<HandheldLightComponent, GetActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<HandheldLightComponent, ToggleActionEvent>(OnToggleAction);
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, HandheldLightComponent component, GetActionsEvent args)
|
||||
{
|
||||
args.Actions.Add(component.ToggleAction);
|
||||
}
|
||||
|
||||
private void OnToggleAction(EntityUid uid, HandheldLightComponent component, ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (component.Activated)
|
||||
TurnOff(component);
|
||||
else
|
||||
TurnOn(args.Performer, component);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, HandheldLightComponent component, ref ComponentGetState args)
|
||||
@@ -155,7 +171,6 @@ namespace Content.Server.Light.EntitySystems
|
||||
|
||||
SetState(component, false);
|
||||
component.Activated = false;
|
||||
UpdateLightAction(component);
|
||||
_activeLights.Remove(component);
|
||||
component.LastLevel = null;
|
||||
component.Dirty(EntityManager);
|
||||
@@ -174,7 +189,6 @@ namespace Content.Server.Light.EntitySystems
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
|
||||
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), component.Owner, Filter.Entities(user));
|
||||
UpdateLightAction(component);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -185,12 +199,10 @@ namespace Content.Server.Light.EntitySystems
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(component.Owner), component.TurnOnFailSound.GetSound(), component.Owner);
|
||||
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), component.Owner, Filter.Entities(user));
|
||||
UpdateLightAction(component);
|
||||
return false;
|
||||
}
|
||||
|
||||
component.Activated = true;
|
||||
UpdateLightAction(component);
|
||||
SetState(component, true);
|
||||
_activeLights.Add(component);
|
||||
component.LastLevel = GetLevel(component);
|
||||
@@ -217,13 +229,8 @@ namespace Content.Server.Light.EntitySystems
|
||||
{
|
||||
item.EquippedPrefix = on ? "on" : "off";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLightAction(HandheldLightComponent component)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(component.Owner, out ItemActionsComponent? actions)) return;
|
||||
|
||||
actions.Toggle(ItemActionType.ToggleLight, component.Activated);
|
||||
_actionSystem.SetToggled(component.ToggleAction, on);
|
||||
}
|
||||
|
||||
public void TryUpdate(HandheldLightComponent component, float frameTime)
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
using Content.Server.Light.Components;
|
||||
using Content.Server.Light.Events;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Toggleable;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using System;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems
|
||||
{
|
||||
public sealed class UnpoweredFlashlightSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetVerbsEvent<ActivationVerb>>(AddToggleLightVerbs);
|
||||
SubscribeLocalEvent<UnpoweredFlashlightComponent, GetActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<UnpoweredFlashlightComponent, ToggleActionEvent>(OnToggleAction);
|
||||
}
|
||||
|
||||
private void OnToggleAction(EntityUid uid, UnpoweredFlashlightComponent component, ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
ToggleLight(component);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, UnpoweredFlashlightComponent component, GetActionsEvent args)
|
||||
{
|
||||
args.Actions.Add(component.ToggleAction);
|
||||
}
|
||||
|
||||
private void AddToggleLightVerbs(EntityUid uid, UnpoweredFlashlightComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
@@ -49,7 +66,7 @@ namespace Content.Server.Light.EntitySystems
|
||||
SoundSystem.Play(Filter.Pvs(light.Owner), flashlight.ToggleSound.GetSound(), flashlight.Owner);
|
||||
|
||||
RaiseLocalEvent(flashlight.Owner, new LightToggleEvent(flashlight.LightOn));
|
||||
_actionsSystem.SetToggled(flashlight.ToggleAction, flashlight.LightOn);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Robust.Shared.Log;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Actions;
|
||||
|
||||
namespace Content.Server.PAI
|
||||
{
|
||||
@@ -20,6 +21,7 @@ namespace Content.Server.PAI
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly InstrumentSystem _instrumentSystem = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -30,6 +32,21 @@ namespace Content.Server.PAI
|
||||
SubscribeLocalEvent<PAIComponent, MindAddedMessage>(OnMindAdded);
|
||||
SubscribeLocalEvent<PAIComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
SubscribeLocalEvent<PAIComponent, GetVerbsEvent<ActivationVerb>>(AddWipeVerb);
|
||||
|
||||
SubscribeLocalEvent<PAIComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<PAIComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, PAIComponent component, ComponentStartup args)
|
||||
{
|
||||
if (component.MidiAction != null)
|
||||
_actionsSystem.AddAction(uid, component.MidiAction, null);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, PAIComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.MidiAction != null)
|
||||
_actionsSystem.RemoveAction(uid, component.MidiAction);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, PAIComponent component, ExaminedEvent args)
|
||||
|
||||
42
Content.Server/Speech/Components/VocalComponent.cs
Normal file
42
Content.Server/Speech/Components/VocalComponent.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
namespace Content.Server.Speech.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Component required for entities to be able to scream.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class VocalComponent : Component
|
||||
{
|
||||
[DataField("maleScream")]
|
||||
public SoundSpecifier MaleScream = new SoundCollectionSpecifier("MaleScreams");
|
||||
|
||||
[DataField("femaleScream")]
|
||||
public SoundSpecifier FemaleScream = new SoundCollectionSpecifier("FemaleScreams");
|
||||
|
||||
[DataField("wilhelm")]
|
||||
public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
|
||||
|
||||
[DataField("audioParams")]
|
||||
public AudioParams AudioParams = AudioParams.Default.WithVolume(4f);
|
||||
|
||||
public const float Variation = 0.125f;
|
||||
|
||||
// Not using the in-build sound support for actions, given that the sound is modified non-prototype specific factors like gender.
|
||||
[DataField("action", required: true)]
|
||||
public InstantAction Action = new()
|
||||
{
|
||||
UseDelay = TimeSpan.FromSeconds(10),
|
||||
Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/scream.png")),
|
||||
Name = "action-name-scream",
|
||||
Description = "AAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
Event = new ScreamActionEvent(),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class ScreamActionEvent : PerformActionEvent { };
|
||||
86
Content.Server/Speech/VocalSystem.cs
Normal file
86
Content.Server/Speech/VocalSystem.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.CharacterAppearance;
|
||||
using Content.Shared.CharacterAppearance.Components;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech;
|
||||
|
||||
/// <summary>
|
||||
/// Fer Screamin
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Or I guess other vocalizations, like laughing. If fun is ever legalized on the station.
|
||||
/// </remarks>
|
||||
public sealed class VocalSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VocalComponent, ScreamActionEvent>(OnActionPerform);
|
||||
SubscribeLocalEvent<VocalComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<VocalComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, VocalComponent component, ComponentStartup args)
|
||||
{
|
||||
_actions.AddAction(uid, component.Action, null);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
|
||||
{
|
||||
_actions.RemoveAction(uid, component.Action);
|
||||
}
|
||||
|
||||
private void OnActionPerform(EntityUid uid, VocalComponent component, ScreamActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = TryScream(uid, component);
|
||||
}
|
||||
|
||||
public bool TryScream(EntityUid uid, VocalComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return false;
|
||||
|
||||
if (!_blocker.CanSpeak(uid))
|
||||
return false;
|
||||
|
||||
// Currently this requires humanoid appearance & doesn't have any sort of fall-back or gender-neutral scream.
|
||||
if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid))
|
||||
return false;
|
||||
|
||||
if (_random.Prob(.01f))
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(uid), component.Wilhelm.GetSound(), uid, component.AudioParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
var scale = (float) _random.NextGaussian(1, VocalComponent.Variation);
|
||||
var pitchedParams = component.AudioParams.WithPitchScale(scale);
|
||||
|
||||
switch (humanoid.Sex)
|
||||
{
|
||||
case Sex.Male:
|
||||
SoundSystem.Play(Filter.Pvs(uid), component.MaleScream.GetSound(), uid, pitchedParams);
|
||||
break;
|
||||
case Sex.Female:
|
||||
SoundSystem.Play(Filter.Pvs(uid), component.FemaleScream.GetSound(), uid, pitchedParams);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ namespace Content.Server.Stunnable
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StatusEffectsComponent, DisarmedActEvent>(OnDisarmed);
|
||||
SubscribeLocalEvent<StatusEffectsComponent, DisarmedEvent>(OnDisarmed);
|
||||
}
|
||||
|
||||
private void OnDisarmed(EntityUid uid, StatusEffectsComponent status, DisarmedActEvent args)
|
||||
private void OnDisarmed(EntityUid uid, StatusEffectsComponent status, DisarmedEvent args)
|
||||
{
|
||||
if (args.Handled || !_random.Prob(args.PushProbability))
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -21,12 +23,29 @@ namespace Content.Server.UserInterface
|
||||
SubscribeLocalEvent<ActivatableUIComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<ActivatableUIComponent, UseInHandEvent>(OnUseInHand);
|
||||
SubscribeLocalEvent<ActivatableUIComponent, HandDeselectedEvent>((uid, aui, _) => CloseAll(uid, aui));
|
||||
SubscribeLocalEvent<ActivatableUIComponent, UnequippedHandEvent>((uid, aui, _) => CloseAll(uid, aui));
|
||||
SubscribeLocalEvent<ActivatableUIComponent, GotUnequippedHandEvent>((uid, aui, _) => CloseAll(uid, aui));
|
||||
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
|
||||
SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
|
||||
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
|
||||
|
||||
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
|
||||
|
||||
SubscribeLocalEvent<ServerUserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
|
||||
}
|
||||
|
||||
private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args)
|
||||
{
|
||||
if (args.Handled || args.Key == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(args.Performer, out ActorComponent? actor))
|
||||
return;
|
||||
|
||||
if (!component.TryGetBoundUserInterface(args.Key, out var bui))
|
||||
return;
|
||||
|
||||
bui.Toggle(actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void AddOpenUiVerb(EntityUid uid, ActivatableUIComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
|
||||
23
Content.Server/UserInterface/OpenUiActionEvent.cs
Normal file
23
Content.Server/UserInterface/OpenUiActionEvent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.UserInterface;
|
||||
|
||||
public sealed class OpenUiActionEvent : PerformActionEvent, ISerializationHooks
|
||||
{
|
||||
[ViewVariables]
|
||||
public Enum? Key { get; set; }
|
||||
|
||||
[DataField("key", readOnly: true, required: true)]
|
||||
private string _keyRaw = default!;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
var reflectionManager = IoCManager.Resolve<IReflectionManager>();
|
||||
if (reflectionManager.TryParseEnumReference(_keyRaw, out var key))
|
||||
Key = key;
|
||||
else
|
||||
Logger.Error($"Invalid UI key ({_keyRaw}) in open-UI action");
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ namespace Content.Server.Wieldable
|
||||
SubscribeLocalEvent<WieldableComponent, UseInHandEvent>(OnUseInHand);
|
||||
SubscribeLocalEvent<WieldableComponent, ItemWieldedEvent>(OnItemWielded);
|
||||
SubscribeLocalEvent<WieldableComponent, ItemUnwieldedEvent>(OnItemUnwielded);
|
||||
SubscribeLocalEvent<WieldableComponent, UnequippedHandEvent>(OnItemLeaveHand);
|
||||
SubscribeLocalEvent<WieldableComponent, GotUnequippedHandEvent>(OnItemLeaveHand);
|
||||
SubscribeLocalEvent<WieldableComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
|
||||
SubscribeLocalEvent<WieldableComponent, GetVerbsEvent<InteractionVerb>>(AddToggleWieldVerb);
|
||||
|
||||
@@ -208,7 +208,7 @@ namespace Content.Server.Wieldable
|
||||
_virtualItemSystem.DeleteInHandsMatching(args.User.Value, uid);
|
||||
}
|
||||
|
||||
private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, UnequippedHandEvent args)
|
||||
private void OnItemLeaveHand(EntityUid uid, WieldableComponent component, GotUnequippedHandEvent args)
|
||||
{
|
||||
if (!component.Wielded || component.Owner != args.Unequipped)
|
||||
return;
|
||||
|
||||
@@ -63,5 +63,6 @@ public enum LogType
|
||||
Emitter = 59,
|
||||
GhostRoleTaken = 60,
|
||||
Chat = 61,
|
||||
Action = 62,
|
||||
Emag = 69,
|
||||
}
|
||||
|
||||
61
Content.Shared/Actions/ActionEvents.cs
Normal file
61
Content.Shared/Actions/ActionEvents.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
public sealed class GetActionsEvent : EntityEventArgs
|
||||
{
|
||||
public SortedSet<ActionType> Actions = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event used to communicate with the client that the user wishes to perform some action.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Basically a wrapper for <see cref="PerformActionEvent"/> that the action system will validate before performing
|
||||
/// (check cooldown, target, enabling-entity)
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RequestPerformActionEvent : EntityEventArgs
|
||||
{
|
||||
public readonly ActionType Action;
|
||||
public readonly EntityUid? EntityTarget;
|
||||
public readonly MapCoordinates? MapTarget;
|
||||
|
||||
public RequestPerformActionEvent(InstantAction action)
|
||||
{
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public RequestPerformActionEvent(EntityTargetAction action, EntityUid entityTarget)
|
||||
{
|
||||
Action = action;
|
||||
EntityTarget = entityTarget;
|
||||
}
|
||||
|
||||
public RequestPerformActionEvent(WorldTargetAction action, MapCoordinates mapTarget)
|
||||
{
|
||||
Action = action;
|
||||
MapTarget = mapTarget;
|
||||
}
|
||||
}
|
||||
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class PerformActionEvent : HandledEntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The user performing the action
|
||||
/// </summary>
|
||||
public EntityUid Performer;
|
||||
}
|
||||
|
||||
public abstract class PerformEntityTargetActionEvent : PerformActionEvent
|
||||
{
|
||||
public EntityUid Target;
|
||||
}
|
||||
|
||||
public abstract class PerformWorldTargetActionEvent : PerformActionEvent
|
||||
{
|
||||
public MapCoordinates Target;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to all configured actions by action type.
|
||||
/// </summary>
|
||||
public sealed class ActionManager
|
||||
{
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private readonly Dictionary<ActionType, ActionPrototype> _typeToAction = new();
|
||||
private readonly Dictionary<ItemActionType, ItemActionPrototype> _typeToItemAction = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ActionPrototype>())
|
||||
{
|
||||
if (!_typeToAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found action with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ItemActionPrototype>())
|
||||
{
|
||||
if (!_typeToItemAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found itemAction with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>all action prototypes of all types</returns>
|
||||
public IEnumerable<BaseActionPrototype> EnumerateActions()
|
||||
{
|
||||
return _typeToAction.Values.Concat<BaseActionPrototype>(_typeToItemAction.Values);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ActionType actionType, [NotNullWhen(true)] out ActionPrototype? action)
|
||||
{
|
||||
return _typeToAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the item action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ItemActionType actionType, [NotNullWhen(true)] out ItemActionPrototype? action)
|
||||
{
|
||||
return _typeToItemAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Every possible action. Corresponds to actionType in action prototypes.
|
||||
/// </summary>
|
||||
public enum ActionType : byte
|
||||
{
|
||||
Error,
|
||||
HumanScream,
|
||||
VoxScream,
|
||||
CombatMode,
|
||||
Disarm,
|
||||
GhostBoo,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat,
|
||||
SpellPie,
|
||||
ManifestGuardian,
|
||||
PAIMidi
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every possible item action. Corresponds to actionType in itemAction prototypes.
|
||||
/// </summary>
|
||||
public enum ItemActionType : byte
|
||||
{
|
||||
Error,
|
||||
ToggleInternals,
|
||||
ToggleLight,
|
||||
ToggleMagboots,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat
|
||||
}
|
||||
}
|
||||
276
Content.Shared/Actions/ActionTypes/ActionType.cs
Normal file
276
Content.Shared/Actions/ActionTypes/ActionType.cs
Normal file
@@ -0,0 +1,276 @@
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
[DataDefinition]
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class ActionType : IEquatable<ActionType>, IComparable, ICloneable
|
||||
{
|
||||
/// <summary>
|
||||
/// Icon representing this action in the UI.
|
||||
/// </summary>
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier? Icon;
|
||||
|
||||
/// <summary>
|
||||
/// For toggle actions only, icon to show when toggled on. If omitted, the action will simply be highlighted
|
||||
/// when turned on.
|
||||
/// </summary>
|
||||
[DataField("iconOn")]
|
||||
public SpriteSpecifier? IconOn;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this color will modulate the action icon color.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This currently only exists for decal-placement actions, so that the action icons correspond to the color of
|
||||
/// the decal. But this is probably useful for other actions, including maybe changing color on toggle.
|
||||
/// </remarks>
|
||||
[DataField("iconColor")]
|
||||
public Color IconColor = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Name to show in UI.
|
||||
/// </summary>
|
||||
[DataField("name", required: true)]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Description to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
[DataField("description")]
|
||||
public string Description = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Keywords that can be used to search for this action in the action menu.
|
||||
/// </summary>
|
||||
[DataField("keywords")]
|
||||
public HashSet<string> Keywords = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this action is currently enabled. If not enabled, this action cannot be performed.
|
||||
/// </summary>
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// The toggle state of this action. Toggling switches the currently displayed icon, see <see cref="Icon"/> and <see cref="IconOn"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The toggle can set directly via <see cref="SharedActionsSystem.SetToggled()"/>, but it will also be
|
||||
/// automatically toggled for targeted-actions while selecting a target.
|
||||
/// </remarks>
|
||||
public bool Toggled;
|
||||
|
||||
/// <summary>
|
||||
/// The current cooldown on the action.
|
||||
/// </summary>
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown;
|
||||
|
||||
/// <summary>
|
||||
/// Time interval between action uses.
|
||||
/// </summary>
|
||||
[DataField("useDelay")]
|
||||
public TimeSpan? UseDelay;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience tool for actions with limited number of charges. Automatically decremented on use, and the
|
||||
/// action is disabled when it reaches zero. Does NOT automatically remove the action from the action bar.
|
||||
/// </summary>
|
||||
[DataField("charges")]
|
||||
public int? Charges;
|
||||
|
||||
/// <summary>
|
||||
/// The entity that enables / provides this action. If the action is innate, this may be the user themselves. If
|
||||
/// this action has no provider (e.g., mapping tools), the this will result in broadcast events.
|
||||
/// </summary>
|
||||
public EntityUid? Provider;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the action system should block this action if the user cannot currently interact. Some spells or
|
||||
/// abilities may want to disable this and implement their own checks.
|
||||
/// </summary>
|
||||
[DataField("checkCanInteract")]
|
||||
public bool CheckCanInteract = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, will simply execute the action locally without sending to the server.
|
||||
/// </summary>
|
||||
[DataField("clientExclusive")]
|
||||
public bool ClientExclusive = false;
|
||||
|
||||
/// <summary>
|
||||
/// Determines the order in which actions are automatically added the action bar.
|
||||
/// </summary>
|
||||
[DataField("priority")]
|
||||
public int Priority = 0;
|
||||
|
||||
/// <summary>
|
||||
/// What entity, if any, currently has this action in the actions component?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? AttachedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically add this action to the action bar when it becomes available.
|
||||
/// </summary>
|
||||
[DataField("autoPopulate")]
|
||||
public bool AutoPopulate = true;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to automatically remove this action to the action bar when it becomes unavailable.
|
||||
/// </summary>
|
||||
[DataField("autoRemove")]
|
||||
public bool AutoRemove = true;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary actions are removed from the action component when removed from the action-bar/GUI. Currently,
|
||||
/// should only be used for client-exclusive actions (server is not notified).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Currently there is no way for a player to just voluntarily remove actions. They can hide them from the
|
||||
/// toolbar, but not actually remove them. This is undesirable for things like dynamically added mapping
|
||||
/// entity-selection actions, as the # of actions would just keep increasing.
|
||||
/// </remarks>
|
||||
[DataField("temporary")]
|
||||
public bool Temporary;
|
||||
|
||||
/// <summary>
|
||||
/// Determines the appearance of the entity-icon for actions that are enabled via some entity.
|
||||
/// </summary>
|
||||
[DataField("itemIconStyle")]
|
||||
public ItemActionIconStyle ItemIconStyle;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, the user will speak these words when performing the action. Convenient feature to have for some
|
||||
/// actions. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("speech")]
|
||||
public string? Speech;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this sound will be played when performing this action.
|
||||
/// </summary>
|
||||
[DataField("sound")]
|
||||
public SoundSpecifier? Sound;
|
||||
|
||||
[DataField("audioParams")]
|
||||
public AudioParams? AudioParams;
|
||||
|
||||
/// <summary>
|
||||
/// A pop-up to show the user when performing this action. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("userPopup")]
|
||||
public string? UserPopup;
|
||||
|
||||
/// <summary>
|
||||
/// A pop-up to show to all players when performing this action. Gets passed through localization.
|
||||
/// </summary>
|
||||
[DataField("popup")]
|
||||
public string? Popup;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this string will be appended to the pop-up localization strings when the action was toggled on
|
||||
/// after execution. Exists to make it easy to have a different pop-up for turning the action on or off (e.g.,
|
||||
/// combat mode toggle).
|
||||
/// </summary>
|
||||
[DataField("popupToggleSuffix")]
|
||||
public string? PopupToggleSuffix = null;
|
||||
|
||||
/// <summary>
|
||||
/// Compares two actions based on their properties. This is used to determine equality when the client requests the
|
||||
/// server to perform some action. Also determines the order in which actions are automatically added to the action bar.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Basically: if an action has the same priority, name, and is enabled by the same entity, then the actions are considered equal.
|
||||
/// The entity-check is required to avoid toggling all flashlights simultaneously whenever a flashlight-hoarder uses an action.
|
||||
/// </remarks>
|
||||
public virtual int CompareTo(object? obj)
|
||||
{
|
||||
if (obj is not ActionType otherAction)
|
||||
return -1;
|
||||
|
||||
if (Priority != otherAction.Priority)
|
||||
return otherAction.Priority - Priority;
|
||||
|
||||
var name = FormattedMessage.RemoveMarkup(Loc.GetString(Name));
|
||||
var otherName = FormattedMessage.RemoveMarkup(Loc.GetString(otherAction.Name));
|
||||
if (name != otherName)
|
||||
return string.Compare(name, otherName, StringComparison.CurrentCulture);
|
||||
|
||||
if (Provider != otherAction.Provider)
|
||||
{
|
||||
if (Provider == null)
|
||||
return -1;
|
||||
|
||||
if (otherAction.Provider == null)
|
||||
return 1;
|
||||
|
||||
// uid to int casting... it says "Do NOT use this in content". You can't tell me what to do.
|
||||
return (int) Provider - (int) otherAction.Provider;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proper client-side state handling requires the ability to clone an action from the component state.
|
||||
/// Otherwise modifying the action can lead to modifying the stored server state.
|
||||
/// </summary>
|
||||
public abstract object Clone();
|
||||
|
||||
public virtual void CopyFrom(object objectToClone)
|
||||
{
|
||||
if (objectToClone is not ActionType toClone)
|
||||
return;
|
||||
|
||||
// This is pretty Ugly to look at. But actions are sent to the client in a component state, so they have to be
|
||||
// cloneable. Would be easy if this were a struct of only value-types, but I don't want to restrict actions like
|
||||
// that.
|
||||
Priority = toClone.Priority;
|
||||
Icon = toClone.Icon;
|
||||
IconOn = toClone.IconOn;
|
||||
Name = toClone.Name;
|
||||
Description = toClone.Description;
|
||||
Provider = toClone.Provider;
|
||||
AttachedEntity = toClone.AttachedEntity;
|
||||
Enabled = toClone.Enabled;
|
||||
Toggled = toClone.Toggled;
|
||||
Cooldown = toClone.Cooldown;
|
||||
Charges = toClone.Charges;
|
||||
Keywords = new(toClone.Keywords);
|
||||
AutoPopulate = toClone.AutoPopulate;
|
||||
AutoRemove = toClone.AutoRemove;
|
||||
ItemIconStyle = toClone.ItemIconStyle;
|
||||
CheckCanInteract = toClone.CheckCanInteract;
|
||||
Speech = toClone.Speech;
|
||||
UseDelay = toClone.UseDelay;
|
||||
Sound = toClone.Sound;
|
||||
AudioParams = toClone.AudioParams;
|
||||
UserPopup = toClone.UserPopup;
|
||||
Popup = toClone.Popup;
|
||||
PopupToggleSuffix = toClone.PopupToggleSuffix;
|
||||
ItemIconStyle = toClone.ItemIconStyle;
|
||||
}
|
||||
|
||||
public bool Equals(ActionType? other)
|
||||
{
|
||||
return CompareTo(other) == 0;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var hashCode = Priority.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Name.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ Provider.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Content.Shared/Actions/ActionTypes/InstantAction.cs
Normal file
41
Content.Shared/Actions/ActionTypes/InstantAction.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Instantaneous action with no extra targeting information. Will result in <see cref="PerformActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class InstantAction : ActionType
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[DataField("event")]
|
||||
[NonSerialized]
|
||||
public PerformActionEvent? Event;
|
||||
|
||||
public InstantAction() { }
|
||||
public InstantAction(InstantAction toClone)
|
||||
{
|
||||
CopyFrom(toClone);
|
||||
}
|
||||
|
||||
public override void CopyFrom(object objectToClone)
|
||||
{
|
||||
base.CopyFrom(objectToClone);
|
||||
|
||||
if (objectToClone is not InstantAction toClone)
|
||||
return;
|
||||
|
||||
// Events should be re-usable, and shouldn't be modified during prediction.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new InstantAction(this);
|
||||
}
|
||||
}
|
||||
153
Content.Shared/Actions/ActionTypes/TargetedAction.cs
Normal file
153
Content.Shared/Actions/ActionTypes/TargetedAction.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions.ActionTypes;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class TargetedAction : ActionType
|
||||
{
|
||||
/// <summary>
|
||||
/// For entity- or map-targeting actions, if this is true the action will remain selected after it is used, so
|
||||
/// it can be continuously re-used. If this is false, the action will be deselected after one use.
|
||||
/// </summary>
|
||||
[DataField("repeat")]
|
||||
public bool Repeat;
|
||||
|
||||
/// <summary>
|
||||
/// For entity- or map-targeting action, determines whether the action is deselected if the user doesn't click a valid target.
|
||||
/// </summary>
|
||||
[DataField("deselectOnMiss")]
|
||||
public bool DeselectOnMiss;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the action system should block this action if the user cannot actually access the target
|
||||
/// (unobstructed, in inventory, in backpack, etc). Some spells or abilities may want to disable this and
|
||||
/// implement their own checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Even if this is false, the <see cref="Range"/> will still be checked.
|
||||
/// </remarks>
|
||||
[DataField("checkCanAccess")]
|
||||
public bool CheckCanAccess = true;
|
||||
|
||||
[DataField("range")]
|
||||
public float Range = SharedInteractionSystem.InteractionRange;
|
||||
|
||||
/// <summary>
|
||||
/// If the target is invalid, this bool determines whether the left-click will default to performing a standard-interaction
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Interactions will still be blocked if the target-validation generates a pop-up
|
||||
/// </remarks>
|
||||
[DataField("interactOnMiss")]
|
||||
public bool InteractOnMiss = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, and if <see cref="ShowHandItemOverlay"/> is enabled, then this action's icon will be drawn by that
|
||||
/// over lay in place of the currently held item "held item".
|
||||
/// </summary>
|
||||
[DataField("targetingIndicator")]
|
||||
public bool TargetingIndicator = true;
|
||||
|
||||
public override void CopyFrom(object objectToClone)
|
||||
{
|
||||
base.CopyFrom(objectToClone);
|
||||
|
||||
if (objectToClone is not TargetedAction toClone)
|
||||
return;
|
||||
|
||||
Range = toClone.Range;
|
||||
CheckCanAccess = toClone.CheckCanAccess;
|
||||
DeselectOnMiss = toClone.DeselectOnMiss;
|
||||
Repeat = toClone.Repeat;
|
||||
InteractOnMiss = toClone.InteractOnMiss;
|
||||
TargetingIndicator = toClone.TargetingIndicator;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action that targets some entity. Will result in <see cref="PerformEntityTargetActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class EntityTargetAction : TargetedAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
[DataField("event")]
|
||||
public PerformEntityTargetActionEvent? Event;
|
||||
|
||||
[DataField("whitelist")]
|
||||
public EntityWhitelist? Whitelist;
|
||||
|
||||
[DataField("canTargetSelf")]
|
||||
public bool CanTargetSelf = true;
|
||||
|
||||
public EntityTargetAction() { }
|
||||
public EntityTargetAction(EntityTargetAction toClone)
|
||||
{
|
||||
CopyFrom(toClone);
|
||||
}
|
||||
public override void CopyFrom(object objectToClone)
|
||||
{
|
||||
base.CopyFrom(objectToClone);
|
||||
|
||||
if (objectToClone is not EntityTargetAction toClone)
|
||||
return;
|
||||
|
||||
CanTargetSelf = toClone.CanTargetSelf;
|
||||
|
||||
// This isn't a deep copy, but I don't expect white-lists to ever be edited during prediction. So good enough?
|
||||
Whitelist = toClone.Whitelist;
|
||||
|
||||
// Events should be re-usable, and shouldn't be modified during prediction.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new EntityTargetAction(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action that targets some map coordinates. Will result in <see cref="PerformWorldTargetActionEvent"/> being raised.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
[Virtual]
|
||||
public class WorldTargetAction : TargetedAction
|
||||
{
|
||||
/// <summary>
|
||||
/// The local-event to raise when this action is performed.
|
||||
/// </summary>
|
||||
[DataField("event")]
|
||||
[NonSerialized]
|
||||
public PerformWorldTargetActionEvent? Event;
|
||||
|
||||
public WorldTargetAction() { }
|
||||
public WorldTargetAction(WorldTargetAction toClone)
|
||||
{
|
||||
CopyFrom(toClone);
|
||||
}
|
||||
|
||||
public override void CopyFrom(object objectToClone)
|
||||
{
|
||||
base.CopyFrom(objectToClone);
|
||||
|
||||
if (objectToClone is not WorldTargetAction toClone)
|
||||
return;
|
||||
|
||||
// Events should be re-usable, and shouldn't be modified during prediction.
|
||||
Event = toClone.Event;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new WorldTargetAction(this);
|
||||
}
|
||||
}
|
||||
46
Content.Shared/Actions/ActionsComponent.cs
Normal file
46
Content.Shared/Actions/ActionsComponent.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
[NetworkedComponent]
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(SharedActionsSystem))]
|
||||
public sealed class ActionsComponent : Component
|
||||
{
|
||||
[ViewVariables]
|
||||
public SortedSet<ActionType> Actions = new();
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ActionsComponentState : ComponentState
|
||||
{
|
||||
public readonly List<ActionType> Actions;
|
||||
|
||||
public ActionsComponentState(List<ActionType> actions)
|
||||
{
|
||||
Actions = actions;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the action icon appears in the hotbar for item actions.
|
||||
/// </summary>
|
||||
public enum ItemActionIconStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The default - The item icon will be big with a small action icon in the corner
|
||||
/// </summary>
|
||||
BigItem,
|
||||
|
||||
/// <summary>
|
||||
/// The action icon will be big with a small item icon in the corner
|
||||
/// </summary>
|
||||
BigAction,
|
||||
|
||||
/// <summary>
|
||||
/// BigAction but no item icon will be shown in the corner.
|
||||
/// </summary>
|
||||
NoItem
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of action behaviors.
|
||||
/// </summary>
|
||||
public interface IActionBehavior
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all action event args
|
||||
/// </summary>
|
||||
public abstract class ActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly EntityUid Performer;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ActionType ActionType;
|
||||
/// <summary>
|
||||
/// Actions component of the performer.
|
||||
/// </summary>
|
||||
public readonly SharedActionsComponent? PerformerActions;
|
||||
|
||||
public ActionEventArgs(EntityUid performer, ActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent(Performer, out PerformerActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {IoCManager.Resolve<IEntityManager>().GetComponent<MetaDataComponent>(performer).EntityName} tried to perform action {actionType} " +
|
||||
$" but the performer had no actions component," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantAction : IActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class InstantActionEventArgs : ActionEventArgs
|
||||
{
|
||||
public InstantActionEventArgs(EntityUid performer, ActionType actionType) : base(performer, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetEntityActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public TargetEntityActionEventArgs(EntityUid performer, ActionType actionType, EntityUid target) :
|
||||
base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetEntityItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public TargetEntityItemActionEventArgs(EntityUid performer, EntityUid target, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetPointActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointActionEventArgs(EntityUid performer, EntityCoordinates target, ActionType actionType)
|
||||
: base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class TargetPointItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointItemActionEventArgs(EntityUid performer, EntityCoordinates target, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via SharedActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class ToggleActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle is attempting to be toggled on, false if attempting to toggle off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleActionEventArgs(EntityUid performer, ActionType actionType, bool toggledOn) : base(performer, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantItemAction : IItemActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class InstantItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
public InstantItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType) :
|
||||
base(performer, item, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of item action behaviors.
|
||||
/// </summary>
|
||||
public interface IItemActionBehavior
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all item action event args
|
||||
/// </summary>
|
||||
public abstract class ItemActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly EntityUid Performer;
|
||||
/// <summary>
|
||||
/// Item being used to perform the action
|
||||
/// </summary>
|
||||
public readonly EntityUid Item;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ItemActionType ActionType;
|
||||
/// <summary>
|
||||
/// Item actions component of the item.
|
||||
/// </summary>
|
||||
public readonly ItemActionsComponent? ItemActions;
|
||||
|
||||
public ItemActionEventArgs(EntityUid performer, EntityUid item, ItemActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
if (!entMan.TryGetComponent(Item, out ItemActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {entMan.GetComponent<MetaDataComponent>(performer).EntityName} tried to perform item action {actionType} " +
|
||||
$" for item {entMan.GetComponent<MetaDataComponent>(Item).EntityName} but the item had no ItemActionsComponent," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions.Behaviors.Item
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via ItemActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public sealed class ToggleItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle was toggled on, false if it was toggled off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleItemActionEventArgs(EntityUid performer, bool toggledOn, EntityUid item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This should be used on items which provide actions. Defines which actions the item provides
|
||||
/// and allows modifying the states of those actions. Item components should use this rather than
|
||||
/// SharedActionsComponent on the player to handle granting / revoking / modifying the states of the
|
||||
/// actions provided by this item.
|
||||
///
|
||||
/// When a player equips this item, all the actions defined in this component will be granted to the
|
||||
/// player in their current states. This means the states will persist between players.
|
||||
///
|
||||
/// Currently only maintained server side and not synced to client, as are all the equip/unequip events.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class ItemActionsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the item actions initially provided by this item. Actions defined here
|
||||
/// will be automatically granted unless their state is modified using the methods
|
||||
/// on this component. Additional actions can be granted by this item via GrantOrUpdate
|
||||
/// </summary>
|
||||
public IEnumerable<ItemActionConfig> ActionConfigs => _actionConfigs;
|
||||
|
||||
public bool IsEquipped;
|
||||
|
||||
/// <summary>
|
||||
/// hand it's currently in, null if not in a hand.
|
||||
/// </summary>
|
||||
public Hand? InHand;
|
||||
|
||||
/// <summary>
|
||||
/// Entity currently holding this in hand or equip slot. Null if not held.
|
||||
/// </summary>
|
||||
public EntityUid? Holder;
|
||||
// cached actions component of the holder, since we'll need to access it frequently
|
||||
public SharedActionsComponent? HolderActionsComponent;
|
||||
|
||||
[DataField("actions")]
|
||||
private List<ItemActionConfig> _actionConfigs
|
||||
{
|
||||
get => internalActionConfigs;
|
||||
set
|
||||
{
|
||||
internalActionConfigs = value;
|
||||
foreach (var actionConfig in value)
|
||||
{
|
||||
GrantOrUpdate(actionConfig.ActionType, actionConfig.Enabled, false, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State of all actions provided by this item.
|
||||
private readonly Dictionary<ItemActionType, ActionState> _actions = new();
|
||||
private List<ItemActionConfig> internalActionConfigs = new ();
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
RevokeAllFromHolder();
|
||||
}
|
||||
|
||||
public void GrantOrUpdateAllToHolder()
|
||||
{
|
||||
if (HolderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
HolderActionsComponent.GrantOrUpdateItemAction(actionType, Owner, state);
|
||||
}
|
||||
}
|
||||
|
||||
public void RevokeAllFromHolder()
|
||||
{
|
||||
if (HolderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
HolderActionsComponent.RevokeItemAction(actionType, Owner);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the state of the action, granting it if it isn't already granted.
|
||||
/// If the action had any existing state, those specific fields will be overwritten by any
|
||||
/// corresponding non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="actionType">action being granted / updated</param>
|
||||
/// <param name="enabled">When null, preserves the current enable status of the action, defaulting
|
||||
/// to true if action has no current state.
|
||||
/// When non-null, indicates whether the entity is able to perform the action (if disabled,
|
||||
/// the player will see they have the action but it will appear greyed out)</param>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null (unless clearCooldown is true), preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null or clearCooldown is true, action cooldown will be set to this value. Note that this cooldown
|
||||
/// is tied to this item.</param>
|
||||
/// <param name="clearCooldown"> If true, setting cooldown to null will clear the current cooldown
|
||||
/// of this action rather than preserving it.</param>
|
||||
public void GrantOrUpdate(ItemActionType actionType, bool? enabled = null,
|
||||
bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
// this will be overwritten if we find the value in our dict, otherwise
|
||||
// we will use this as our new action state.
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? true, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && enabled != actionState.Enabled)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = true;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
HolderActionsComponent?.GrantOrUpdateItemAction(actionType, Owner, actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cooldown of a particular action. Actions on cooldown cannot be used.
|
||||
/// Setting the cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ItemActionType actionType, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable / disable this action. Disabled actions are still shown to the player, but
|
||||
/// shown as not usable.
|
||||
/// </summary>
|
||||
public void SetEnabled(ItemActionType actionType, bool enabled)
|
||||
{
|
||||
GrantOrUpdate(actionType, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the action on / off
|
||||
/// </summary>
|
||||
public void Toggle(ItemActionType actionType, bool toggleOn)
|
||||
{
|
||||
GrantOrUpdate(actionType, toggleOn: toggleOn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for an item action provided by an item.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed class ItemActionConfig : ISerializationHooks
|
||||
{
|
||||
[DataField("actionType", required: true)]
|
||||
public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
|
||||
|
||||
/// <summary>
|
||||
/// Whether action is initially enabled on this item. Defaults to true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; private set; } = true;
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "invalid or missing actionType");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,641 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Actions.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the actions available to an entity.
|
||||
/// Should only be used for player-controlled entities.
|
||||
///
|
||||
/// Actions are granted directly to the owner entity. Item actions are granted via a particular item which
|
||||
/// must be in the owner's inventory (the action is revoked when item leaves the owner's inventory). This
|
||||
/// should almost always be done via ItemActionsComponent on the item entity (which also tracks the
|
||||
/// cooldowns associated with the actions on that item).
|
||||
///
|
||||
/// Actions can still have an associated state even when revoked. For example, a flashlight toggle action
|
||||
/// may be unusable while the player is stunned, but this component will still have an entry for the action
|
||||
/// so the user can see whether it's currently toggled on or off.
|
||||
/// </summary>
|
||||
[NetworkedComponent()]
|
||||
public abstract class SharedActionsComponent : Component
|
||||
{
|
||||
private static readonly TimeSpan CooldownExpiryThreshold = TimeSpan.FromSeconds(10);
|
||||
|
||||
[Dependency]
|
||||
protected readonly ActionManager ActionManager = default!;
|
||||
[Dependency]
|
||||
protected readonly IGameTiming GameTiming = default!;
|
||||
[Dependency]
|
||||
protected readonly IEntityManager EntityManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Actions granted to this entity as soon as they spawn, regardless
|
||||
/// of the status of the entity.
|
||||
/// </summary>
|
||||
public IEnumerable<ActionType> InnateActions => _innateActions ?? Enumerable.Empty<ActionType>();
|
||||
[DataField("innateActions")]
|
||||
private List<ActionType>? _innateActions = null;
|
||||
|
||||
|
||||
// entries are removed from this if they are at the initial state (not enabled, no cooldown, toggled off).
|
||||
// a system runs which periodically removes cooldowns from entries when they are revoked and their
|
||||
// cooldowns have expired for a long enough time, also removing the entry if it is then at initial state.
|
||||
// This helps to keep our component state smaller.
|
||||
[ViewVariables]
|
||||
private Dictionary<ActionType, ActionState> _actions = new();
|
||||
|
||||
// Holds item action states. Item actions are only added to this when granted, and are removed
|
||||
// when revoked or when they leave inventory. This is almost entirely handled by ItemActionsComponent on
|
||||
// item entities.
|
||||
[ViewVariables]
|
||||
private Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> _itemActions =
|
||||
new();
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
foreach (var actionType in InnateActions)
|
||||
{
|
||||
Grant(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new ActionComponentState(_actions, _itemActions);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_actions = state.Actions;
|
||||
_itemActions = state.ItemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action state associated with the specified action type, if it has been
|
||||
/// granted, has a cooldown, or has been toggled on
|
||||
/// </summary>
|
||||
/// <returns>false if not found for this action type</returns>
|
||||
public bool TryGetActionState(ActionType actionType, out ActionState actionState)
|
||||
{
|
||||
return _actions.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action states associated with the specified item if any have been granted
|
||||
/// and not yet revoked.
|
||||
/// </summary>
|
||||
/// <returns>false if no states found for this item action type.</returns>
|
||||
public bool TryGetItemActionStates(EntityUid item, [NotNullWhen((true))] out IReadOnlyDictionary<ItemActionType, ActionState>? itemActionStates)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
itemActionStates = actualItemActionStates;
|
||||
return true;
|
||||
}
|
||||
|
||||
itemActionStates = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action state associated with the specified item action type for the specified item, if it has any.
|
||||
/// </summary>
|
||||
/// <returns>false if no state found for this item action type for this item</returns>
|
||||
public bool TryGetItemActionState(ItemActionType actionType, EntityUid item, out ActionState actionState)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
return actualItemActionStates.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
actionState = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled (if item action, if granted and enabled for any item)</returns>
|
||||
public bool IsGranted(BaseActionPrototype actionType)
|
||||
{
|
||||
return actionType switch
|
||||
{
|
||||
ActionPrototype actionPrototype => IsGranted(actionPrototype.ActionType),
|
||||
ItemActionPrototype itemActionPrototype => IsGranted(itemActionPrototype.ActionType),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsGranted(ActionType actionType)
|
||||
{
|
||||
if (TryGetActionState(actionType, out var actionState))
|
||||
{
|
||||
return actionState.Enabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled for any item. This
|
||||
/// has to traverse the entire item state dictionary so please avoid frequent calls.</returns>
|
||||
public bool IsGranted(ItemActionType actionType)
|
||||
{
|
||||
return _itemActions.Values.SelectMany(vals => vals)
|
||||
.Any(state => state.Key == actionType && state.Value.Enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all action types that have non-initial state (granted, have a cooldown, or toggled on).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<ActionType, ActionState> ActionStates()
|
||||
{
|
||||
return _actions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items that have actions currently granted (that are not revoked
|
||||
/// and still in inventory).
|
||||
/// Map from item uid -> (action type -> associated action state)
|
||||
/// PLEASE DO NOT MODIFY THE INNER DICTIONARY! I CANNOT CAST IT TO IReadOnlyDictionary!
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<EntityUid,Dictionary<ItemActionType, ActionState>> ItemActionStates()
|
||||
{
|
||||
return _itemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the action state with the supplied non-null values
|
||||
/// </summary>
|
||||
private void GrantOrUpdate(ActionType actionType, bool? enabled = null, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
// no state at all for this action, create it anew
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? false, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && actionState.Enabled != enabled.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = enabled.Value;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent.
|
||||
/// Updates the state of the item action provided by the item, granting the action
|
||||
/// if it is not yet granted to the player. Should be called whenever the
|
||||
/// status changes. The existing state will be completely overwritten by the new state.
|
||||
/// </summary>
|
||||
public void GrantOrUpdateItemAction(ItemActionType actionType, EntityUid item, ActionState state)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
{
|
||||
itemStates = new Dictionary<ItemActionType, ActionState>();
|
||||
_itemActions[item] = itemStates;
|
||||
}
|
||||
|
||||
itemStates[actionType] = state;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent. Revokes the item action so the player no longer
|
||||
/// sees it and can no longer use it.
|
||||
/// </summary>
|
||||
public void RevokeItemAction(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
return;
|
||||
|
||||
itemStates.Remove(actionType);
|
||||
if (itemStates.Count == 0)
|
||||
{
|
||||
_itemActions.Remove(item);
|
||||
}
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, optionally overriding its
|
||||
/// current state with specified values.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be preserved, with specific fields optionally overridden by any of the provided
|
||||
/// non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null, preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null, action cooldown will be set to this value.</param>
|
||||
public void Grant(ActionType actionType, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, true, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, resetting its state
|
||||
/// to its initial state and settings its state based on supplied parameters.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be reset to initial (no cooldown, toggled off).
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown">action cooldown will be set to this value (by default the cooldown is cleared).</param>
|
||||
public void GrantFromInitialState(ActionType actionType, bool toggleOn = false,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
Grant(actionType, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cooldown for the action. Actions on cooldown cannot be used.
|
||||
///
|
||||
/// This will work even if the action is revoked -
|
||||
/// for example if there's an ability with a cooldown which is temporarily unusable due
|
||||
/// to the player being stunned, the cooldown will still tick down even while the player
|
||||
/// is stunned.
|
||||
///
|
||||
/// Setting cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ActionType actionType, (TimeSpan start, TimeSpan end)? cooldown)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revokes the ability to perform the action for this entity. Current state
|
||||
/// of the action (toggle / cooldown) is preserved.
|
||||
/// </summary>
|
||||
public void Revoke(ActionType actionType)
|
||||
{
|
||||
if (!_actions.TryGetValue(actionType, out var actionState)) return;
|
||||
|
||||
if (!actionState.Enabled) return;
|
||||
|
||||
actionState.Enabled = false;
|
||||
|
||||
// don't store it anymore if its at its initial state.
|
||||
if (actionState.IsAtInitialState)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actions[actionType] = actionState;
|
||||
}
|
||||
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action to the specified value. Works even if the action is on cooldown
|
||||
/// or revoked.
|
||||
/// </summary>
|
||||
public void ToggleAction(ActionType actionType, bool toggleOn)
|
||||
{
|
||||
Grant(actionType, toggleOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears any cooldowns which have expired beyond the predefined threshold.
|
||||
/// this should be run periodically to ensure we don't have unbounded growth of
|
||||
/// our saved action data, and keep our component state sent to the client as minimal as possible.
|
||||
/// </summary>
|
||||
public void ExpireCooldowns()
|
||||
{
|
||||
|
||||
// actions - only clear cooldowns and remove associated action state
|
||||
// if the action is at initial state
|
||||
var actionTypesToRemove = new List<ActionType>();
|
||||
foreach (var (actionType, actionState) in _actions)
|
||||
{
|
||||
// ignore it unless we may be able to delete it due to
|
||||
// clearing the cooldown
|
||||
if (!actionState.IsAtInitialStateExceptCooldown) continue;
|
||||
if (!actionState.Cooldown.HasValue)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
continue;
|
||||
}
|
||||
var expiryTime = GameTiming.CurTime - actionState.Cooldown.Value.Item2;
|
||||
if (expiryTime > CooldownExpiryThreshold)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var remove in actionTypesToRemove)
|
||||
{
|
||||
_actions.Remove(remove);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a change has been made to an action state in this component.
|
||||
/// </summary>
|
||||
protected virtual void AfterActionChanged() { }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ActionComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<ActionType, ActionState> Actions;
|
||||
public Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> ItemActions;
|
||||
|
||||
public ActionComponentState(Dictionary<ActionType, ActionState> actions,
|
||||
Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> itemActions)
|
||||
{
|
||||
Actions = actions;
|
||||
ItemActions = itemActions;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public struct ActionState
|
||||
{
|
||||
/// <summary>
|
||||
/// False if this action is not currently allowed to be performed.
|
||||
/// </summary>
|
||||
public bool Enabled;
|
||||
/// <summary>
|
||||
/// Only used for toggle actions, indicates whether it's currently toggled on or off
|
||||
/// TODO: Eventually this should probably be a byte so we it can toggle through multiple states.
|
||||
/// </summary>
|
||||
public bool ToggledOn;
|
||||
public (TimeSpan start, TimeSpan end)? Cooldown;
|
||||
public bool IsAtInitialState => IsAtInitialStateExceptCooldown && !Cooldown.HasValue;
|
||||
public bool IsAtInitialStateExceptCooldown => !Enabled && !ToggledOn;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action state for the indicated type, defaulting to the
|
||||
/// initial state.
|
||||
/// </summary>
|
||||
public ActionState(bool enabled = false, bool toggledOn = false, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
Enabled = enabled;
|
||||
ToggledOn = toggledOn;
|
||||
Cooldown = cooldown;
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(TimeSpan curTime)
|
||||
{
|
||||
if (Cooldown == null) return false;
|
||||
return curTime < Cooldown.Value.Item2;
|
||||
}
|
||||
public bool IsOnCooldown(IGameTiming gameTiming)
|
||||
{
|
||||
return IsOnCooldown(gameTiming.CurTime);
|
||||
}
|
||||
|
||||
public bool Equals(ActionState other)
|
||||
{
|
||||
return Enabled == other.Enabled && ToggledOn == other.ToggledOn && Nullable.Equals(Cooldown, other.Cooldown);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ActionState other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Enabled, ToggledOn, Cooldown);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
#pragma warning disable 618
|
||||
public abstract class BasePerformActionMessage : ComponentMessage
|
||||
#pragma warning restore 618
|
||||
{
|
||||
public abstract BehaviorType BehaviorType { get; }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ActionType ActionType;
|
||||
|
||||
protected PerformActionMessage(ActionType actionType)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformItemActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ItemActionType ActionType;
|
||||
public readonly EntityUid Item;
|
||||
|
||||
protected PerformItemActionMessage(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformInstantActionMessage : PerformActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantActionMessage(ActionType actionType) : base(actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformInstantItemActionMessage : PerformItemActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IToggleActionMessage
|
||||
{
|
||||
bool ToggleOn { get; }
|
||||
}
|
||||
|
||||
public interface ITargetPointActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted local coordinates
|
||||
/// </summary>
|
||||
EntityCoordinates Target { get; }
|
||||
}
|
||||
|
||||
public interface ITargetEntityActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted entity
|
||||
/// </summary>
|
||||
EntityUid Target { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOnActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOffActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOnItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformToggleOffItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetPointActionMessage : PerformActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
|
||||
public PerformTargetPointActionMessage(ActionType actionType, EntityCoordinates target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetPointItemActionMessage : PerformItemActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
|
||||
public PerformTargetPointItemActionMessage(ItemActionType actionType, EntityUid item, EntityCoordinates target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetEntityActionMessage : PerformActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityActionMessage(ActionType actionType, EntityUid target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PerformTargetEntityItemActionMessage : PerformItemActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityItemActionMessage(ItemActionType actionType, EntityUid item, EntityUid target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// An attempt to perform a specific action. Main purpose of this interface is to
|
||||
/// reduce code duplication related to handling attempts to perform non-item vs item actions by
|
||||
/// providing a single interface for various functionality that needs to be performed on both.
|
||||
/// </summary>
|
||||
public interface IActionAttempt
|
||||
{
|
||||
/// <summary>
|
||||
/// Action Prototype attempting to be performed
|
||||
/// </summary>
|
||||
BaseActionPrototype Action { get; }
|
||||
#pragma warning disable 618
|
||||
ComponentMessage PerformInstantActionMessage();
|
||||
ComponentMessage PerformToggleActionMessage(bool on);
|
||||
ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
#pragma warning restore 618
|
||||
/// <summary>
|
||||
/// Tries to get the action state for this action from the actionsComponent, returning
|
||||
/// true if found.
|
||||
/// </summary>
|
||||
bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action within the provided action component
|
||||
/// </summary>
|
||||
void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the action
|
||||
/// </summary>
|
||||
void DoInstantAction(EntityUid player);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the toggle action
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the
|
||||
/// indicated value</returns>
|
||||
bool DoToggleAction(EntityUid player, bool on);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target point action
|
||||
/// </summary>
|
||||
void DoTargetPointAction(EntityUid player, EntityCoordinates target);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target entity action
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(EntityUid player, EntityUid target);
|
||||
}
|
||||
|
||||
public sealed class ActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ActionPrototype _action;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ActionAttempt(ActionPrototype action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetActionState(_action.ActionType, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
actionsComponent.ToggleAction(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
public void DoInstantAction(EntityUid player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantActionEventArgs(player, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(EntityUid player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleActionEventArgs(player, _action.ActionType, on));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointActionEventArgs(player, target, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(EntityUid player, EntityUid target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityActionEventArgs(player, _action.ActionType,
|
||||
target));
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformInstantActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnActionMessage(_action.ActionType);
|
||||
}
|
||||
return new PerformToggleOffActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetPointActionMessage(_action.ActionType, args.Coordinates);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetEntityActionMessage(_action.ActionType, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ItemActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ItemActionPrototype _action;
|
||||
private readonly EntityUid _item;
|
||||
private readonly ItemActionsComponent _itemActions;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ItemActionAttempt(ItemActionPrototype action, EntityUid item, ItemActionsComponent itemActions)
|
||||
{
|
||||
_action = action;
|
||||
_item = item;
|
||||
_itemActions = itemActions;
|
||||
}
|
||||
|
||||
public void DoInstantAction(EntityUid player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantItemActionEventArgs(player, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(EntityUid player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleItemActionEventArgs(player, on, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(EntityUid player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointItemActionEventArgs(player, target, _item,
|
||||
_action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(EntityUid player, EntityUid target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityItemActionEventArgs(player, target,
|
||||
_item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetItemActionState(_action.ActionType, _item, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
_itemActions.Toggle(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformInstantItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
return new PerformToggleOffItemActionMessage(_action.ActionType, _item);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetPointItemActionMessage(_action.ActionType, _item, args.Coordinates);
|
||||
}
|
||||
|
||||
#pragma warning disable 618
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
return new PerformTargetEntityItemActionMessage(_action.ActionType, _item, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}, {nameof(_item)}: {_item}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Module;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted directly to an entity (such as an innate ability
|
||||
/// or skill).
|
||||
/// </summary>
|
||||
[Prototype("action")]
|
||||
[DataDefinition]
|
||||
public sealed class ActionPrototype : BaseActionPrototype, ISerializationHooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of action, no 2 action prototypes should have the same one.
|
||||
/// </summary>
|
||||
[DataField("actionType", required: true)]
|
||||
public ActionType ActionType { get; set; }
|
||||
|
||||
[DataField("behavior", serverOnly: true)]
|
||||
private IActionBehavior? Behavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantAction InstantAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleAction ToggleAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityAction TargetEntityAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointAction TargetPointAction { get; private set; } = default!;
|
||||
|
||||
public override string ID => ActionType.ToString();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
base.AfterDeserialization();
|
||||
|
||||
if (ActionType == ActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
switch (Behavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for action prototypes.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract class BaseActionPrototype : IPrototype, ISerializationHooks
|
||||
{
|
||||
public abstract string ID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Icon representing this action in the UI.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier Icon { get; } = SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// For toggle actions only, icon to show when toggled on. If omitted,
|
||||
/// the action will simply be highlighted when turned on.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("iconOn")]
|
||||
public SpriteSpecifier IconOn { get; } = SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// Name to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
[DataField("name")]
|
||||
public FormattedMessage Name { get; private set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Description to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
[DataField("description")]
|
||||
public FormattedMessage Description { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Requirements message to show in UI. Accepts formatting, but generally should be avoided
|
||||
/// so the requirements message isn't too prominent in the tooltip.
|
||||
/// </summary>
|
||||
[DataField("requires")]
|
||||
public string Requires { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The type of behavior this action has. This is valid clientside and serverside.
|
||||
/// </summary>
|
||||
[DataField("behaviorType")]
|
||||
public BehaviorType BehaviorType { get; protected set; } = BehaviorType.None;
|
||||
|
||||
/// <summary>
|
||||
/// For targetpoint or targetentity actions, if this is true the action will remain
|
||||
/// selected after it is used, so it can be continuously re-used. If this is false,
|
||||
/// the action will be deselected after one use.
|
||||
/// </summary>
|
||||
[DataField("repeat")]
|
||||
public bool Repeat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For TargetEntity/TargetPoint actions, should the action be de-selected if currently selected (choosing a target)
|
||||
/// when it goes on cooldown. Defaults to false.
|
||||
/// </summary>
|
||||
[DataField("deselectOnCooldown")]
|
||||
public bool DeselectOnCooldown { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For TargetEntity actions, should the action be de-selected if the user doesn't click an entity when
|
||||
/// selecting a target. Defaults to false.
|
||||
/// </summary>
|
||||
[DataField("deselectWhenEntityNotClicked")]
|
||||
public bool DeselectWhenEntityNotClicked { get; }
|
||||
|
||||
[DataField("filters")] private List<string> _filters = new();
|
||||
|
||||
/// <summary>
|
||||
/// Filters that can be used to filter this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Filters => _filters;
|
||||
|
||||
[DataField("keywords")] private List<string> _keywords = new();
|
||||
|
||||
/// <summary>
|
||||
/// Keywords that can be used to search this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Keywords => _keywords;
|
||||
|
||||
/// <summary>
|
||||
/// True if this is an action that requires selecting a target
|
||||
/// </summary>
|
||||
public bool IsTargetAction =>
|
||||
BehaviorType is BehaviorType.TargetEntity or BehaviorType.TargetPoint;
|
||||
|
||||
public virtual void AfterDeserialization()
|
||||
{
|
||||
Name = new FormattedMessage();
|
||||
Name.AddText(ID);
|
||||
|
||||
if (BehaviorType == BehaviorType.None)
|
||||
{
|
||||
Logger.ErrorS("action", $"Missing behaviorType for action with name {Name}");
|
||||
}
|
||||
|
||||
if (BehaviorType is not (BehaviorType.Toggle or BehaviorType.TargetEntity or BehaviorType.TargetPoint) && IconOn != SpriteSpecifier.Invalid)
|
||||
{
|
||||
Logger.ErrorS("action", $"for action {Name}, iconOn was specified but behavior" +
|
||||
" $type was {BehaviorType}. iconOn is only supported for Toggle behavior type.");
|
||||
}
|
||||
|
||||
if (Repeat && BehaviorType != BehaviorType.TargetEntity && BehaviorType != BehaviorType.TargetPoint)
|
||||
{
|
||||
Logger.ErrorS("action", $" action named {Name} used repeat: true, but this is only supported for" +
|
||||
" $TargetEntity and TargetPoint behaviorType and its behaviorType is {BehaviorType}");
|
||||
}
|
||||
}
|
||||
|
||||
protected void ValidateBehaviorType(BehaviorType expected, Type actualInterface)
|
||||
{
|
||||
if (BehaviorType != expected)
|
||||
{
|
||||
Logger.ErrorS("action", $"for action named {Name}, behavior implements " +
|
||||
$"{actualInterface.Name}, so behaviorType should be {expected} but was {BehaviorType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The behavior / logic of the action. Each of these corresponds to a particular IActionBehavior
|
||||
/// (for actions) or IItemActionBehavior (for item actions)
|
||||
/// interface. Corresponds to action.behaviorType in YAML
|
||||
/// </summary>
|
||||
public enum BehaviorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Action doesn't do anything.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// IInstantAction/IInstantItemAction. Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
Instant,
|
||||
|
||||
/// <summary>
|
||||
/// IToggleAction/IToggleItemAction Action which can be toggled on and off
|
||||
/// </summary>
|
||||
Toggle,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetEntityAction/ITargetEntityItemAction. Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
TargetEntity,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetPointAction/ITargetPointItemAction. Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
TargetPoint
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
using Content.Shared.Actions.Behaviors;
|
||||
using Content.Shared.Actions.Behaviors.Item;
|
||||
using Content.Shared.Module;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Actions.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted to an entity via an item (such as toggling a flashlight).
|
||||
/// </summary>
|
||||
[Prototype("itemAction")]
|
||||
[DataDefinition]
|
||||
public sealed class ItemActionPrototype : BaseActionPrototype, ISerializationHooks
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of item action, no 2 itemAction prototypes should have the same one.
|
||||
/// </summary>
|
||||
[DataField("actionType")]
|
||||
public ItemActionType ActionType { get; private set; } = ItemActionType.Error;
|
||||
|
||||
/// <see cref="ItemActionIconStyle"/>
|
||||
[DataField("iconStyle")]
|
||||
public ItemActionIconStyle IconStyle { get; private set; } = ItemActionIconStyle.BigItem;
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantItemAction InstantAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleItemAction ToggleAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityItemAction TargetEntityAction { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointItemAction TargetPointAction { get; private set; } = default!;
|
||||
|
||||
[DataField("behavior", readOnly: true, serverOnly: true)]
|
||||
public IItemActionBehavior? ItemActionBehavior { get; private set; }
|
||||
|
||||
public override string ID => ActionType.ToString();
|
||||
|
||||
public override void AfterDeserialization()
|
||||
{
|
||||
base.AfterDeserialization();
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
// TODO: Split this class into server/client after RobustToolbox#1405
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
switch (ItemActionBehavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantItemAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantItemAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleItemAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleItemAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityItemAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityItemAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointItemAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointItemAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the action icon appears in the hotbar for item actions.
|
||||
/// </summary>
|
||||
public enum ItemActionIconStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The default - the item icon will be big with a small action icon in the corner
|
||||
/// </summary>
|
||||
BigItem,
|
||||
/// <summary>
|
||||
/// The action icon will be big with a small item icon in the corner
|
||||
/// </summary>
|
||||
BigAction,
|
||||
/// <summary>
|
||||
/// BigAction but no item icon will be shown in the corner.
|
||||
/// </summary>
|
||||
NoItem
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Evicts action states with expired cooldowns.
|
||||
/// </summary>
|
||||
public sealed class SharedActionSystem : EntitySystem
|
||||
{
|
||||
private const float CooldownCheckIntervalSeconds = 10;
|
||||
private float _timeSinceCooldownCheck;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<ItemActionsComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ItemActionsComponent, EquippedHandEvent>(OnHandEquipped);
|
||||
SubscribeLocalEvent<ItemActionsComponent, UnequippedHandEvent>((uid, comp, _) => OnUnequipped(uid, comp));
|
||||
SubscribeLocalEvent<ItemActionsComponent, GotUnequippedEvent>((uid, comp, _) => OnUnequipped(uid, comp));
|
||||
}
|
||||
|
||||
private void OnGotEquipped(EntityUid uid, ItemActionsComponent component, GotEquippedEvent args)
|
||||
{
|
||||
if (!TryComp(args.Equipee, out SharedActionsComponent? actionsComponent))
|
||||
return;
|
||||
|
||||
component.Holder = args.Equipee;
|
||||
component.HolderActionsComponent = actionsComponent;
|
||||
component.IsEquipped = true;
|
||||
component.GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
private void OnHandEquipped(EntityUid uid, ItemActionsComponent component, EquippedHandEvent args)
|
||||
{
|
||||
if (!TryComp(args.User, out SharedActionsComponent? actionsComponent))
|
||||
return;
|
||||
|
||||
component.Holder = args.User;
|
||||
component.HolderActionsComponent = actionsComponent;
|
||||
component.IsEquipped = true;
|
||||
component.InHand = args.Hand;
|
||||
component.GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
private void OnUnequipped(EntityUid uid, ItemActionsComponent component)
|
||||
{
|
||||
component.RevokeAllFromHolder();
|
||||
component.Holder = null;
|
||||
component.HolderActionsComponent = null;
|
||||
component.IsEquipped = false;
|
||||
component.InHand = null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_timeSinceCooldownCheck += frameTime;
|
||||
if (_timeSinceCooldownCheck < CooldownCheckIntervalSeconds) return;
|
||||
|
||||
foreach (var comp in EntityManager.EntityQuery<SharedActionsComponent>(false))
|
||||
{
|
||||
comp.ExpireCooldowns();
|
||||
}
|
||||
_timeSinceCooldownCheck -= CooldownCheckIntervalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
459
Content.Shared/Actions/SharedActionsSystem.cs
Normal file
459
Content.Shared/Actions/SharedActionsSystem.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
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;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Shared.Actions;
|
||||
|
||||
public abstract class SharedActionsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAdminLogSystem _logSystem = default!;
|
||||
[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!;
|
||||
[Dependency] protected readonly IGameTiming GameTiming = default!;
|
||||
|
||||
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);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentGetStateAttemptEvent>(OnCanGetState);
|
||||
|
||||
SubscribeAllEvent<RequestPerformActionEvent>(OnActionRequest);
|
||||
}
|
||||
|
||||
#region ComponentStateManagement
|
||||
protected virtual void Dirty(ActionType action)
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
private void OnCanGetState(EntityUid uid, ActionsComponent component, ref ComponentGetStateAttemptEvent args)
|
||||
{
|
||||
// Only send action state data to the relevant player.
|
||||
if (args.Player.AttachedEntity != uid)
|
||||
args.Cancelled = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Execution
|
||||
/// <summary>
|
||||
/// When receiving a request to perform an action, this validates whether the action is allowed. If it is, it
|
||||
/// will raise the relevant <see cref="PerformActionEvent"/>
|
||||
/// </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))
|
||||
{
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} attempted to perform an action that they do not have: {ev.Action.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!act.Enabled)
|
||||
return;
|
||||
|
||||
var curTime = GameTiming.CurTime;
|
||||
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
|
||||
return;
|
||||
|
||||
PerformActionEvent? performEvent = null;
|
||||
|
||||
// Validate request by checking action blockers and the like:
|
||||
var name = Loc.GetString(act.Name);
|
||||
|
||||
switch (act)
|
||||
{
|
||||
case EntityTargetAction entityAction:
|
||||
|
||||
if (ev.EntityTarget is not EntityUid { Valid: true } entityTarget)
|
||||
{
|
||||
Logger.Error($"Attempted to perform an entity-targeted action without a target! Action: {entityAction.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_rotateToFaceSystem.TryFaceCoordinates(user, Transform(entityTarget).WorldPosition);
|
||||
|
||||
if (!ValidateEntityTarget(user, entityTarget, entityAction))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {ToPrettyString(entityTarget):target}.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{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:
|
||||
|
||||
if (ev.MapTarget is not MapCoordinates mapTarget)
|
||||
{
|
||||
Logger.Error($"Attempted to perform a map-targeted action without a target! Action: {worldAction.Name}");
|
||||
return;
|
||||
}
|
||||
|
||||
_rotateToFaceSystem.TryFaceCoordinates(user, mapTarget.Position);
|
||||
|
||||
if (!ValidateWorldTarget(user, mapTarget, worldAction))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action targeted at {mapTarget:target}.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action (provided by {ToPrettyString(act.Provider.Value):provider}) targeted at {mapTarget:target}.");
|
||||
|
||||
if (worldAction.Event != null)
|
||||
{
|
||||
worldAction.Event.Target = mapTarget;
|
||||
performEvent = worldAction.Event;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case InstantAction instantAction:
|
||||
|
||||
if (act.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null))
|
||||
return;
|
||||
|
||||
if (act.Provider == null)
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{ToPrettyString(user):user} is performing the {name:action} action.");
|
||||
else
|
||||
_logSystem.Add(LogType.Action,
|
||||
$"{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!
|
||||
PerformAction(component, act, performEvent, curTime);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public bool ValidateWorldTarget(EntityUid user, MapCoordinates coords, WorldTargetAction action)
|
||||
{
|
||||
if (coords == MapCoordinates.Nullspace)
|
||||
return false;
|
||||
|
||||
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);
|
||||
|
||||
if (xform.MapID != coords.MapId)
|
||||
return false;
|
||||
|
||||
if (action.Range <= 0)
|
||||
return true;
|
||||
|
||||
return (xform.WorldPosition - coords.Position).Length <= action.Range;
|
||||
}
|
||||
|
||||
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
|
||||
}
|
||||
|
||||
protected void PerformAction(ActionsComponent component, ActionType action, PerformActionEvent? actionEvent, TimeSpan curTime)
|
||||
{
|
||||
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)
|
||||
RaiseLocalEvent(component.Owner, (object) actionEvent, broadcast: true);
|
||||
else
|
||||
RaiseLocalEvent(action.Provider.Value, (object) actionEvent, broadcast: true);
|
||||
|
||||
handled = actionEvent.Handled;
|
||||
}
|
||||
|
||||
// Execute convenience functionality (pop-ups, sound, speech)
|
||||
handled |= PerformBasicActions(component.Owner, action);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action)
|
||||
{
|
||||
if (action.Sound == null && string.IsNullOrWhiteSpace(action.Popup))
|
||||
return false;
|
||||
|
||||
var filter = Filter.Pvs(performer).RemoveWhereAttachedEntity(e => e == performer);
|
||||
|
||||
if (action.Sound != null)
|
||||
SoundSystem.Play(filter, action.Sound.GetSound(), performer, action.AudioParams);
|
||||
|
||||
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);
|
||||
|
||||
_popupSystem.PopupEntity(msg, performer, filter);
|
||||
|
||||
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)
|
||||
{
|
||||
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.
|
||||
if (dirty)
|
||||
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);
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
AddAction(uid, action, provider, comp, false);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
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();
|
||||
|
||||
RemoveActions(uid, provided, comp);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var ev = new GetActionsEvent();
|
||||
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)
|
||||
{
|
||||
var ev = new GetActionsEvent();
|
||||
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
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
using System;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Movement.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Clothing
|
||||
{
|
||||
[NetworkedComponent()]
|
||||
public abstract class SharedMagbootsComponent : Component
|
||||
{
|
||||
[DataField("toggleAction", required: true)]
|
||||
public InstantAction ToggleAction = new();
|
||||
|
||||
public abstract bool On { get; set; }
|
||||
|
||||
protected void OnChanged()
|
||||
{
|
||||
EntitySystem.Get<SharedActionsSystem>().SetToggled(ToggleAction, On);
|
||||
|
||||
// inventory system will automatically hook into the event raised by this and update accordingly
|
||||
if (Owner.TryGetContainer(out var container))
|
||||
{
|
||||
|
||||
30
Content.Shared/Clothing/SharedMagbootsSystem.cs
Normal file
30
Content.Shared/Clothing/SharedMagbootsSystem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Toggleable;
|
||||
|
||||
namespace Content.Shared.Clothing;
|
||||
|
||||
public abstract class SharedMagbootsSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharedMagbootsComponent, GetActionsEvent>(OnGetActions);
|
||||
SubscribeLocalEvent<SharedMagbootsComponent, ToggleActionEvent>(OnToggleAction);
|
||||
}
|
||||
|
||||
private void OnGetActions(EntityUid uid, SharedMagbootsComponent component, GetActionsEvent args)
|
||||
{
|
||||
args.Actions.Add(component.ToggleAction);
|
||||
}
|
||||
|
||||
private void OnToggleAction(EntityUid uid, SharedMagbootsComponent component, ToggleActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
component.On = !component.On;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Targeting;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.CombatMode
|
||||
{
|
||||
@@ -14,6 +15,50 @@ namespace Content.Shared.CombatMode
|
||||
private bool _isInCombatMode;
|
||||
private TargetingZone _activeZone;
|
||||
|
||||
[DataField("disarmFailChance")]
|
||||
public readonly float DisarmFailChance = 0.4f;
|
||||
|
||||
[DataField("pushChance")]
|
||||
public readonly float DisarmPushChance = 0.4f;
|
||||
|
||||
[DataField("disarmFailSound")]
|
||||
public readonly SoundSpecifier DisarmFailSound = new SoundPathSpecifier("/Audio/Weapons/punchmiss.ogg");
|
||||
|
||||
[DataField("disarmSuccessSound")]
|
||||
public readonly SoundSpecifier DisarmSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
|
||||
|
||||
// These are chonky default definitions for combat actions. But its a pain to add a yaml version of this for
|
||||
// every entity that wants combat mode, especially given that they're currently all identical... so ummm.. yeah.
|
||||
[DataField("disarmAction")]
|
||||
public readonly EntityTargetAction DisarmAction = new()
|
||||
{
|
||||
Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/disarmOff.png")),
|
||||
IconOn = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/disarm.png")),
|
||||
Name = "action-name-disarm",
|
||||
Description = "action-description-disarm",
|
||||
Repeat = true,
|
||||
UseDelay = TimeSpan.FromSeconds(1.5f),
|
||||
InteractOnMiss = true,
|
||||
Event = new DisarmActionEvent(),
|
||||
CanTargetSelf = false,
|
||||
Whitelist = new()
|
||||
{
|
||||
Components = new[] { "Hands", "StatusEffects" },
|
||||
},
|
||||
};
|
||||
|
||||
[DataField("combatToggleAction")]
|
||||
public readonly InstantAction CombatToggleAction = new()
|
||||
{
|
||||
Icon = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harmOff.png")),
|
||||
IconOn = new SpriteSpecifier.Texture(new ResourcePath("Interface/Actions/harm.png")),
|
||||
UserPopup = "action-popup-combat",
|
||||
PopupToggleSuffix = "-disabling",
|
||||
Name = "action-name-combat",
|
||||
Description = "action-description-combat",
|
||||
Event = new ToggleCombatActionEvent(),
|
||||
};
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public virtual bool IsInCombatMode
|
||||
{
|
||||
@@ -22,6 +67,7 @@ namespace Content.Shared.CombatMode
|
||||
{
|
||||
if (_isInCombatMode == value) return;
|
||||
_isInCombatMode = value;
|
||||
EntitySystem.Get<SharedActionsSystem>().SetToggled(CombatToggleAction, _isInCombatMode);
|
||||
Dirty();
|
||||
|
||||
// Regenerate physics contacts -> Can probably just selectively check
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Shared.Actions;
|
||||
|
||||
namespace Content.Shared.CombatMode
|
||||
{
|
||||
public abstract class SharedCombatModeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<CombatModeSystemMessages.SetCombatModeActiveMessage>(CombatModeActiveHandler);
|
||||
SubscribeLocalEvent<CombatModeSystemMessages.SetCombatModeActiveMessage>(CombatModeActiveHandler);
|
||||
|
||||
SubscribeLocalEvent<SharedCombatModeComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<SharedCombatModeComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<SharedCombatModeComponent, ToggleCombatActionEvent>(OnActionPerform);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, SharedCombatModeComponent component, ComponentStartup args)
|
||||
{
|
||||
_actionsSystem.AddAction(uid, component.CombatToggleAction, null);
|
||||
_actionsSystem.AddAction(uid, component.DisarmAction, null);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, SharedCombatModeComponent component, ComponentShutdown args)
|
||||
{
|
||||
_actionsSystem.RemoveAction(uid, component.CombatToggleAction);
|
||||
_actionsSystem.RemoveAction(uid, component.DisarmAction);
|
||||
}
|
||||
|
||||
private void OnActionPerform(EntityUid uid, SharedCombatModeComponent component, ToggleCombatActionEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
component.IsInCombatMode = !component.IsInCombatMode;
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void CombatModeActiveHandler(CombatModeSystemMessages.SetCombatModeActiveMessage ev, EntitySessionEventArgs eventArgs)
|
||||
@@ -24,4 +51,7 @@ namespace Content.Shared.CombatMode
|
||||
combatModeComponent.IsInCombatMode = ev.Active;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ToggleCombatActionEvent : PerformActionEvent { }
|
||||
public sealed class DisarmActionEvent : PerformEntityTargetActionEvent { }
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ namespace Content.Shared.Hands
|
||||
/// Raised when putting an entity into a hand slot
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class EquippedHandEvent : HandledEntityEventArgs
|
||||
public abstract class EquippedHandEvent : HandledEntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity that equipped the item.
|
||||
@@ -133,7 +133,7 @@ namespace Content.Shared.Hands
|
||||
/// Raised when removing an entity from an inventory slot.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class UnequippedHandEvent : HandledEntityEventArgs
|
||||
public abstract class UnequippedHandEvent : HandledEntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity that equipped the item.
|
||||
@@ -157,4 +157,24 @@ namespace Content.Shared.Hands
|
||||
Hand = hand;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GotEquippedHandEvent : EquippedHandEvent
|
||||
{
|
||||
public GotEquippedHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
|
||||
}
|
||||
|
||||
public sealed class GotUnequippedHandEvent : UnequippedHandEvent
|
||||
{
|
||||
public GotUnequippedHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
|
||||
}
|
||||
|
||||
public sealed class DidEquipHandEvent : EquippedHandEvent
|
||||
{
|
||||
public DidEquipHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
|
||||
}
|
||||
|
||||
public sealed class DidUnequipHandEvent : UnequippedHandEvent
|
||||
{
|
||||
public DidUnequipHandEvent(EntityUid user, EntityUid unequipped, Hand hand) : base(user, unequipped, hand) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,10 +92,11 @@ namespace Content.Shared.Hands
|
||||
|
||||
hands.Dirty();
|
||||
|
||||
var unequippedHandMessage = new UnequippedHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(entity, unequippedHandMessage);
|
||||
if (unequippedHandMessage.Handled)
|
||||
return;
|
||||
var gotUnequipped = new GotUnequippedHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(entity, gotUnequipped, false);
|
||||
|
||||
var didUnequip = new DidUnequipHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(uid, didUnequip);
|
||||
|
||||
if (hand.Name == hands.ActiveHand)
|
||||
RaiseLocalEvent(entity, new HandDeselectedEvent(uid, entity), false);
|
||||
@@ -123,11 +124,15 @@ namespace Content.Shared.Hands
|
||||
|
||||
hands.Dirty();
|
||||
|
||||
var equippedHandMessage = new EquippedHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(entity, equippedHandMessage);
|
||||
var didEquip = new DidEquipHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(uid, didEquip, false);
|
||||
|
||||
var gotEquipped = new GotEquippedHandEvent(uid, entity, hand);
|
||||
RaiseLocalEvent(entity, gotEquipped);
|
||||
|
||||
// TODO this should REALLY be a cancellable thing, not a handled event.
|
||||
// If one of the interactions resulted in the item being dropped, return early.
|
||||
if (equippedHandMessage.Handled)
|
||||
if (gotEquipped.Handled)
|
||||
return;
|
||||
|
||||
if (hand.Name == hands.ActiveHand)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -8,6 +7,9 @@ namespace Content.Shared.Light.Component
|
||||
[NetworkedComponent]
|
||||
public abstract class SharedHandheldLightComponent : Robust.Shared.GameObjects.Component
|
||||
{
|
||||
[DataField("toggleAction", required: true)]
|
||||
public InstantAction ToggleAction = default!;
|
||||
|
||||
public const int StatusLevels = 6;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.PAI
|
||||
{
|
||||
@@ -17,6 +16,8 @@ namespace Content.Shared.PAI
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class PAIComponent : Component
|
||||
{
|
||||
[DataField("midiAction", required: true, serverOnly: true)] // server only, as it uses a server-BUI event !type
|
||||
public InstantAction? MidiAction;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
Content.Shared/Toggleable/ToggleActionEvent.cs
Normal file
8
Content.Shared/Toggleable/ToggleActionEvent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Actions;
|
||||
|
||||
namespace Content.Shared.Toggleable;
|
||||
|
||||
/// <summary>
|
||||
/// Generic action-event for toggle-able components.
|
||||
/// </summary>
|
||||
public sealed class ToggleActionEvent : PerformActionEvent { }
|
||||
@@ -25,6 +25,7 @@ namespace Content.Shared.Whitelist
|
||||
/// - AsteroidRock
|
||||
/// </code>
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class EntityWhitelist : ISerializationHooks
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,6 +33,7 @@ namespace Content.Shared.Whitelist
|
||||
/// </summary>
|
||||
[DataField("components")] public string[]? Components = null;
|
||||
|
||||
[NonSerialized]
|
||||
private List<IComponentRegistration>? _registrations = null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
hud-combat-enabled = Combat mode enabled!
|
||||
hud-combat-disabled = Combat mode disabled.
|
||||
action-name-combat = [color=red]Combat Mode[/color]
|
||||
action-description-combat = Enter combat mode.
|
||||
|
||||
|
||||
action-popup-combat = Combat mode enabled!
|
||||
action-popup-combat-disabling = Combat mode disabled.
|
||||
@@ -1,2 +1,5 @@
|
||||
disarm-action-popup-message-other-clients = {$performerName} fails to disarm {$targetName}!
|
||||
disarm-action-popup-message-cursor = You fail to disarm {$targetName}!
|
||||
|
||||
action-name-disarm = [color=red]Disarm[/color]
|
||||
action-description-disarm = Attempt to [color=red]disarm[/color] someone.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user