Actions System + UI (#2710)

Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
chairbender
2020-12-13 14:28:20 -08:00
committed by GitHub
parent fd0df9a00a
commit 7a3c281f60
150 changed files with 7283 additions and 854 deletions

View File

@@ -12,6 +12,7 @@ using Content.Client.UserInterface;
using Content.Client.UserInterface.AdminMenu; using Content.Client.UserInterface.AdminMenu;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility; using Content.Client.Utility;
using Content.Shared.Actions;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Alert; using Content.Shared.Alert;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -39,6 +40,7 @@ namespace Content.Client
IoCManager.Register<IStationEventManager, StationEventManager>(); IoCManager.Register<IStationEventManager, StationEventManager>();
IoCManager.Register<IAdminMenuManager, AdminMenuManager>(); IoCManager.Register<IAdminMenuManager, AdminMenuManager>();
IoCManager.Register<AlertManager, AlertManager>(); IoCManager.Register<AlertManager, AlertManager>();
IoCManager.Register<ActionManager, ActionManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>(); IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>(); IoCManager.Register<EuiManager, EuiManager>();
} }

View File

@@ -13,6 +13,7 @@ using Content.Client.StationEvents;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Client.UserInterface.AdminMenu; using Content.Client.UserInterface.AdminMenu;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Cargo; using Content.Shared.GameObjects.Components.Cargo;
using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Chemistry;
@@ -157,6 +158,7 @@ namespace Content.Client
IoCManager.Resolve<IAdminMenuManager>().Initialize(); IoCManager.Resolve<IAdminMenuManager>().Initialize();
IoCManager.Resolve<EuiManager>().Initialize(); IoCManager.Resolve<EuiManager>().Initialize();
IoCManager.Resolve<AlertManager>().Initialize(); IoCManager.Resolve<AlertManager>().Initialize();
IoCManager.Resolve<ActionManager>().Initialize();
_baseClient.RunLevelChanged += (sender, args) => _baseClient.RunLevelChanged += (sender, args) =>
{ {

View File

@@ -20,10 +20,13 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
/// A character UI which shows items the user has equipped within his inventory /// A character UI which shows items the user has equipped within his inventory
/// </summary> /// </summary>
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(SharedInventoryComponent))]
public class ClientInventoryComponent : SharedInventoryComponent public class ClientInventoryComponent : SharedInventoryComponent
{ {
private readonly Dictionary<Slots, IEntity> _slots = new(); private readonly Dictionary<Slots, IEntity> _slots = new();
public IReadOnlyDictionary<Slots, IEntity> AllSlots => _slots;
[ViewVariables] public InventoryInterfaceController InterfaceController { get; private set; } = default!; [ViewVariables] public InventoryInterfaceController InterfaceController { get; private set; } = default!;
private ISpriteComponent? _sprite; private ISpriteComponent? _sprite;
@@ -70,6 +73,11 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
} }
} }
public override bool IsEquipped(IEntity item)
{
return item != null && _slots.Values.Any(e => e == item);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{ {
base.HandleComponentState(curState, nextState); base.HandleComponentState(curState, nextState);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Client.Utility; using Content.Client.Utility;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -84,6 +85,16 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
public override SS14Window Window => _window; public override SS14Window Window => _window;
private HumanInventoryWindow _window; private HumanInventoryWindow _window;
public override IEnumerable<ItemSlotButton> GetItemSlotButtons(Slots slot)
{
if (!_inventoryButtons.TryGetValue(slot, out var buttons))
{
return Enumerable.Empty<ItemSlotButton>();
}
return buttons;
}
public override void AddToSlot(Slots slot, IEntity entity) public override void AddToSlot(Slots slot, IEntity entity)
{ {
base.AddToSlot(slot, entity); base.AddToSlot(slot, entity);

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.Input; using Content.Shared.Input;
@@ -53,6 +54,10 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
{ {
} }
/// <returns>the button controls associated with the
/// specified slot, if any. Empty if none.</returns>
public abstract IEnumerable<ItemSlotButton> GetItemSlotButtons(EquipmentSlotDefines.Slots slot);
public virtual void AddToSlot(EquipmentSlotDefines.Slots slot, IEntity entity) public virtual void AddToSlot(EquipmentSlotDefines.Slots slot, IEntity entity)
{ {
} }

View File

@@ -15,6 +15,7 @@ namespace Content.Client.GameObjects.Components.Items
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(ISharedHandsComponent))] [ComponentReference(typeof(ISharedHandsComponent))]
[ComponentReference(typeof(SharedHandsComponent))]
public class HandsComponent : SharedHandsComponent public class HandsComponent : SharedHandsComponent
{ {
[Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly IGameHud _gameHud = default!;
@@ -31,6 +32,18 @@ namespace Content.Client.GameObjects.Components.Items
[ViewVariables] public IEntity? ActiveHand => GetEntity(ActiveIndex); [ViewVariables] public IEntity? ActiveHand => GetEntity(ActiveIndex);
public override bool IsHolding(IEntity entity)
{
foreach (var hand in _hands)
{
if (hand.Entity == entity)
{
return true;
}
}
return false;
}
private void AddHand(Hand hand) private void AddHand(Hand hand)
{ {
_sprite?.LayerMapReserveBlank($"hand-{hand.Name}"); _sprite?.LayerMapReserveBlank($"hand-{hand.Name}");

View File

@@ -0,0 +1,90 @@
using System;
using Content.Shared.Actions;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.Actions
{
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
}
}

View File

@@ -0,0 +1,304 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects;
namespace Content.Client.GameObjects.Components.Mobs.Actions
{
/// <summary>
/// Tracks and manages the hotbar assignments for actions.
/// </summary>
public class ActionAssignments
{
// the slots and assignments fields hold client's assignments (what action goes in what slot),
// which are completely client side and independent of what actions they've actually been granted and
// what item the action is actually for.
/// <summary>
/// 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;
/// <summary>
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
/// slot index 9 corresponds to the one labeled "0"). The key corresponds to an index in the _slots array.
/// The value is a list because actions can be assigned to multiple slots. Even if an action type has not been granted,
/// 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;
/// <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();
private readonly byte _numHotbars;
private readonly byte _numSlots;
public ActionAssignments(byte numHotbars, byte numSlots)
{
_numHotbars = numHotbars;
_numSlots = numSlots;
_assignments = new Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>>();
_slots = new ActionAssignment?[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)
{
// 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)
{
var assignment = ActionAssignment.For(actionState.Key);
if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment))
{
AutoPopulate(assignment, currentHotbar, false);
}
}
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;
AssignSlot(hotbar, slot,
ActionAssignment.For(actionType));
}
}
// 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);
}
}
/// <summary>
/// Assigns the indicated hotbar slot to the specified action type.
/// </summary>
/// <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)
{
ClearSlot(hotbar, slot, false);
_slots[hotbar, slot] = actionType;
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;
}
}
/// <summary>
/// Clear the assignment from the indicated slot.
/// </summary>
/// <param name="hotbar">hotbar whose slot is being cleared</param>
/// <param name="slot">slot of the hotbar to clear (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
/// <param name="preventAutoPopulate">if true, the action assigned to this slot
/// will be prevented from being auto-populated in the future when it is newly granted.
/// Item actions will automatically be allowed to auto populate again
/// when their associated item are unequipped. This ensures that items that are newly
/// picked up will always present their actions to the user even if they had earlier been cleared.
/// </param>
public void ClearSlot(byte hotbar, byte slot, bool preventAutoPopulate)
{
// remove this particular assignment from our data structures
// (keeping in mind something can be assigned multiple slots)
var currentAction = _slots[hotbar, slot];
if (!currentAction.HasValue) return;
if (preventAutoPopulate)
{
var assignment = currentAction.Value;
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];
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
if (assignmentList.Count == 0)
{
_assignments.Remove(currentAction.Value);
}
else
{
_assignments[currentAction.Value] = assignmentList;
}
_slots[hotbar, slot] = null;
}
/// <summary>
/// Finds the next open slot the action can go in and assigns it there,
/// starting from the currently selected hotbar.
/// Does not update any UI elements, only updates the assignment data structures.
/// </summary>
/// <param name="force">if true, will force the assignment to occur
/// 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)
{
if (ShouldPreventAutoPopulate(toAssign, force)) 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++)
{
for (byte slot = 0; slot < _numSlots; slot++)
{
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;
}
continue;
}
// slot's empty, assign
AssignSlot(hotbar, slot, toAssign);
return;
}
}
// 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];
/// <returns>true if we have the assignment assigned to some slot</returns>
public bool HasAssignment(ActionAssignment assignment)
{
return _assignments.ContainsKey(assignment);
}
}
}

View File

@@ -0,0 +1,273 @@
#nullable enable
using System.Collections.Generic;
using Content.Client.GameObjects.Components.HUD.Inventory;
using Content.Client.GameObjects.Components.Items;
using Content.Client.GameObjects.Components.Mobs.Actions;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects;
using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Input.Binding;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Mobs
{
/// <inheritdoc/>
[RegisterComponent]
[ComponentReference(typeof(SharedActionsComponent))]
public sealed class ClientActionsComponent : SharedActionsComponent
{
public const byte Hotbars = 10;
public const byte Slots = 10;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[ComponentDependency] private readonly HandsComponent? _handsComponent = null;
[ComponentDependency] private readonly ClientInventoryComponent? _inventoryComponent = null;
private ActionsUI? _ui;
private readonly List<ItemSlotButton> _highlightingItemSlots = new();
/// <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 HandleMessage(ComponentMessage message, IComponent? component)
{
base.HandleMessage(message, component);
switch (message)
{
case PlayerAttachedMsg _:
PlayerAttached();
break;
case PlayerDetachedMsg _:
PlayerDetached();
break;
}
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not ActionComponentState)
{
return;
}
UpdateUI();
}
private void PlayerAttached()
{
if (!CurrentlyControlled || _ui != null)
{
return;
}
_ui = new ActionsUI(this);
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
UpdateUI();
}
private 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);
}
/// <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.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
SendNetworkMessage(attempt.PerformInstantActionMessage());
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;
SendNetworkMessage(attempt.PerformToggleActionMessage(!actionState.ToggledOn));
}
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.BehaviorType != BehaviorType.TargetEntity &&
_ui.SelectingTargetFor.Action.BehaviorType != BehaviorType.TargetPoint)) 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
SendNetworkMessage(attempt.PerformTargetPointActionMessage(args));
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
SendNetworkMessage(attempt.PerformTargetEntityActionMessage(args));
if (!attempt.Action.Repeat)
{
_ui.StopTargeting();
}
return true;
}
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(IEntity item)
{
StopHighlightingItemSlots();
// figure out if it's in hand or inventory and highlight it
foreach (var hand in _handsComponent!.Hands)
{
if (hand.Entity != item || hand.Button == null) continue;
_highlightingItemSlots.Add(hand.Button);
hand.Button.Highlight(true);
return;
}
foreach (var (slot, slotItem) in _inventoryComponent!.AllSlots)
{
if (slotItem != item) continue;
foreach (var itemSlotButton in
_inventoryComponent.InterfaceController.GetItemSlotButtons(slot))
{
_highlightingItemSlots.Add(itemSlotButton);
itemSlotButton.Highlight(true);
}
return;
}
}
/// <summary>
/// Stops highlighting any item slots we are currently highlighting.
/// </summary>
public void StopHighlightingItemSlots()
{
foreach (var itemSlot in _highlightingItemSlots)
{
itemSlot.Highlight(false);
}
_highlightingItemSlots.Clear();
}
public void ToggleActionsMenu()
{
_ui?.ToggleActionsMenu();
}
}
}

View File

@@ -1,25 +1,20 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Controls;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.Interfaces.UserInterface; using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Mobs namespace Content.Client.GameObjects.Components.Mobs
@@ -29,19 +24,11 @@ namespace Content.Client.GameObjects.Components.Mobs
[ComponentReference(typeof(SharedAlertsComponent))] [ComponentReference(typeof(SharedAlertsComponent))]
public sealed class ClientAlertsComponent : SharedAlertsComponent public sealed class ClientAlertsComponent : SharedAlertsComponent
{ {
private static readonly float TooltipTextMaxWidth = 265;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private AlertsUI _ui; private AlertsUI _ui;
private PanelContainer _tooltip;
private RichTextLabel _stateName;
private RichTextLabel _stateDescription;
private RichTextLabel _stateCooldown;
private AlertOrderPrototype _alertOrder; private AlertOrderPrototype _alertOrder;
private bool _tooltipReady;
[ViewVariables] [ViewVariables]
private readonly Dictionary<AlertKey, AlertControl> _alertControls private readonly Dictionary<AlertKey, AlertControl> _alertControls
@@ -49,7 +36,6 @@ namespace Content.Client.GameObjects.Components.Mobs
/// <summary> /// <summary>
/// Allows calculating if we need to act due to this component being controlled by the current mob /// Allows calculating if we need to act due to this component being controlled by the current mob
/// TODO: should be revisited after space-wizards/RobustToolbox#1255
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner; private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
@@ -78,14 +64,11 @@ namespace Content.Client.GameObjects.Components.Mobs
{ {
base.HandleComponentState(curState, nextState); base.HandleComponentState(curState, nextState);
if (curState is not AlertsComponentState state) if (curState is not AlertsComponentState)
{ {
return; return;
} }
// update the dict of states based on the array we got in the message
SetAlerts(state.Alerts);
UpdateAlertsControls(); UpdateAlertsControls();
} }
@@ -102,48 +85,24 @@ namespace Content.Client.GameObjects.Components.Mobs
Logger.ErrorS("alert", "no alertOrder prototype found, alerts will be in random order"); Logger.ErrorS("alert", "no alertOrder prototype found, alerts will be in random order");
} }
_ui = new AlertsUI(IoCManager.Resolve<IClyde>()); _ui = new AlertsUI();
var uiManager = IoCManager.Resolve<IUserInterfaceManager>(); IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
uiManager.StateRoot.AddChild(_ui);
_tooltip = new PanelContainer
{
Visible = false,
StyleClasses = { StyleNano.StyleClassTooltipPanel }
};
var tooltipVBox = new VBoxContainer
{
RectClipContent = true
};
_tooltip.AddChild(tooltipVBox);
_stateName = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertTitle }
};
tooltipVBox.AddChild(_stateName);
_stateDescription = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertDescription }
};
tooltipVBox.AddChild(_stateDescription);
_stateCooldown = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = { StyleNano.StyleClassTooltipAlertCooldown }
};
tooltipVBox.AddChild(_stateCooldown);
uiManager.PopupRoot.AddChild(_tooltip);
UpdateAlertsControls(); UpdateAlertsControls();
} }
private void PlayerDetached() private void PlayerDetached()
{ {
_ui?.Dispose(); foreach (var alertControl in _alertControls.Values)
{
alertControl.OnPressed -= AlertControlOnPressed;
}
if (_ui != null)
{
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.RemoveChild(_ui);
_ui = null; _ui = null;
}
_alertControls.Clear(); _alertControls.Clear();
} }
@@ -168,39 +127,49 @@ namespace Content.Client.GameObjects.Components.Mobs
toRemove.Add(existingKey); toRemove.Add(existingKey);
} }
} }
foreach (var alertKeyToRemove in toRemove) foreach (var alertKeyToRemove in toRemove)
{ {
// remove and dispose the control
_alertControls.Remove(alertKeyToRemove, out var control); _alertControls.Remove(alertKeyToRemove, out var control);
control?.Dispose(); if (control == null) return;
_ui.Grid.Children.Remove(control);
} }
// now we know that alertControls contains alerts that should still exist but // now we know that alertControls contains alerts that should still exist but
// may need to updated, // may need to updated,
// also there may be some new alerts we need to show. // also there may be some new alerts we need to show.
// further, we need to ensure they are ordered w.r.t their configured order // further, we need to ensure they are ordered w.r.t their configured order
foreach (var alertStatus in EnumerateAlertStates()) foreach (var (alertKey, alertState) in EnumerateAlertStates())
{ {
if (!AlertManager.TryDecode(alertStatus.AlertEncoded, out var newAlert)) if (!alertKey.AlertType.HasValue)
{ {
Logger.ErrorS("alert", "Unable to decode alert {0}", alertStatus.AlertEncoded); Logger.WarningS("alert", "found alertkey without alerttype," +
" alert keys should never be stored without an alerttype set: {0}", alertKey);
continue;
}
var alertType = alertKey.AlertType.Value;
if (!AlertManager.TryGet(alertType, out var newAlert))
{
Logger.ErrorS("alert", "Unrecognized alertType {0}", alertType);
continue; continue;
} }
if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) && if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
existingAlertControl.Alert.AlertType == newAlert.AlertType) existingAlertControl.Alert.AlertType == newAlert.AlertType)
{ {
// id is the same, simply update the existing control severity // key is the same, simply update the existing control severity / cooldown
existingAlertControl.SetSeverity(alertStatus.Severity); existingAlertControl.SetSeverity(alertState.Severity);
existingAlertControl.Cooldown = alertState.Cooldown;
} }
else else
{ {
existingAlertControl?.Dispose(); if (existingAlertControl != null)
{
_ui.Grid.Children.Remove(existingAlertControl);
}
// this is a new alert + alert key or just a different alert with the same // this is a new alert + alert key or just a different alert with the same
// key, create the control and add it in the appropriate order // key, create the control and add it in the appropriate order
var newAlertControl = CreateAlertControl(newAlert, alertStatus); var newAlertControl = CreateAlertControl(newAlert, alertState);
if (_alertOrder != null) if (_alertOrder != null)
{ {
var added = false; var added = false;
@@ -233,14 +202,11 @@ namespace Content.Client.GameObjects.Components.Mobs
private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState) private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
{ {
var alertControl = new AlertControl(alert, alertState.Severity, _resourceCache)
var alertControl = new AlertControl(alert, alertState.Severity, _resourceCache); {
// show custom tooltip for the status control Cooldown = alertState.Cooldown
alertControl.OnShowTooltip += AlertOnOnShowTooltip; };
alertControl.OnHideTooltip += AlertOnOnHideTooltip;
alertControl.OnPressed += AlertControlOnPressed; alertControl.OnPressed += AlertControlOnPressed;
return alertControl; return alertControl;
} }
@@ -249,36 +215,6 @@ namespace Content.Client.GameObjects.Components.Mobs
AlertPressed(args, args.Button as AlertControl); AlertPressed(args, args.Button as AlertControl);
} }
private void AlertOnOnHideTooltip(object sender, EventArgs e)
{
_tooltipReady = false;
_tooltip.Visible = false;
}
private void AlertOnOnShowTooltip(object sender, EventArgs e)
{
var alertControl = (AlertControl) sender;
_stateName.SetMessage(alertControl.Alert.Name);
_stateDescription.SetMessage(alertControl.Alert.Description);
// check for a cooldown
if (alertControl.TotalDuration != null && alertControl.TotalDuration > 0)
{
_stateCooldown.SetMessage(FormattedMessage.FromMarkup("[color=#776a6a]" +
alertControl.TotalDuration +
" sec cooldown[/color]"));
_stateCooldown.Visible = true;
}
else
{
_stateCooldown.Visible = false;
}
// TODO: Text display of cooldown
Tooltips.PositionTooltip(_tooltip);
// if we set it visible here the size of the previous tooltip will flicker for a frame,
// so instead we wait until FrameUpdate to make it visible
_tooltipReady = true;
}
private void AlertPressed(BaseButton.ButtonEventArgs args, AlertControl alert) private void AlertPressed(BaseButton.ButtonEventArgs args, AlertControl alert)
{ {
if (args.Event.Function != EngineKeyFunctions.UIClick) if (args.Event.Function != EngineKeyFunctions.UIClick)
@@ -286,57 +222,17 @@ namespace Content.Client.GameObjects.Components.Mobs
return; return;
} }
if (AlertManager.TryEncode(alert.Alert, out var encoded)) SendNetworkMessage(new ClickAlertMessage(alert.Alert.AlertType));
{
SendNetworkMessage(new ClickAlertMessage(encoded));
}
else
{
Logger.ErrorS("alert", "unable to encode alert {0}", alert.Alert.AlertType);
} }
} protected override void AfterShowAlert()
public void FrameUpdate(float frameTime)
{ {
if (_tooltipReady) UpdateAlertsControls();
{
_tooltipReady = false;
_tooltip.Visible = true;
}
foreach (var (alertKey, alertControl) in _alertControls)
{
// reconcile all alert controls with their current cooldowns
if (TryGetAlertState(alertKey, out var alertState))
{
alertControl.UpdateCooldown(alertState.Cooldown, _gameTiming.CurTime);
}
else
{
Logger.WarningS("alert", "coding error - no alert state for alert {0} " +
"even though we had an AlertControl for it, this" +
" should never happen", alertControl.Alert.AlertType);
}
}
} }
protected override void AfterClearAlert() protected override void AfterClearAlert()
{ {
UpdateAlertsControls(); UpdateAlertsControls();
} }
public override void OnRemove()
{
base.OnRemove();
foreach (var alertControl in _alertControls.Values)
{
alertControl.OnShowTooltip -= AlertOnOnShowTooltip;
alertControl.OnHideTooltip -= AlertOnOnHideTooltip;
alertControl.OnPressed -= AlertControlOnPressed;
}
}
} }
} }

View File

@@ -0,0 +1,91 @@
using Content.Client.GameObjects.Components.Mobs;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.IoC;
namespace Content.Client.GameObjects.EntitySystems
{
[UsedImplicitly]
public class ActionsSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override void Initialize()
{
base.Initialize();
// set up hotkeys for hotbar
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenActionsMenu,
InputCmdHandler.FromDelegate(_ => ToggleActionsMenu()))
.Bind(ContentKeyFunctions.Hotbar1,
HandleHotbarKeybind(0))
.Bind(ContentKeyFunctions.Hotbar2,
HandleHotbarKeybind(1))
.Bind(ContentKeyFunctions.Hotbar3,
HandleHotbarKeybind(2))
.Bind(ContentKeyFunctions.Hotbar4,
HandleHotbarKeybind(3))
.Bind(ContentKeyFunctions.Hotbar5,
HandleHotbarKeybind(4))
.Bind(ContentKeyFunctions.Hotbar6,
HandleHotbarKeybind(5))
.Bind(ContentKeyFunctions.Hotbar7,
HandleHotbarKeybind(6))
.Bind(ContentKeyFunctions.Hotbar8,
HandleHotbarKeybind(7))
.Bind(ContentKeyFunctions.Hotbar9,
HandleHotbarKeybind(8))
.Bind(ContentKeyFunctions.Hotbar0,
HandleHotbarKeybind(9))
// 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),
typeof(ConstructionSystem), typeof(DragDropSystem))
.Register<ActionsSystem>();
}
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<ActionsSystem>();
}
private PointerInputCmdHandler HandleHotbarKeybind(byte slot)
{
// delegate to the ActionsUI, simulating a click on it
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
{
var playerEntity = _playerManager.LocalPlayer.ControlledEntity;
if (playerEntity == null ||
!playerEntity.TryGetComponent<ClientActionsComponent>( out var actionsComponent)) return false;
actionsComponent.HandleHotbarKeybind(slot, args);
return true;
},
false);
}
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
var playerEntity = _playerManager.LocalPlayer.ControlledEntity;
if (playerEntity == null ||
!playerEntity.TryGetComponent<ClientActionsComponent>( out var actionsComponent)) return false;
return actionsComponent.TargetingOnUse(args);
}
private void ToggleActionsMenu()
{
var playerEntity = _playerManager.LocalPlayer.ControlledEntity;
if (playerEntity == null ||
!playerEntity.TryGetComponent<ClientActionsComponent>( out var actionsComponent)) return;
actionsComponent.ToggleActionsMenu();
}
}
}

View File

@@ -1,27 +0,0 @@
using Content.Client.GameObjects.Components.Mobs;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
namespace Content.Client.GameObjects.EntitySystems
{
[UsedImplicitly]
public class AlertsSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
if (!_gameTiming.IsFirstTimePredicted)
return;
foreach (var clientAlertsComponent in EntityManager.ComponentManager.EntityQuery<ClientAlertsComponent>())
{
clientAlertsComponent.FrameUpdate(frameTime);
}
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Content.Client.State; using Content.Client.State;
using Content.Client.Utility;
using Content.Shared.GameObjects; using Content.Shared.GameObjects;
using Content.Shared.GameObjects.EntitySystemMessages; using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
@@ -10,7 +11,6 @@ using Robust.Client.GameObjects;
using Robust.Client.GameObjects.EntitySystems; using Robust.Client.GameObjects.EntitySystems;
using Robust.Client.Graphics.Shaders; using Robust.Client.Graphics.Shaders;
using Robust.Client.Interfaces.Graphics.ClientEye; using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.Input;
using Robust.Client.Interfaces.State; using Robust.Client.Interfaces.State;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Input; using Robust.Shared.Input;
@@ -18,7 +18,6 @@ using Robust.Shared.Input.Binding;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Client.GameObjects.EntitySystems namespace Content.Client.GameObjects.EntitySystems
@@ -30,12 +29,9 @@ namespace Content.Client.GameObjects.EntitySystems
public class DragDropSystem : EntitySystem public class DragDropSystem : EntitySystem
{ {
[Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// drag will be triggered when mouse leaves this deadzone around the click position.
private const float DragDeadzone = 2f;
// how often to recheck possible targets (prevents calling expensive // how often to recheck possible targets (prevents calling expensive
// check logic each update) // check logic each update)
private const float TargetRecheckInterval = 0.25f; private const float TargetRecheckInterval = 0.25f;
@@ -50,14 +46,10 @@ namespace Content.Client.GameObjects.EntitySystems
// entity performing the drag action // entity performing the drag action
private IEntity _dragger; private IEntity _dragger;
private IEntity _draggedEntity;
private readonly List<IDraggable> _draggables = new(); private readonly List<IDraggable> _draggables = new();
private IEntity _dragShadow; private IEntity _dragShadow;
private DragState _state;
// time since mouse down over the dragged entity // time since mouse down over the dragged entity
private float _mouseDownTime; private float _mouseDownTime;
// screen pos where the mouse down began
private Vector2 _mouseDownScreenPos;
// how much time since last recheck of all possible targets // how much time since last recheck of all possible targets
private float _targetRecheckTime; private float _targetRecheckTime;
// reserved initial mousedown event so we can replay it if no drag ends up being performed // reserved initial mousedown event so we can replay it if no drag ends up being performed
@@ -66,6 +58,8 @@ namespace Content.Client.GameObjects.EntitySystems
// can ignore any events sent to this system // can ignore any events sent to this system
private bool _isReplaying; private bool _isReplaying;
private DragDropHelper<IEntity> _dragDropHelper;
private ShaderInstance _dropTargetInRangeShader; private ShaderInstance _dropTargetInRangeShader;
private ShaderInstance _dropTargetOutOfRangeShader; private ShaderInstance _dropTargetOutOfRangeShader;
private SharedInteractionSystem _interactionSystem; private SharedInteractionSystem _interactionSystem;
@@ -73,20 +67,9 @@ namespace Content.Client.GameObjects.EntitySystems
private readonly List<SpriteComponent> _highlightedSprites = new(); private readonly List<SpriteComponent> _highlightedSprites = new();
private enum DragState : byte
{
NotDragging,
// not dragging yet, waiting to see
// if they hold for long enough
MouseDown,
// currently dragging something
Dragging,
}
public override void Initialize() public override void Initialize()
{ {
_state = DragState.NotDragging; _dragDropHelper = new DragDropHelper<IEntity>(OnBeginDrag, OnContinueDrag, OnEndDrag);
_dropTargetInRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetInRange).Instance(); _dropTargetInRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetInRange).Instance();
_dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance(); _dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance();
@@ -101,7 +84,7 @@ namespace Content.Client.GameObjects.EntitySystems
public override void Shutdown() public override void Shutdown()
{ {
CancelDrag(false, null); _dragDropHelper.EndDrag();
CommandBinds.Unregister<DragDropSystem>(); CommandBinds.Unregister<DragDropSystem>();
base.Shutdown(); base.Shutdown();
} }
@@ -132,7 +115,7 @@ namespace Content.Client.GameObjects.EntitySystems
var dragger = args.Session.AttachedEntity; var dragger = args.Session.AttachedEntity;
// cancel any current dragging if there is one (shouldn't be because they would've had to have lifted // cancel any current dragging if there is one (shouldn't be because they would've had to have lifted
// the mouse, canceling the drag, but just being cautious) // the mouse, canceling the drag, but just being cautious)
CancelDrag(false, null); _dragDropHelper.EndDrag();
// possibly initiating a drag // possibly initiating a drag
// check if the clicked entity is draggable // check if the clicked entity is draggable
@@ -150,19 +133,21 @@ namespace Content.Client.GameObjects.EntitySystems
var dragEventArgs = new StartDragDropEventArgs(args.Session.AttachedEntity, entity); var dragEventArgs = new StartDragDropEventArgs(args.Session.AttachedEntity, entity);
if (draggable.CanStartDrag(dragEventArgs)) if (draggable.CanStartDrag(dragEventArgs))
{ {
// wait to initiate a drag
_dragger = dragger;
_draggedEntity = entity;
_draggables.Add(draggable); _draggables.Add(draggable);
canDrag = true;
}
}
if (canDrag)
{
// wait to initiate a drag
_dragDropHelper.MouseDown(entity);
_dragger = dragger;
_mouseDownTime = 0; _mouseDownTime = 0;
_state = DragState.MouseDown;
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
// don't want anything else to process the click, // don't want anything else to process the click,
// but we will save the event so we can "re-play" it if this drag does // but we will save the event so we can "re-play" it if this drag does
// not turn into an actual drag so the click can be handled normally // not turn into an actual drag so the click can be handled normally
_savedMouseDown = args; _savedMouseDown = args;
canDrag = true;
}
} }
return canDrag; return canDrag;
@@ -171,75 +156,20 @@ namespace Content.Client.GameObjects.EntitySystems
return false; return false;
} }
private bool OnUseMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (_state == DragState.MouseDown)
{
// quick mouseup, definitely treat it as a normal click by
// replaying the original
CancelDrag(true, args.OriginalMessage);
return false;
}
if (_state != DragState.Dragging) return false;
// remaining CancelDrag calls will not replay the click because private bool OnBeginDrag()
// by this time we've determined the input was actually a drag attempt
// tell the server we are dropping if we are over a valid drop target in range.
// We don't use args.EntityUid here because drag interactions generally should
// work even if there's something "on top" of the drop target
if (!_interactionSystem.InRangeUnobstructed(_dragger,
args.Coordinates, ignoreInsideBlocker: true))
{ {
CancelDrag(false, null); if (_dragDropHelper.Dragged == null || _dragDropHelper.Dragged.Deleted)
{
// something happened to the clicked entity or we moved the mouse off the target so
// we shouldn't replay the original click
return false; return false;
} }
var entities = GameScreenBase.GetEntitiesUnderPosition(_stateManager, args.Coordinates); if (_dragDropHelper.Dragged.TryGetComponent<SpriteComponent>(out var draggedSprite))
foreach (var entity in entities)
{ {
// check if it's able to be dropped on by current dragged entity
var dropArgs = new DragDropEventArgs(_dragger, args.Coordinates, _draggedEntity, entity);
foreach (var draggable in _draggables)
{
if (!draggable.CanDrop(dropArgs))
{
continue;
}
// tell the server about the drop attempt
RaiseNetworkEvent(new DragDropMessage(args.Coordinates, _draggedEntity.Uid,
entity.Uid));
draggable.Drop(dropArgs);
CancelDrag(false, null);
return true;
}
}
CancelDrag(false, null);
return false;
}
private void StartDragging()
{
// this is checked elsewhere but adding this as a failsafe
if (_draggedEntity == null || _draggedEntity.Deleted)
{
Logger.Error("Programming error. Cannot initiate drag, no dragged entity or entity" +
" was deleted.");
return;
}
if (_draggedEntity.TryGetComponent<SpriteComponent>(out var draggedSprite))
{
_state = DragState.Dragging;
// pop up drag shadow under mouse // pop up drag shadow under mouse
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition); var mousePos = _eyeManager.ScreenToMap(_dragDropHelper.MouseScreenPosition);
_dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos); _dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos);
var dragSprite = _dragShadow.GetComponent<SpriteComponent>(); var dragSprite = _dragShadow.GetComponent<SpriteComponent>();
dragSprite.CopyFrom(draggedSprite); dragSprite.CopyFrom(draggedSprite);
@@ -249,22 +179,132 @@ namespace Content.Client.GameObjects.EntitySystems
dragSprite.DrawDepth = (int) DrawDepth.Overlays; dragSprite.DrawDepth = (int) DrawDepth.Overlays;
if (dragSprite.Directional) if (dragSprite.Directional)
{ {
_dragShadow.Transform.WorldRotation = _draggedEntity.Transform.WorldRotation; _dragShadow.Transform.WorldRotation = _dragDropHelper.Dragged.Transform.WorldRotation;
} }
HighlightTargets(); HighlightTargets();
// drag initiated
return true;
} }
else
{
Logger.Warning("Unable to display drag shadow for {0} because it" + Logger.Warning("Unable to display drag shadow for {0} because it" +
" has no sprite component.", _draggedEntity.Name); " has no sprite component.", _dragDropHelper.Dragged.Name);
return false;
} }
private bool OnContinueDrag(float frameTime)
{
if (_dragDropHelper.Dragged == null || _dragDropHelper.Dragged.Deleted)
{
return false;
}
// still in range of the thing we are dragging?
if (!_interactionSystem.InRangeUnobstructed(_dragger, _dragDropHelper.Dragged))
{
return false;
}
// keep dragged entity under mouse
var mousePos = _eyeManager.ScreenToMap(_dragDropHelper.MouseScreenPosition);
// TODO: would use MapPosition instead if it had a setter, but it has no setter.
// is that intentional, or should we add a setter for Transform.MapPosition?
_dragShadow.Transform.WorldPosition = mousePos.Position;
_targetRecheckTime += frameTime;
if (_targetRecheckTime > TargetRecheckInterval)
{
HighlightTargets();
_targetRecheckTime = 0;
}
return true;
}
private void OnEndDrag()
{
RemoveHighlights();
if (_dragShadow != null)
{
EntityManager.DeleteEntity(_dragShadow);
}
_dragShadow = null;
_draggables.Clear();
_dragger = null;
_mouseDownTime = 0;
_savedMouseDown = null;
}
private bool OnUseMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (!_dragDropHelper.IsDragging)
{
// haven't started the drag yet, quick mouseup, definitely treat it as a normal click by
// replaying the original cmd
if (_savedMouseDown.HasValue && _mouseDownTime < MaxMouseDownTimeForReplayingClick)
{
var savedValue = _savedMouseDown.Value;
_isReplaying = true;
// adjust the timing info based on the current tick so it appears as if it happened now
var replayMsg = savedValue.OriginalMessage;
var adjustedInputMsg = new FullInputCmdMessage(args.OriginalMessage.Tick, args.OriginalMessage.SubTick,
replayMsg.InputFunctionId, replayMsg.State, replayMsg.Coordinates, replayMsg.ScreenCoordinates, replayMsg.Uid);
_inputSystem.HandleInputCommand(savedValue.Session, EngineKeyFunctions.Use,
adjustedInputMsg, true);
_isReplaying = false;
}
_dragDropHelper.EndDrag();
return false;
}
// now when ending the drag, we will not replay the click because
// by this time we've determined the input was actually a drag attempt
// tell the server we are dropping if we are over a valid drop target in range.
// We don't use args.EntityUid here because drag interactions generally should
// work even if there's something "on top" of the drop target
if (!_interactionSystem.InRangeUnobstructed(_dragger,
args.Coordinates, ignoreInsideBlocker: true))
{
_dragDropHelper.EndDrag();
return false;
}
var entities = GameScreenBase.GetEntitiesUnderPosition(_stateManager, args.Coordinates);
foreach (var entity in entities)
{
// check if it's able to be dropped on by current dragged entity
var dropArgs = new DragDropEventArgs(_dragger, args.Coordinates, _dragDropHelper.Dragged, entity);
foreach (var draggable in _draggables)
{
if (!draggable.CanDrop(dropArgs))
{
continue;
}
// tell the server about the drop attempt
RaiseNetworkEvent(new DragDropMessage(args.Coordinates, _dragDropHelper.Dragged.Uid,
entity.Uid));
draggable.Drop(dropArgs);
_dragDropHelper.EndDrag();
return true;
}
}
_dragDropHelper.EndDrag();
return false;
} }
private void HighlightTargets() private void HighlightTargets()
{ {
if (_state != DragState.Dragging || _draggedEntity == null || if (_dragDropHelper.Dragged == null ||
_draggedEntity.Deleted || _dragShadow == null || _dragShadow.Deleted) _dragDropHelper.Dragged.Deleted || _dragShadow == null || _dragShadow.Deleted)
{ {
Logger.Warning("Programming error. Can't highlight drag and drop targets, not currently " + Logger.Warning("Programming error. Can't highlight drag and drop targets, not currently " +
"dragging anything or dragged entity / shadow was deleted."); "dragging anything or dragged entity / shadow was deleted.");
@@ -289,7 +329,7 @@ namespace Content.Client.GameObjects.EntitySystems
if (inRangeSprite.Visible == false) continue; if (inRangeSprite.Visible == false) continue;
// check if it's able to be dropped on by current dragged entity // check if it's able to be dropped on by current dragged entity
var canDropArgs = new CanDropEventArgs(_dragger, _draggedEntity, pvsEntity); var canDropArgs = new CanDropEventArgs(_dragger, _dragDropHelper.Dragged, pvsEntity);
var anyValidDraggable = _draggables.Any(draggable => draggable.CanDrop(canDropArgs)); var anyValidDraggable = _draggables.Any(draggable => draggable.CanDrop(canDropArgs));
if (anyValidDraggable) if (anyValidDraggable)
@@ -314,95 +354,10 @@ namespace Content.Client.GameObjects.EntitySystems
_highlightedSprites.Clear(); _highlightedSprites.Clear();
} }
/// <summary>
/// Cancels the drag, firing our saved drag event if instructed to do so and
/// we are within the threshold for replaying the click
/// (essentially reverting the drag attempt and allowing the original click
/// to proceed as if no drag was performed)
/// </summary>
/// <param name="cause">if fireSavedCmd is true, this should be passed with the value of
/// the pointer cmd that caused the drag to be cancelled</param>
private void CancelDrag(bool fireSavedCmd, FullInputCmdMessage cause)
{
RemoveHighlights();
if (_dragShadow != null)
{
EntityManager.DeleteEntity(_dragShadow);
}
_dragShadow = null;
_draggedEntity = null;
_draggables.Clear();
_dragger = null;
_state = DragState.NotDragging;
_mouseDownTime = 0;
if (fireSavedCmd && _savedMouseDown.HasValue && _mouseDownTime < MaxMouseDownTimeForReplayingClick)
{
var savedValue = _savedMouseDown.Value;
_isReplaying = true;
// adjust the timing info based on the current tick so it appears as if it happened now
var replayMsg = savedValue.OriginalMessage;
var adjustedInputMsg = new FullInputCmdMessage(cause.Tick, cause.SubTick, replayMsg.InputFunctionId, replayMsg.State, replayMsg.Coordinates, replayMsg.ScreenCoordinates, replayMsg.Uid);
_inputSystem.HandleInputCommand(savedValue.Session, EngineKeyFunctions.Use,
adjustedInputMsg, true);
_isReplaying = false;
}
_savedMouseDown = null;
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
if (_state == DragState.MouseDown) _dragDropHelper.Update(frameTime);
{
var screenPos = _inputManager.MouseScreenPosition;
if (_draggedEntity == null || _draggedEntity.Deleted)
{
// something happened to the clicked entity or we moved the mouse off the target so
// we shouldn't replay the original click
CancelDrag(false, null);
return;
}
else if ((_mouseDownScreenPos - screenPos).Length > DragDeadzone)
{
// initiate actual drag
StartDragging();
_mouseDownTime = 0;
}
}
else if (_state == DragState.Dragging)
{
if (_draggedEntity == null || _draggedEntity.Deleted)
{
CancelDrag(false, null);
return;
}
// still in range of the thing we are dragging?
if (!_interactionSystem.InRangeUnobstructed(_dragger, _draggedEntity))
{
CancelDrag(false, null);
return;
}
// keep dragged entity under mouse
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
// TODO: would use MapPosition instead if it had a setter, but it has no setter.
// is that intentional, or should we add a setter for Transform.MapPosition?
_dragShadow.Transform.WorldPosition = mousePos.Position;
_targetRecheckTime += frameTime;
if (_targetRecheckTime > TargetRecheckInterval)
{
HighlightTargets();
_targetRecheckTime = 0;
}
}
} }
} }
} }

View File

@@ -210,6 +210,8 @@
"CrematoriumEntityStorage", "CrematoriumEntityStorage",
"RandomArcade", "RandomArcade",
"RandomSpriteState", "RandomSpriteState",
"DebugEquip",
"InnateActions",
"ReagentGrinder", "ReagentGrinder",
"Grindable", "Grindable",
"Juiceable", "Juiceable",

View File

@@ -46,6 +46,17 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.Arcade1); human.AddFunction(ContentKeyFunctions.Arcade1);
human.AddFunction(ContentKeyFunctions.Arcade2); human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3); human.AddFunction(ContentKeyFunctions.Arcade3);
human.AddFunction(ContentKeyFunctions.OpenActionsMenu);
human.AddFunction(ContentKeyFunctions.Hotbar0);
human.AddFunction(ContentKeyFunctions.Hotbar1);
human.AddFunction(ContentKeyFunctions.Hotbar2);
human.AddFunction(ContentKeyFunctions.Hotbar3);
human.AddFunction(ContentKeyFunctions.Hotbar4);
human.AddFunction(ContentKeyFunctions.Hotbar5);
human.AddFunction(ContentKeyFunctions.Hotbar6);
human.AddFunction(ContentKeyFunctions.Hotbar7);
human.AddFunction(ContentKeyFunctions.Hotbar8);
human.AddFunction(ContentKeyFunctions.Hotbar9);
var ghost = contexts.New("ghost", "common"); var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp); ghost.AddFunction(EngineKeyFunctions.MoveUp);

View File

@@ -0,0 +1,100 @@
#nullable enable
using System;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface
{
/// <summary>
/// Tooltip for actions or alerts because they are very similar.
/// </summary>
public class ActionAlertTooltip : PanelContainer
{
private const float TooltipTextMaxWidth = 350;
private readonly RichTextLabel _cooldownLabel;
private readonly IGameTiming _gameTiming;
/// <summary>
/// Current cooldown displayed in this tooltip. Set to null to show no cooldown.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
VBoxContainer vbox;
AddChild(vbox = new VBoxContainer {RectClipContent = true});
var nameLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
};
nameLabel.SetMessage(name);
vbox.AddChild(nameLabel);
if (desc != null && !string.IsNullOrWhiteSpace(desc.ToString()))
{
var description = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
};
description.SetMessage(desc);
vbox.AddChild(description);
}
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
Visible = false
});
if (!string.IsNullOrWhiteSpace(requires))
{
var requiresLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
};
requiresLabel.SetMessage(FormattedMessage.FromMarkup("[color=#635c5c]" +
requires +
"[/color]"));
vbox.AddChild(requiresLabel);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!Cooldown.HasValue)
{
_cooldownLabel.Visible = false;
return;
}
var timeLeft = Cooldown.Value.End - _gameTiming.CurTime;
if (timeLeft > TimeSpan.Zero)
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
$"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
_cooldownLabel.Visible = true;
}
else
{
_cooldownLabel.Visible = false;
}
}
}
}

View File

@@ -0,0 +1,499 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.GameObjects.Components.Mobs.Actions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.Actions;
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;
namespace Content.Client.UserInterface
{
/// <summary>
/// Action selection menu, allows filtering and searching over all possible
/// actions and populating those actions into the hotbar.
/// </summary>
public class ActionMenu : SS14Window
{
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";
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>();
// parallel list of actions currently selectable in itemList
private BaseActionPrototype[] _actionList;
private readonly ActionManager _actionManager;
private readonly ClientActionsComponent _actionsComponent;
private readonly ActionsUI _actionsUI;
private readonly LineEdit _searchBar;
private readonly MultiselectOptionButton<string> _filterButton;
private readonly Label _filterLabel;
private readonly Button _clearButton;
private readonly GridContainer _resultsGrid;
private readonly TextureRect _dragShadow;
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
{
_actionsComponent = actionsComponent;
_actionsUI = actionsUI;
_actionManager = IoCManager.Resolve<ActionManager>();
Title = Loc.GetString("Actions");
CustomMinimumSize = (300, 300);
Contents.AddChild(new VBoxContainer
{
Children =
{
new HBoxContainer
{
Children =
{
(_searchBar = new LineEdit
{
StyleClasses = { StyleNano.StyleClassActionSearchBox },
SizeFlagsHorizontal = SizeFlags.FillExpand,
PlaceHolder = Loc.GetString("Search")
}),
(_filterButton = new MultiselectOptionButton<string>()
{
Label = Loc.GetString("Filter")
}),
}
},
(_clearButton = new Button
{
Text = Loc.GetString("Clear"),
}),
(_filterLabel = new Label()),
new ScrollContainer
{
//TODO: needed? CustomMinimumSize = new Vector2(200.0f, 0.0f),
SizeFlagsVertical = SizeFlags.FillExpand,
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
(_resultsGrid = new GridContainer
{
MaxWidth = 300
})
}
}
}
});
// 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))
{
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
}
UpdateFilterLabel();
_dragShadow = new TextureRect
{
CustomMinimumSize = (64, 64),
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
LayoutContainer.SetSize(_dragShadow, (64, 64));
_dragDropHelper = new DragDropHelper<ActionMenuItem>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
}
protected override void EnteredTree()
{
base.EnteredTree();
_clearButton.OnPressed += OnClearButtonPressed;
_searchBar.OnTextChanged += OnSearchTextChanged;
_filterButton.OnItemSelected += OnFilterItemSelected;
foreach (var actionMenuControl in _resultsGrid.Children)
{
var actionMenuItem = (actionMenuControl as ActionMenuItem);
actionMenuItem.OnButtonDown += OnItemButtonDown;
actionMenuItem.OnButtonUp += OnItemButtonUp;
actionMenuItem.OnPressed += OnItemPressed;
}
}
protected override void ExitedTree()
{
base.ExitedTree();
_clearButton.OnPressed -= OnClearButtonPressed;
_searchBar.OnTextChanged -= OnSearchTextChanged;
_filterButton.OnItemSelected -= OnFilterItemSelected;
foreach (var actionMenuControl in _resultsGrid.Children)
{
var actionMenuItem = (actionMenuControl as ActionMenuItem);
actionMenuItem.OnButtonDown -= OnItemButtonDown;
actionMenuItem.OnButtonUp -= OnItemButtonUp;
actionMenuItem.OnPressed -= OnItemPressed;
}
}
private void OnFilterItemSelected(MultiselectOptionButton<string>.ItemPressedEventArgs args)
{
UpdateFilterLabel();
SearchAndDisplay();
}
protected override void Resized()
{
base.Resized();
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// currently no good way to let the grid know what size it has to "work with", so must manually resize
_resultsGrid.MaxWidth = Width;
}
private bool OnBeginActionDrag()
{
_dragShadow.Texture = _dragDropHelper.Dragged.Action.Icon.Frame0();
// don't make visible until frameupdate, otherwise it'll flicker
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
return true;
}
private bool OnContinueActionDrag(float frameTime)
{
// keep dragged entity centered under mouse
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
// we don't set this visible until frameupdate, otherwise it flickers
_dragShadow.Visible = true;
return true;
}
private void OnEndActionDrag()
{
_dragShadow.Visible = false;
}
private void OnItemButtonDown(BaseButton.ButtonEventArgs args)
{
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
_dragDropHelper.MouseDown(args.Button as ActionMenuItem);
}
private void OnItemButtonUp(BaseButton.ButtonEventArgs args)
{
// note the buttonup only fires on the control that was originally
// pressed to initiate the drag, NOT the one we are currently hovering
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
if (UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot)
{
if (!_dragDropHelper.IsDragging || _dragDropHelper.Dragged?.Action == null)
{
_dragDropHelper.EndDrag();
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.UpdateUI();
}
_dragDropHelper.EndDrag();
}
private void OnItemPressed(BaseButton.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.UpdateUI();
}
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
_searchBar.Clear();
_filterButton.DeselectAll();
UpdateFilterLabel();
SearchAndDisplay();
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj)
{
SearchAndDisplay();
}
private void SearchAndDisplay()
{
var search = Standardize(_searchBar.Text);
// only display nothing if there are no filters selected and text is not long enough.
// otherwise we will search if even one filter is applied, regardless of length of search string
if (_filterButton.SelectedKeys.Count == 0 &&
(string.IsNullOrWhiteSpace(search) || search.Length < MinSearchLength))
{
ClearList();
return;
}
var matchingActions = _actionManager.EnumerateActions()
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
PopulateActions(matchingActions);
}
private void UpdateFilterLabel()
{
if (_filterButton.SelectedKeys.Count == 0)
{
_filterLabel.Visible = false;
}
else
{
_filterLabel.Visible = true;
_filterLabel.Text = Loc.GetString("Filters: {0}", string.Join(", ", _filterButton.SelectedLabels));
}
}
private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
IReadOnlyList<string> selectedFilterTags)
{
// check filter tag match first - each action must contain all filter tags currently selected.
// if no tags selected, don't check tags
if (selectedFilterTags.Count > 0 && selectedFilterTags.Any(filterTag => !ActionMatchesFilterTag(action, filterTag)))
{
return false;
}
// check search tag match against the search query
if (action.Keywords.Any(standardizedSearch.Contains))
{
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;
}
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)
{
return tag switch
{
AllActionsTag => true,
GrantedActionsTag => _actionsComponent.IsGranted(action),
ItemTag => action is ItemActionPrototype,
NotItemTag => action is ActionPrototype,
InstantActionTag => action.BehaviorType == BehaviorType.Instant,
TargetActionTag => action.BehaviorType == BehaviorType.TargetEntity ||
action.BehaviorType == BehaviorType.TargetPoint,
ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
_ => action.Filters.Contains(tag)
};
}
/// <summary>
/// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace),
/// trimmed, 1 space max per whitespace gap,
/// and optional spaces between case change
/// </summary>
private static string Standardize(string rawText, bool splitOnCaseChange = false)
{
rawText ??= "";
// treat non-alphanumeric characters as whitespace
rawText = NonAlphanumeric.Replace(rawText, " ");
// trim spaces and reduce internal whitespaces to 1 max
rawText = Whitespace.Replace(rawText, " ").Trim();
if (splitOnCaseChange)
{
// insert a space when case switches from lower to upper
rawText = AddSpaces(rawText, true);
}
return rawText.ToLowerInvariant().Trim();
}
// taken from https://stackoverflow.com/a/272929 (CC BY-SA 3.0)
private static string AddSpaces(string text, bool preserveAcronyms)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
var newText = new StringBuilder(text.Length * 2);
newText.Append(text[0]);
for (var i = 1; i < text.Length; i++)
{
if (char.IsUpper(text[i]))
{
if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) ||
(preserveAcronyms && char.IsUpper(text[i - 1]) &&
i < text.Length - 1 && !char.IsUpper(text[i + 1])))
newText.Append(' ');
}
newText.Append(text[i]);
}
return newText.ToString();
}
private void PopulateActions(IEnumerable<BaseActionPrototype> actions)
{
ClearList();
_actionList = actions.ToArray();
foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
{
var actionItem = new ActionMenuItem(action);
_resultsGrid.Children.Add(actionItem);
actionItem.SetActionState(_actionsComponent.IsGranted(action));
actionItem.OnButtonDown += OnItemButtonDown;
actionItem.OnButtonUp += OnItemButtonUp;
actionItem.OnPressed += OnItemPressed;
}
}
private void ClearList()
{
// TODO: Not sure if this unsub is needed if children are all being cleared
foreach (var actionItem in _resultsGrid.Children)
{
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
}
_resultsGrid.Children.Clear();
_actionList = EmptyActionList;
}
/// <summary>
/// Should be invoked when action states change, ensures
/// currently displayed actions are properly showing their revoked / granted status
/// </summary>
public void UpdateUI()
{
foreach (var actionItem in _resultsGrid.Children)
{
var actionMenuItem = ((ActionMenuItem) actionItem);
actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
_dragDropHelper.Update(args.DeltaSeconds);
}
}
}

View File

@@ -0,0 +1,68 @@
#nullable enable
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
namespace Content.Client.UserInterface
{
/// <summary>
/// An individual action visible in the action menu.
/// </summary>
public class ActionMenuItem : ContainerButton
{
// shorter than default tooltip delay so user can
// quickly explore what each action is
private const float CustomTooltipDelay = 0.2f;
public BaseActionPrototype Action { get; private set; }
public ActionMenuItem(BaseActionPrototype action)
{
Action = action;
CustomMinimumSize = (64, 64);
SizeFlagsVertical = SizeFlags.None;
AddChild(new TextureRect
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Stretch = TextureRect.StretchMode.Scale,
Texture = action.Icon.Frame0()
});
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
}
private Control SupplyTooltip(Control? sender)
{
return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
}
/// <summary>
/// Change how this is displayed depending on if it is granted or revoked
/// </summary>
public void SetActionState(bool granted)
{
if (granted)
{
if (HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
{
RemoveStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
}
}
else
{
if (!HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
{
AddStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
}
}
}
}
}

View File

@@ -0,0 +1,556 @@
#nullable enable
using System.Collections.Generic;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.GameObjects.Components.Mobs.Actions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.Actions;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.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.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface
{
/// <summary>
/// The action hotbar on the left side of the screen.
/// </summary>
public sealed class ActionsUI : Container
{
private readonly ClientActionsComponent _actionsComponent;
private readonly ActionManager _actionManager;
private readonly IEntityManager _entityManager;
private readonly IGameTiming _gameTiming;
private readonly ActionSlot[] _slots;
private readonly GridContainer _slotContainer;
private readonly TextureButton _lockButton;
private readonly TextureButton _settingsButton;
private readonly TextureButton _previousHotbarButton;
private readonly Label _loadoutNumber;
private readonly TextureButton _nextHotbarButton;
private readonly Texture _lockTexture;
private readonly Texture _unlockTexture;
private readonly TextureRect _dragShadow;
private readonly ActionMenu _menu;
/// <summary>
/// Index of currently selected hotbar
/// </summary>
public byte SelectedHotbar { get; private set; }
/// <summary>
/// Action slot we are currently selecting a target for.
/// </summary>
public ActionSlot? SelectingTargetFor { get; private set; }
/// <summary>
/// Drag drop helper for coordinating drag drops between action slots
/// </summary>
public DragDropHelper<ActionSlot> DragDropHelper { get; }
/// <summary>
/// Whether the bar is currently locked by the user. This is intended to prevent drag / drop
/// and right click clearing slots. Anything else is still doable.
/// </summary>
public bool Locked { get; private set; }
/// <summary>
/// All the action slots in order.
/// </summary>
public IEnumerable<ActionSlot> Slots => _slots;
public ActionsUI(ClientActionsComponent actionsComponent)
{
_actionsComponent = actionsComponent;
_actionManager = IoCManager.Resolve<ActionManager>();
_entityManager = IoCManager.Resolve<IEntityManager>();
_gameTiming = IoCManager.Resolve<IGameTiming>();
_menu = new ActionMenu(_actionsComponent, this);
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetAnchorTop(this, 0f);
LayoutContainer.SetAnchorBottom(this, 0.8f);
LayoutContainer.SetMarginLeft(this, 10);
LayoutContainer.SetMarginTop(this, 100);
SizeFlagsHorizontal = SizeFlags.None;
SizeFlagsVertical = SizeFlags.FillExpand;
var resourceCache = IoCManager.Resolve<IResourceCache>();
// everything needs to go within an inner panel container so the panel resizes to fit the elements.
// Because ActionsUI is being anchored by layoutcontainer, the hotbar backing would appear too tall
// if ActionsUI was the panel container
var panelContainer = new PanelContainer()
{
StyleClasses = {StyleNano.StyleClassHotbarPanel},
SizeFlagsHorizontal = SizeFlags.None,
SizeFlagsVertical = SizeFlags.None
};
AddChild(panelContainer);
var hotbarContainer = new VBoxContainer
{
SeparationOverride = 3,
SizeFlagsHorizontal = SizeFlags.None
};
panelContainer.AddChild(hotbarContainer);
var settingsContainer = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand
};
hotbarContainer.AddChild(settingsContainer);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.png");
_unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.png");
_lockButton = new TextureButton
{
TextureNormal = _unlockTexture,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
settingsContainer.AddChild(_lockButton);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_settingsButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
settingsContainer.AddChild(_settingsButton);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
// this allows a 2 column layout if window gets too small
_slotContainer = new GridContainer
{
MaxHeight = CalcMaxHeight()
};
hotbarContainer.AddChild(_slotContainer);
var loadoutContainer = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand
};
hotbarContainer.AddChild(loadoutContainer);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_previousHotbarButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_previousHotbarButton);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_loadoutNumber = new Label
{
Text = "1",
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_loadoutNumber);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_nextHotbarButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_nextHotbarButton);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_slots = new ActionSlot[ClientActionsComponent.Slots];
_dragShadow = new TextureRect
{
CustomMinimumSize = (64, 64),
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
LayoutContainer.SetSize(_dragShadow, (64, 64));
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
{
var slot = new ActionSlot(this, actionsComponent, i);
_slotContainer.AddChild(slot);
_slots[i] = slot;
}
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
}
protected override void EnteredTree()
{
base.EnteredTree();
_lockButton.OnPressed += OnLockPressed;
_nextHotbarButton.OnPressed += NextHotbar;
_previousHotbarButton.OnPressed += PreviousHotbar;
_settingsButton.OnPressed += OnToggleActionsMenu;
}
protected override void ExitedTree()
{
base.ExitedTree();
StopTargeting();
_menu.Close();
_lockButton.OnPressed -= OnLockPressed;
_nextHotbarButton.OnPressed -= NextHotbar;
_previousHotbarButton.OnPressed -= PreviousHotbar;
_settingsButton.OnPressed -= OnToggleActionsMenu;
}
protected override Vector2 CalculateMinimumSize()
{
// allows us to shrink down to a 2-column layout minimum
return (10, 400);
}
protected override void Resized()
{
base.Resized();
_slotContainer.MaxHeight = CalcMaxHeight();
}
private float CalcMaxHeight()
{
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// this is here because there isn't currently a good way to allow the grid to adjust its height based
// on constraints, otherwise we would use anchors to lay it out
// it looks bad to have an uneven number of slots in the columns,
// so we either do a single column or 2 equal sized columns
if (Height < 650)
{
// 2 column
return 400;
}
else
{
// 1 column
return 900;
}
}
protected override void UIScaleChanged()
{
_slotContainer.MaxHeight = CalcMaxHeight();
base.UIScaleChanged();
}
/// <summary>
/// Refresh the display of all the slots in the currently displayed hotbar,
/// to reflect the current component state and assignments of actions component.
/// </summary>
public void UpdateUI()
{
_menu.UpdateUI();
foreach (var actionSlot in Slots)
{
var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
if (!assignedActionType.HasValue)
{
actionSlot.Clear();
continue;
}
if (assignedActionType.Value.TryGetAction(out var actionType))
{
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();
}
}
}
private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
{
if (_actionManager.TryGet(actionType, out var action))
{
actionSlot.Assign(action, true);
}
else
{
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
actionSlot.Clear();
return;
}
if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.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;
}
else
{
// action is currently granted
actionSlot.EnableAction();
actionSlot.Cooldown = actionState.Cooldown;
// if we are targeting with an action now on cooldown, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
actionState.IsOnCooldown(_gameTiming))
{
StopTargeting();
}
}
// check if we need to toggle it
if (action.BehaviorType == BehaviorType.Toggle)
{
actionSlot.ToggledOn = actionState.ToggledOn;
}
}
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.TryGetEntity(item, out var itemEntity)) return;
if (_actionManager.TryGet(itemActionType, out var action))
{
actionSlot.Assign(action, itemEntity, 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 (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
SelectingTargetFor.Item == itemEntity &&
actionState.IsOnCooldown(_gameTiming))
{
StopTargeting();
}
}
actionSlot.Cooldown = actionState.Cooldown;
// check if we need to toggle it
if (action.BehaviorType == BehaviorType.Toggle)
{
actionSlot.ToggledOn = actionState.ToggledOn;
}
}
private void NextHotbar(BaseButton.ButtonEventArgs args)
{
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
}
private void PreviousHotbar(BaseButton.ButtonEventArgs args)
{
var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
ChangeHotbar((byte) newBar);
}
private void ChangeHotbar(byte hotbar)
{
StopTargeting();
SelectedHotbar = hotbar;
_loadoutNumber.Text = (hotbar + 1).ToString();
UpdateUI();
}
/// <summary>
/// If currently targeting with this slot, stops targeting.
/// If currently targeting with no slot or a different slot, switches to
/// targeting with the specified slot.
/// </summary>
/// <param name="slot"></param>
public void ToggleTargeting(ActionSlot slot)
{
if (SelectingTargetFor == slot)
{
StopTargeting();
return;
}
StartTargeting(slot);
}
/// <summary>
/// Puts us in targeting mode, where we need to pick either a target point or entity
/// </summary>
private void StartTargeting(ActionSlot actionSlot)
{
// 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;
}
}
/// <summary>
/// Switch out of targeting mode if currently selecting target for an action
/// </summary>
public void StopTargeting()
{
if (SelectingTargetFor == null) return;
if (SelectingTargetFor.ToggledOn)
{
SelectingTargetFor.ToggledOn = false;
}
SelectingTargetFor = null;
}
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
{
ToggleActionsMenu();
}
public void ToggleActionsMenu()
{
if (_menu.IsOpen)
{
_menu.Close();
}
else
{
_menu.OpenCentered();
}
}
private void OnLockPressed(BaseButton.ButtonEventArgs obj)
{
Locked = !Locked;
_lockButton.TextureNormal = Locked ? _lockTexture : _unlockTexture;
}
private bool OnBeginActionDrag()
{
// 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();
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
DragDropHelper.Dragged.CancelPress();
return true;
}
private bool OnContinueActionDrag(float frameTime)
{
// stop if there's no action in the slot
if (Locked || DragDropHelper.Dragged.Action == null) return false;
// keep dragged entity centered under mouse
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
// we don't set this visible until frameupdate, otherwise it flickers
_dragShadow.Visible = true;
return true;
}
private void OnEndActionDrag()
{
_dragShadow.Visible = false;
}
/// <summary>
/// Handle keydown / keyup for one of the slots via a keybinding, simulates mousedown/mouseup on it.
/// </summary>
/// <param name="slot">slot index to to receive the press (0 corresponds to the one labeled 1, 9 corresponds to the one labeled 0)</param>
public void HandleHotbarKeybind(byte slot, PointerInputCmdHandler.PointerInputCmdArgs args)
{
var actionSlot = _slots[slot];
actionSlot.Depress(args.State == BoundKeyState.Down);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
DragDropHelper.Update(args.DeltaSeconds);
}
}
}

View File

@@ -1,10 +1,8 @@
using System; using System;
using Content.Client.UserInterface.Stylesheets; using Content.Client.UserInterface.Stylesheets;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.Graphics; using Robust.Client.Interfaces.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Maths; using Robust.Shared.Maths;
namespace Content.Client.UserInterface namespace Content.Client.UserInterface
@@ -16,58 +14,51 @@ namespace Content.Client.UserInterface
{ {
public GridContainer Grid { get; } public GridContainer Grid { get; }
private readonly IClyde _clyde; public AlertsUI()
public AlertsUI(IClyde clyde)
{ {
_clyde = clyde; LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetAnchorTop(this, 0f);
LayoutContainer.SetAnchorRight(this, 1f);
LayoutContainer.SetAnchorBottom(this, 1f);
LayoutContainer.SetMarginBottom(this, -180);
LayoutContainer.SetMarginTop(this, 250);
LayoutContainer.SetMarginRight(this, -10);
var panelContainer = new PanelContainer var panelContainer = new PanelContainer
{ {
StyleClasses = {StyleNano.StyleClassTransparentBorderedWindowPanel}, StyleClasses = {StyleNano.StyleClassTransparentBorderedWindowPanel},
SizeFlagsVertical = SizeFlags.FillExpand, SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.None
}; };
AddChild(panelContainer); AddChild(panelContainer);
Grid = new GridContainer Grid = new GridContainer
{ {
MaxHeight = CalcMaxHeight(clyde.ScreenSize), MaxHeight = 64,
ExpandBackwards = true ExpandBackwards = true
}; };
panelContainer.AddChild(Grid); panelContainer.AddChild(Grid);
clyde.OnWindowResized += ClydeOnOnWindowResized;
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
LayoutContainer.SetMarginTop(this, 250);
} }
protected override void UIScaleChanged() protected override void Resized()
{
Grid.MaxHeight = CalcMaxHeight(_clyde.ScreenSize);
base.UIScaleChanged();
}
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
{ {
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done, // TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// this is here because there isn't currently a good way to allow the grid to adjust its height based // this is here because there isn't currently a good way to allow the grid to adjust its height based
// on constraints, otherwise we would use anchors to lay it out // on constraints, otherwise we would use anchors to lay it out
Grid.MaxHeight = CalcMaxHeight(obj.NewSize);; base.Resized();
Grid.MaxHeight = Height;
} }
private float CalcMaxHeight(Vector2i screenSize) protected override Vector2 CalculateMinimumSize()
{ {
return Math.Max(((screenSize.Y) / UIScale) - 420, 1); // allows us to shrink down to a single row
return (64, 64);
} }
protected override void Dispose(bool disposing) protected override void UIScaleChanged()
{ {
base.Dispose(disposing); Grid.MaxHeight = Height;
base.UIScaleChanged();
if (disposing)
{
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
}
} }
} }
} }

View File

@@ -0,0 +1,652 @@
#nullable enable
using System;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Controls
{
/// <summary>
/// A slot in the action hotbar. Not extending BaseButton because
/// its needs diverged too much.
/// </summary>
public class ActionSlot : PanelContainer
{
// shorter than default tooltip delay so user can more easily
// see what actions they've been given
private const float CustomTooltipDelay = 0.5f;
private static readonly string EnabledColor = "#7b7e9e";
private static readonly string DisabledColor = "#950000";
/// <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?
/// </summary>
public bool CanUseAction => HasAssignment && ActionEnabled && !IsOnCooldown;
/// <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 IEntity? 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();
}
}
/// <summary>
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
/// </summary>
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;
private readonly TextureRect _smallActionIcon;
private readonly SpriteView _smallItemSpriteView;
private readonly SpriteView _bigItemSpriteView;
private readonly CooldownGraphic _cooldownGraphic;
private readonly ActionsUI _actionsUI;
private readonly ClientActionsComponent _actionsComponent;
private bool _toggledOn;
// whether button is currently pressed down by mouse or keybind down.
private bool _depressed;
private bool _beingHovered;
/// <summary>
/// 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, ClientActionsComponent actionsComponent, byte slotIndex)
{
_actionsComponent = actionsComponent;
_actionsUI = actionsUI;
_gameTiming = IoCManager.Resolve<IGameTiming>();
SlotIndex = slotIndex;
MouseFilter = MouseFilterMode.Stop;
CustomMinimumSize = (64, 64);
SizeFlagsVertical = SizeFlags.None;
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
_number = new RichTextLabel
{
StyleClasses = {StyleNano.StyleClassHotbarSlotNumber}
};
_number.SetMessage(SlotNumberLabel());
_bigActionIcon = new TextureRect
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
_bigItemSpriteView = new SpriteView
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Scale = (2,2),
Visible = false
};
_smallActionIcon = new TextureRect
{
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.ShrinkEnd,
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
_smallItemSpriteView = new SpriteView
{
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.ShrinkEnd,
Visible = false
};
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
// padding to the left of the number to shift it right
var paddingBox = new HBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
CustomMinimumSize = (64, 64)
};
paddingBox.AddChild(new Control()
{
CustomMinimumSize = (4, 4),
SizeFlagsVertical = SizeFlags.Fill
});
paddingBox.AddChild(_number);
// padding to the left of the small icon
var paddingBoxItemIcon = new HBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
CustomMinimumSize = (64, 64)
};
paddingBoxItemIcon.AddChild(new Control()
{
CustomMinimumSize = (32, 32),
SizeFlagsVertical = SizeFlags.Fill
});
paddingBoxItemIcon.AddChild(new Control
{
Children =
{
_smallActionIcon,
_smallItemSpriteView
}
});
AddChild(_bigActionIcon);
AddChild(_bigItemSpriteView);
AddChild(_cooldownGraphic);
AddChild(paddingBox);
AddChild(paddingBoxItemIcon);
DrawModeChanged();
}
private Control? SupplyTooltip(Control sender)
{
return Action == null ? null :
new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
}
/// <summary>
/// Action attempt for performing the action in the slot
/// </summary>
public IActionAttempt? ActionAttempt()
{
IActionAttempt? attempt = Action switch
{
ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
ItemActionPrototype itemActionPrototype =>
(Item != null && Item.TryGetComponent<ItemActionsComponent>(out var itemActions)) ?
new ItemActionAttempt(itemActionPrototype, Item, itemActions) : null,
_ => null
};
return attempt;
}
protected override void MouseEntered()
{
base.MouseEntered();
_beingHovered = true;
DrawModeChanged();
if (Action is not ItemActionPrototype) return;
if (Item == null) return;
_actionsComponent.HighlightItemSlot(Item);
}
protected override void MouseExited()
{
base.MouseExited();
_beingHovered = false;
CancelPress();
DrawModeChanged();
_actionsComponent.StopHighlightingItemSlots();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Function == EngineKeyFunctions.UIRightClick)
{
if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging)
{
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
_actionsUI.StopTargeting();
_actionsUI.UpdateUI();
}
return;
}
// only handle clicks, and can't do anything to this if no assignment
if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment)
return;
// might turn into a drag or a full press if released
Depress(true);
_actionsUI.DragDropHelper.MouseDown(this);
DrawModeChanged();
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (args.Function != EngineKeyFunctions.UIClick)
return;
// might be finishing a drag or using the action
if (_actionsUI.DragDropHelper.IsDragging &&
_actionsUI.DragDropHelper.Dragged == this &&
UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot &&
targetSlot != this)
{
// finish the drag, swap the 2 slots
var fromIdx = SlotIndex;
var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
var toIdx = targetSlot.SlotIndex;
var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
if (fromIdx == toIdx) return;
if (!fromAssignment.HasValue) return;
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
if (toAssignment.HasValue)
{
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
}
else
{
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
}
_actionsUI.UpdateUI();
}
else
{
// perform the action
if (UserInterfaceManager.CurrentlyHovered == this)
{
Depress(false);
}
}
_actionsUI.DragDropHelper.EndDrag();
DrawModeChanged();
}
/// <summary>
/// Cancel current press without triggering the action
/// </summary>
public void CancelPress()
{
_depressed = false;
DrawModeChanged();
}
/// <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.
/// </summary>
public void Depress(bool depress)
{
if (!CanUseAction) return;
if (_depressed && !depress)
{
// fire the action
// no left-click interaction with it on cooldown or revoked
_actionsComponent.AttemptAction(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 = null;
_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 == null) return;
Action = action;
Item = null;
_depressed = false;
ToggledOn = false;
ActionEnabled = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Updates the item action assigned to this slot, tied to a specific item.
/// </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, IEntity item, bool actionEnabled)
{
// already assigned
if (Action != null && Action == action && Item == item) return;
Action = action;
Item = item;
_depressed = false;
ToggledOn = false;
ActionEnabled = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Clears the action assigned to this slot
/// </summary>
public void Clear()
{
if (!HasAssignment) return;
Action = null;
Item = null;
ToggledOn = false;
_depressed = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Display the action in this slot (if there is one) as enabled
/// </summary>
public void EnableAction()
{
if (ActionEnabled || !HasAssignment) return;
ActionEnabled = true;
_depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Display the action in this slot (if there is one) as disabled.
/// The slot is still clickable.
/// </summary>
public void DisableAction()
{
if (!ActionEnabled || !HasAssignment) return;
ActionEnabled = false;
_depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
private FormattedMessage SlotNumberLabel()
{
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
}
private void UpdateIcons()
{
if (!HasAssignment)
{
SetActionIcon(null);
SetItemIcon(null);
return;
}
if (HasToggleSprite && ToggledOn && Action != null)
{
SetActionIcon(Action.IconOn.Frame0());
}
else if (Action != null)
{
SetActionIcon(Action.Icon.Frame0());
}
if (Item != null)
{
SetItemIcon(Item.TryGetComponent<ISpriteComponent>(out var spriteComponent) ? spriteComponent : null);
}
else
{
SetItemIcon(null);
}
}
private void SetActionIcon(Texture? texture)
{
if (texture == null || !HasAssignment)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.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;
}
}
}
private void SetItemIcon(ISpriteComponent? sprite)
{
if (sprite == null || !HasAssignment)
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
else
{
if (Action is ItemActionPrototype actionPrototype)
{
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;
}
}
}
else
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
}
}
private void DrawModeChanged()
{
// always show the normal empty button style if no action in this slot
if (!HasAssignment)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// it's only depress-able if it's usable, so if we're depressed
// show the depressed style
if (_depressed)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
return;
}
// show a hover only if the action is usable
if (_beingHovered)
{
if (ActionEnabled && !IsOnCooldown)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
return;
}
}
// if it's toggled on, always show the toggled on style (currently same as depressed style)
if (ToggledOn)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
ContainerButton.StylePseudoClassPressed);
return;
}
if (!ActionEnabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;
}
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!Cooldown.HasValue)
{
_cooldownGraphic.Visible = false;
_cooldownGraphic.Progress = 0;
return;
}
var duration = Cooldown.Value.End - Cooldown.Value.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);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
_cooldownGraphic.Visible = ratio > -1f;
}
}
}

View File

@@ -1,30 +1,48 @@
#nullable enable #nullable enable
using System; using System;
using Content.Client.UserInterface;
using Content.Client.Utility; using Content.Client.Utility;
using Content.Shared.Alert; using Content.Shared.Alert;
using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.GameObjects.Components.Mobs namespace Content.Client.UserInterface.Controls
{ {
public class AlertControl : BaseButton public class AlertControl : BaseButton
{ {
// shorter than default tooltip delay so user can more easily
// see what alerts they have
private const float CustomTooltipDelay = 0.5f;
public AlertPrototype Alert { get; } public AlertPrototype Alert { get; }
/// <summary> /// <summary>
/// Total duration of the cooldown in seconds. Null if no duration / cooldown. /// Current cooldown displayed in this slot. Set to null to show no cooldown.
/// </summary> /// </summary>
public int? TotalDuration { get; set; } 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;
private short? _severity; private short? _severity;
private readonly IGameTiming _gameTiming;
private readonly TextureRect _icon; private readonly TextureRect _icon;
private readonly CooldownGraphic _cooldownGraphic; private readonly CooldownGraphic _cooldownGraphic;
private readonly IResourceCache _resourceCache; private readonly IResourceCache _resourceCache;
/// <summary> /// <summary>
/// Creates an alert control reflecting the indicated alert + state /// Creates an alert control reflecting the indicated alert + state
/// </summary> /// </summary>
@@ -33,6 +51,9 @@ namespace Content.Client.GameObjects.Components.Mobs
/// <param name="resourceCache">resourceCache to use to load alert icon textures</param> /// <param name="resourceCache">resourceCache to use to load alert icon textures</param>
public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache) public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache)
{ {
_gameTiming = IoCManager.Resolve<IGameTiming>();
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
_resourceCache = resourceCache; _resourceCache = resourceCache;
Alert = alert; Alert = alert;
_severity = severity; _severity = severity;
@@ -49,6 +70,11 @@ namespace Content.Client.GameObjects.Components.Mobs
} }
private Control SupplyTooltip(Control? sender)
{
return new ActionAlertTooltip(Alert.Name, Alert.Description) {Cooldown = Cooldown};
}
/// <summary> /// <summary>
/// Change the alert severity, changing the displayed icon /// Change the alert severity, changing the displayed icon
/// </summary> /// </summary>
@@ -61,33 +87,24 @@ namespace Content.Client.GameObjects.Components.Mobs
} }
} }
/// <summary> protected override void FrameUpdate(FrameEventArgs args)
/// Updates the displayed cooldown amount, doing nothing if alertCooldown is null
/// </summary>
/// <param name="alertCooldown">cooldown start and end</param>
/// <param name="curTime">current game time</param>
public void UpdateCooldown((TimeSpan Start, TimeSpan End)? alertCooldown, in TimeSpan curTime)
{ {
if (!alertCooldown.HasValue) base.FrameUpdate(args);
if (!Cooldown.HasValue)
{ {
_cooldownGraphic.Progress = 0;
_cooldownGraphic.Visible = false; _cooldownGraphic.Visible = false;
TotalDuration = null; _cooldownGraphic.Progress = 0;
return;
} }
else
{
var start = alertCooldown.Value.Start; var duration = Cooldown.Value.End - Cooldown.Value.Start;
var end = alertCooldown.Value.End; 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 length = (end - start).TotalSeconds;
var progress = (curTime - start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (curTime - end).TotalSeconds * -5);
TotalDuration = (int?) Math.Round(length);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1); _cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
_cooldownGraphic.Visible = ratio > -1f; _cooldownGraphic.Visible = ratio > -1f;
} }
} }
}
} }

View File

@@ -1,14 +1,19 @@
using System; using System;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Graphics.Shaders;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input; using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface namespace Content.Client.UserInterface
{ {
public class ItemSlotButton : MarginContainer public class ItemSlotButton : MarginContainer
{ {
private const string HighlightShader = "SelectionOutlineInrange";
public TextureRect Button { get; } public TextureRect Button { get; }
public SpriteView SpriteView { get; } public SpriteView SpriteView { get; }
public SpriteView HoverSpriteView { get; } public SpriteView HoverSpriteView { get; }
@@ -21,9 +26,11 @@ namespace Content.Client.UserInterface
public bool EntityHover => HoverSpriteView.Sprite != null; public bool EntityHover => HoverSpriteView.Sprite != null;
public bool MouseIsHovering = false; public bool MouseIsHovering = false;
private readonly ShaderInstance _highlightShader;
public ItemSlotButton(Texture texture, Texture storageTexture) public ItemSlotButton(Texture texture, Texture storageTexture)
{ {
_highlightShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>(HighlightShader).Instance();
CustomMinimumSize = (64, 64); CustomMinimumSize = (64, 64);
AddChild(Button = new TextureRect AddChild(Button = new TextureRect
@@ -95,6 +102,20 @@ namespace Content.Client.UserInterface
} }
} }
public void Highlight(bool on)
{
// I make no claim that this actually looks good but it's a start.
if (on)
{
Button.ShaderOverride = _highlightShader;
}
else
{
Button.ShaderOverride = null;
}
}
private void OnButtonPressed(GUIBoundKeyEventArgs args) private void OnButtonPressed(GUIBoundKeyEventArgs args)
{ {
OnPressed?.Invoke(args); OnPressed?.Invoke(args);

View File

@@ -150,7 +150,6 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.ReleasePulledObject, "Release pulled object"); AddButton(ContentKeyFunctions.ReleasePulledObject, "Release pulled object");
AddButton(ContentKeyFunctions.Point, "Point at location"); AddButton(ContentKeyFunctions.Point, "Point at location");
AddHeader("User Interface"); AddHeader("User Interface");
AddButton(ContentKeyFunctions.FocusChat, "Focus chat"); AddButton(ContentKeyFunctions.FocusChat, "Focus chat");
AddButton(ContentKeyFunctions.FocusOOC, "Focus chat (OOC)"); AddButton(ContentKeyFunctions.FocusOOC, "Focus chat (OOC)");
@@ -160,6 +159,7 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.OpenCraftingMenu, "Open crafting menu"); AddButton(ContentKeyFunctions.OpenCraftingMenu, "Open crafting menu");
AddButton(ContentKeyFunctions.OpenInventoryMenu, "Open inventory"); AddButton(ContentKeyFunctions.OpenInventoryMenu, "Open inventory");
AddButton(ContentKeyFunctions.OpenTutorial, "Open tutorial"); AddButton(ContentKeyFunctions.OpenTutorial, "Open tutorial");
AddButton(ContentKeyFunctions.OpenActionsMenu, "Open action menu");
AddButton(ContentKeyFunctions.OpenEntitySpawnWindow, "Open entity spawn menu"); AddButton(ContentKeyFunctions.OpenEntitySpawnWindow, "Open entity spawn menu");
AddButton(ContentKeyFunctions.OpenSandboxWindow, "Open sandbox menu"); AddButton(ContentKeyFunctions.OpenSandboxWindow, "Open sandbox menu");
AddButton(ContentKeyFunctions.OpenTileSpawnWindow, "Open tile spawn menu"); AddButton(ContentKeyFunctions.OpenTileSpawnWindow, "Open tile spawn menu");
@@ -169,6 +169,18 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.TakeScreenshot, "Take screenshot"); AddButton(ContentKeyFunctions.TakeScreenshot, "Take screenshot");
AddButton(ContentKeyFunctions.TakeScreenshotNoUI, "Take screenshot (without UI)"); AddButton(ContentKeyFunctions.TakeScreenshotNoUI, "Take screenshot (without UI)");
AddHeader("Hotbar");
AddButton(ContentKeyFunctions.Hotbar1, "Hotbar slot 1");
AddButton(ContentKeyFunctions.Hotbar2, "Hotbar slot 2");
AddButton(ContentKeyFunctions.Hotbar3, "Hotbar slot 3");
AddButton(ContentKeyFunctions.Hotbar4, "Hotbar slot 4");
AddButton(ContentKeyFunctions.Hotbar5, "Hotbar slot 5");
AddButton(ContentKeyFunctions.Hotbar6, "Hotbar slot 6");
AddButton(ContentKeyFunctions.Hotbar7, "Hotbar slot 7");
AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8");
AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9");
AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0");
AddHeader("Map Editor"); AddHeader("Map Editor");
AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object"); AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object");
AddButton(EngineKeyFunctions.EditorCancelPlace, "Cancel placement"); AddButton(EngineKeyFunctions.EditorCancelPlace, "Cancel placement");

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Client.GameObjects.EntitySystems; using Content.Client.GameObjects.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Client.Utility; using Content.Client.Utility;
using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.Interfaces.ResourceManagement;
@@ -15,10 +16,19 @@ namespace Content.Client.UserInterface.Stylesheets
{ {
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel"; public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel"; public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
public const string StyleClassHotbarPanel = "HotbarPanel";
public const string StyleClassTooltipPanel = "tooltipBox"; public const string StyleClassTooltipPanel = "tooltipBox";
public const string StyleClassTooltipAlertTitle = "tooltipAlertTitle"; public const string StyleClassTooltipAlertTitle = "tooltipAlertTitle";
public const string StyleClassTooltipAlertDescription = "tooltipAlertDesc"; public const string StyleClassTooltipAlertDescription = "tooltipAlertDesc";
public const string StyleClassTooltipAlertCooldown = "tooltipAlertCooldown"; public const string StyleClassTooltipAlertCooldown = "tooltipAlertCooldown";
public const string StyleClassTooltipActionTitle = "tooltipActionTitle";
public const string StyleClassTooltipActionDescription = "tooltipActionDesc";
public const string StyleClassTooltipActionCooldown = "tooltipActionCooldown";
public const string StyleClassTooltipActionRequirements = "tooltipActionCooldown";
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
public const string StyleClassActionSearchBox = "actionSearchBox";
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
public const string StyleClassSliderRed = "Red"; public const string StyleClassSliderRed = "Red";
public const string StyleClassSliderGreen = "Green"; public const string StyleClassSliderGreen = "Green";
@@ -60,6 +70,8 @@ namespace Content.Client.UserInterface.Stylesheets
var notoSansItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12); var notoSansItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
var notoSansBold12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 12); var notoSansBold12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 12);
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14); var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
var notoSansDisplayBold16 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 16);
var notoSans15 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 15);
var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16); var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16);
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16); var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18); var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
@@ -95,6 +107,61 @@ namespace Content.Client.UserInterface.Stylesheets
}; };
borderedTransparentWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2); borderedTransparentWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2);
var hotbarBackground = new StyleBoxTexture
{
Texture = borderedWindowBackgroundTex,
};
hotbarBackground.SetPatchMargin(StyleBox.Margin.All, 2);
hotbarBackground.SetExpandMargin(StyleBox.Margin.All, 4);
var buttonRectTex = resCache.GetTexture("/Textures/Interface/Nano/light_panel_background_bordered.png");
var buttonRect = new StyleBoxTexture(BaseButton)
{
Texture = buttonRectTex
};
buttonRect.SetPatchMargin(StyleBox.Margin.All, 2);
buttonRect.SetPadding(StyleBox.Margin.All, 2);
buttonRect.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
buttonRect.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
var buttonRectHover = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorHovered
};
var buttonRectPressed = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorPressed
};
var buttonRectDisabled = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorDisabled
};
var buttonRectActionMenuItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_light_thin_border.png");
var buttonRectActionMenuRevokedItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_red_thin_border.png");
var buttonRectActionMenuItem = new StyleBoxTexture(BaseButton)
{
Texture = buttonRectActionMenuItemTex
};
buttonRectActionMenuItem.SetPatchMargin(StyleBox.Margin.All, 2);
buttonRectActionMenuItem.SetPadding(StyleBox.Margin.All, 2);
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
var buttonRectActionMenuItemRevoked = new StyleBoxTexture(buttonRectActionMenuItem)
{
Texture = buttonRectActionMenuRevokedItemTex
};
var buttonRectActionMenuItemHover = new StyleBoxTexture(buttonRectActionMenuItem)
{
Modulate = ButtonColorHovered
};
var buttonRectActionMenuItemPressed = new StyleBoxTexture(buttonRectActionMenuItem)
{
Modulate = ButtonColorPressed
};
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png"); var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png"); var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
@@ -105,6 +172,14 @@ namespace Content.Client.UserInterface.Stylesheets
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3); lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
var actionSearchBox = new StyleBoxTexture
{
Texture = actionSearchBoxTex,
};
actionSearchBox.SetPatchMargin(StyleBox.Margin.All, 3);
actionSearchBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var tabContainerPanelTex = resCache.GetTexture("/Textures/Interface/Nano/tabcontainer_panel.png"); var tabContainerPanelTex = resCache.GetTexture("/Textures/Interface/Nano/tabcontainer_panel.png");
var tabContainerPanel = new StyleBoxTexture var tabContainerPanel = new StyleBoxTexture
{ {
@@ -280,6 +355,12 @@ namespace Content.Client.UserInterface.Stylesheets
{ {
new StyleProperty(PanelContainer.StylePropertyPanel, borderedTransparentWindowBackground), new StyleProperty(PanelContainer.StylePropertyPanel, borderedTransparentWindowBackground),
}), }),
// Hotbar background
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassHotbarPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, hotbarBackground),
}),
// Window header. // Window header.
new StyleRule( new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null), new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null),
@@ -376,6 +457,43 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font-color", Color.FromHex("#E5E5E581")), new StyleProperty("font-color", Color.FromHex("#E5E5E581")),
}), }),
// action slot hotbar buttons
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRect),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectHover),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectPressed),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassDisabled}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectDisabled),
}),
// action menu item buttons
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItem),
}),
// we don't actually disable the action menu items, only change their style based on the underlying action being revoked
new StyleRule(new SelectorElement(typeof(ActionMenuItem), new [] {StyleClassActionMenuItemRevoked}, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemRevoked),
}),
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemHover),
}),
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemPressed),
}),
// Main menu: Make those buttons bigger. // Main menu: Make those buttons bigger.
new StyleRule(new SelectorChild( new StyleRule(new SelectorChild(
new SelectorElement(typeof(Button), null, "mainMenu", null), new SelectorElement(typeof(Button), null, "mainMenu", null),
@@ -413,6 +531,13 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font-color", Color.Gray), new StyleProperty("font-color", Color.Gray),
}), }),
// Action searchbox lineedit
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
new[]
{
new StyleProperty(LineEdit.StylePropertyStyleBox, actionSearchBox),
}),
// TabContainer // TabContainer
new StyleRule(new SelectorElement(typeof(TabContainer), null, null, null), new StyleRule(new SelectorElement(typeof(TabContainer), null, null, null),
new[] new[]
@@ -531,6 +656,30 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font", notoSans16) new StyleProperty("font", notoSans16)
}), }),
// action tooltip
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionTitle}, null, null), new[]
{
new StyleProperty("font", notoSansBold16)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionDescription}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionCooldown}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionRequirements}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
// hotbar slot
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassHotbarSlotNumber}, null, null), new[]
{
new StyleProperty("font", notoSansDisplayBold16)
}),
// Entity tooltip // Entity tooltip
new StyleRule( new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null, new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,

View File

@@ -74,6 +74,7 @@ Smart equip from belt: [color=#a4885c]{25}[/color]
Open inventory: [color=#a4885c]{7}[/color] Open inventory: [color=#a4885c]{7}[/color]
Open character window: [color=#a4885c]{8}[/color] Open character window: [color=#a4885c]{8}[/color]
Open crafting window: [color=#a4885c]{9}[/color] Open crafting window: [color=#a4885c]{9}[/color]
Open action menu: [color=#a4885c]{33}[/color]
Focus chat: [color=#a4885c]{10}[/color] Focus chat: [color=#a4885c]{10}[/color]
Focus OOC: [color=#a4885c]{26}[/color] Focus OOC: [color=#a4885c]{26}[/color]
Focus Admin Chat: [color=#a4885c]{27}[/color] Focus Admin Chat: [color=#a4885c]{27}[/color]
@@ -94,7 +95,18 @@ Toggle debug overlay: [color=#a4885c]{18}[/color]
Toggle entity spawner: [color=#a4885c]{19}[/color] Toggle entity spawner: [color=#a4885c]{19}[/color]
Toggle tile spawner: [color=#a4885c]{20}[/color] Toggle tile spawner: [color=#a4885c]{20}[/color]
Toggle sandbox window: [color=#a4885c]{21}[/color] Toggle sandbox window: [color=#a4885c]{21}[/color]
Toggle admin menu [color=#a4885c]{31}[/color]", Toggle admin menu [color=#a4885c]{31}[/color]
Hotbar slot 1: [color=#a4885c]{34}[/color]
Hotbar slot 2: [color=#a4885c]{35}[/color]
Hotbar slot 3: [color=#a4885c]{36}[/color]
Hotbar slot 4: [color=#a4885c]{37}[/color]
Hotbar slot 5: [color=#a4885c]{38}[/color]
Hotbar slot 6: [color=#a4885c]{39}[/color]
Hotbar slot 7: [color=#a4885c]{40}[/color]
Hotbar slot 8: [color=#a4885c]{41}[/color]
Hotbar slot 9: [color=#a4885c]{42}[/color]
Hotbar slot 0: [color=#a4885c]{43}[/color]
",
Key(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight), Key(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight),
Key(SwapHands), Key(SwapHands),
Key(ActivateItemInHand), Key(ActivateItemInHand),
@@ -124,7 +136,18 @@ Toggle admin menu [color=#a4885c]{31}[/color]",
Key(TryPullObject), Key(TryPullObject),
Key(MovePulledObject), Key(MovePulledObject),
Key(OpenAdminMenu), Key(OpenAdminMenu),
Key(ReleasePulledObject))); Key(ReleasePulledObject),
Key(OpenActionsMenu),
Key(Hotbar1),
Key(Hotbar2),
Key(Hotbar3),
Key(Hotbar4),
Key(Hotbar5),
Key(Hotbar6),
Key(Hotbar7),
Key(Hotbar8),
Key(Hotbar9),
Key(Hotbar0)));
//Gameplay //Gameplay
VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" }); VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" });

View File

@@ -0,0 +1,172 @@
using Robust.Client.Interfaces.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Utility
{
/// <summary>
/// Helper for implementing drag and drop interactions.
///
/// The basic flow for a drag drop interaction as per this helper is:
/// 1. User presses mouse down on something (using class should communicate this to helper by calling MouseDown()).
/// 2. User continues to hold the mouse down and moves the mouse outside of the defined
/// deadzone. OnBeginDrag is invoked to see if a drag should be initiated. If so, initiates a drag.
/// If user didn't move the mouse beyond the deadzone the drag is not initiated (OnEndDrag invoked).
/// 3. Every Update/FrameUpdate, OnContinueDrag is invoked.
/// 4. User lifts mouse up. This is not handled by DragDropHelper. The using class of the helper should
/// do whatever they want and then end the drag by calling EndDrag() (which invokes OnEndDrag).
///
/// If for any reason the drag is ended, OnEndDrag is invoked.
/// </summary>
/// <typeparam name="T">thing being dragged and dropped</typeparam>
public class DragDropHelper<T>
{
private const float DefaultDragDeadzone = 2f;
private readonly IInputManager _inputManager;
private readonly OnBeginDrag _onBeginDrag;
private readonly OnEndDrag _onEndDrag;
private readonly OnContinueDrag _onContinueDrag;
private readonly float _deadzone;
/// <summary>
/// Convenience method, current mouse screen position as provided by inputmanager.
/// </summary>
public Vector2 MouseScreenPosition => _inputManager.MouseScreenPosition;
/// <summary>
/// True if initiated a drag and currently dragging something.
/// I.e. this will be false if we've just had a mousedown over something but the mouse
/// has not moved outside of the drag deadzone.
/// </summary>
public bool IsDragging => _state == DragState.Dragging;
/// <summary>
/// Current thing being dragged or which mouse button is being held down on.
/// </summary>
public T Dragged { get; private set; }
// screen pos where the mouse down began for the drag
private Vector2 _mouseDownScreenPos;
private DragState _state = DragState.NotDragging;
private enum DragState : byte
{
NotDragging,
// not dragging yet, waiting to see
// if they hold for long enough
MouseDown,
// currently dragging something
Dragging,
}
/// <param name="onBeginDrag"><see cref="OnBeginDrag"/></param>
/// <param name="onContinueDrag"><see cref="OnContinueDrag"/></param>
/// <param name="onEndDrag"><see cref="OnEndDrag"/></param>
/// <param name="deadzone">drag will be triggered when mouse leaves
/// this deadzone around the mousedown position</param>
public DragDropHelper(OnBeginDrag onBeginDrag, OnContinueDrag onContinueDrag,
OnEndDrag onEndDrag, float deadzone = DefaultDragDeadzone)
{
_deadzone = deadzone;
_inputManager = IoCManager.Resolve<IInputManager>();
_onBeginDrag = onBeginDrag;
_onEndDrag = onEndDrag;
_onContinueDrag = onContinueDrag;
}
/// <summary>
/// Tell the helper that the mouse button was pressed down on
/// a target, thus a drag has the possibility to begin for this target.
/// Assumes current mouse screen position is the location the mouse was clicked.
///
/// EndDrag should be called when the drag is done.
/// </summary>
public void MouseDown(T target)
{
if (_state != DragState.NotDragging)
{
EndDrag();
}
Dragged = target;
_state = DragState.MouseDown;
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
}
/// <summary>
/// Stop the current drag / drop operation no matter what state it is in.
/// </summary>
public void EndDrag()
{
Dragged = default;
_state = DragState.NotDragging;
_onEndDrag.Invoke();
}
private void StartDragging()
{
if (_onBeginDrag.Invoke())
{
_state = DragState.Dragging;
}
else
{
EndDrag();
}
}
/// <summary>
/// Should be invoked by using class every FrameUpdate or Update.
/// </summary>
public void Update(float frameTime)
{
switch (_state)
{
// check if dragging should begin
case DragState.MouseDown:
{
var screenPos = _inputManager.MouseScreenPosition;
if ((_mouseDownScreenPos - screenPos).Length > _deadzone)
{
StartDragging();
}
break;
}
case DragState.Dragging:
{
if (!_onContinueDrag.Invoke(frameTime))
{
EndDrag();
}
break;
}
}
}
}
/// <summary>
/// Invoked when a drag is confirmed and going to be initiated. Implementation should
/// typically set the drag shadow texture based on the target.
/// </summary>
/// <returns>true if drag should begin, false to end.</returns>
public delegate bool OnBeginDrag();
/// <summary>
/// Invoked every frame when drag is ongoing. Typically implementation should
/// make the drag shadow follow the mouse position.
/// </summary>
/// <returns>true if drag should continue, false to end.</returns>
public delegate bool OnContinueDrag(float frameTime);
/// <summary>
/// invoked when
/// the drag drop is ending for any reason. This
/// should typically just clear the drag shadow.
/// </summary>
public delegate void OnEndDrag();
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using NUnit.Framework;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player;
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{
[TestFixture]
[TestOf(typeof(SharedActionsComponent))]
[TestOf(typeof(ClientActionsComponent))]
[TestOf(typeof(ServerActionsComponent))]
public class ActionsComponentTests : ContentIntegrationTest
{
[Test]
public async Task GrantsAndRevokesActionsTest()
{
var (client, server) = await StartConnectedServerClientPair();
await server.WaitIdleAsync();
await client.WaitIdleAsync();
var serverPlayerManager = server.ResolveDependency<Robust.Server.Interfaces.Player.IPlayerManager>();
var innateActions = new List<ActionType>();
await server.WaitAssertion(() =>
{
var player = serverPlayerManager.GetAllPlayers().Single();
var playerEnt = player.AttachedEntity;
var actionsComponent = playerEnt.GetComponent<ServerActionsComponent>();
// 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);
}
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<IPlayerManager>();
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
var expectedOrder = new List<ActionType>();
await client.WaitAssertion(() =>
{
var local = clientPlayerMgr.LocalPlayer;
var controlled = local.ControlledEntity;
var actionsComponent = controlled.GetComponent<ClientActionsComponent>();
// 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.Null);
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.Null);
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 player = serverPlayerManager.GetAllPlayers().Single();
var playerEnt = player.AttachedEntity;
var actionsComponent = playerEnt.GetComponent<ServerActionsComponent>();
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 = controlled.GetComponent<ClientActionsComponent>();
// 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 var state), 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.Null);
Assert.That(slot.Action, Is.Not.Null);
var asAction = slot.Action as ActionPrototype;
Assert.That(asAction, Is.Not.Null);
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.Null);
Assert.That(slot.Action, Is.Null);
Assert.That(slot.ActionEnabled, Is.False);
continue;
}
}
});
}
}
}

View File

@@ -2,14 +2,12 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Client.GameObjects.Components.Mobs; using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Client.UserInterface.Controls;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert; using Content.Shared.Alert;
using NUnit.Framework; using NUnit.Framework;
using Robust.Client.Interfaces.UserInterface; using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Interfaces.Map;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{ {

View File

@@ -25,7 +25,7 @@ namespace Content.IntegrationTests.Tests.Gravity
name: HumanDummy name: HumanDummy
id: HumanDummy id: HumanDummy
components: components:
- type: AlertsUI - type: Alerts
"; ";
[Test] [Test]
public async Task WeightlessStatusTest() public async Task WeightlessStatusTest()

View File

@@ -0,0 +1,39 @@
using Content.Server.Utility;
using Content.Shared.Actions;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
/// <summary>
/// Just shows a popup message.asd
/// </summary>
[UsedImplicitly]
public class DebugInstant : IInstantAction, IInstantItemAction
{
public string Message { get; private set; }
public float Cooldown { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.Message, "message", "Instant action used.");
serializer.DataField(this, x => x.Cooldown, "cooldown", 0);
}
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));
}
}
}

View File

@@ -0,0 +1,28 @@
using Content.Server.Utility;
using Content.Shared.Actions;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
[UsedImplicitly]
public class DebugTargetEntity : ITargetEntityAction, ITargetEntityItemAction
{
public void ExposeData(ObjectSerializer serializer)
{
}
public void DoTargetEntityAction(TargetEntityItemActionEventArgs args)
{
args.Performer.PopupMessageEveryone(args.Item.Name + ": Clicked " +
args.Target.Name);
}
public void DoTargetEntityAction(TargetEntityActionEventArgs args)
{
args.Performer.PopupMessageEveryone("Clicked " +
args.Target.Name);
}
}
}

View File

@@ -0,0 +1,29 @@
using Content.Server.Utility;
using Content.Shared.Actions;
using JetBrains.Annotations;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
[UsedImplicitly]
public class DebugTargetPoint : ITargetPointAction, ITargetPointItemAction
{
public void ExposeData(ObjectSerializer serializer)
{
}
public void DoTargetPointAction(TargetPointItemActionEventArgs args)
{
args.Performer.PopupMessageEveryone(args.Item.Name + ": Clicked local position " +
args.Target);
}
public void DoTargetPointAction(TargetPointActionEventArgs args)
{
args.Performer.PopupMessageEveryone("Clicked local position " +
args.Target);
}
}
}

View File

@@ -0,0 +1,48 @@
using Content.Server.Utility;
using Content.Shared.Actions;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
[UsedImplicitly]
public class DebugToggle : IToggleAction, IToggleItemAction
{
public string MessageOn { get; private set; }
public string MessageOff { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.MessageOn, "messageOn", "on!");
serializer.DataField(this, x => x.MessageOff, "messageOff", "off!");
}
public bool DoToggleAction(ToggleItemActionEventArgs args)
{
if (args.ToggledOn)
{
args.Performer.PopupMessageEveryone(args.Item.Name + ": " + MessageOn);
}
else
{
args.Performer.PopupMessageEveryone(args.Item.Name + ": " +MessageOff);
}
return true;
}
public bool DoToggleAction(ToggleActionEventArgs args)
{
if (args.ToggledOn)
{
args.Performer.PopupMessageEveryone(MessageOn);
}
else
{
args.Performer.PopupMessageEveryone(MessageOff);
}
return true;
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions;
using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Preferences;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.Random;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
namespace Content.Server.Actions
{
[UsedImplicitly]
public class ScreamAction : IInstantAction
{
private const float Variation = 0.125f;
private const float Volume = 4f;
private List<string> _male;
private List<string> _female;
private string _wilhelm;
/// seconds
private float _cooldown;
private IRobustRandom _random;
public ScreamAction()
{
_random = IoCManager.Resolve<IRobustRandom>();
}
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref _male, "male", null);
serializer.DataField(ref _female, "female", null);
serializer.DataField(ref _wilhelm, "wilhelm", null);
serializer.DataField(ref _cooldown, "cooldown", 10);
}
public void DoInstantAction(InstantActionEventArgs args)
{
if (!ActionBlockerSystem.CanSpeak(args.Performer)) return;
if (!args.Performer.TryGetComponent<HumanoidAppearanceComponent>(out var humanoid)) return;
if (!args.Performer.TryGetComponent<SharedActionsComponent>(out var actions)) return;
if (_random.Prob(.01f) && !string.IsNullOrWhiteSpace(_wilhelm))
{
EntitySystem.Get<AudioSystem>().PlayFromEntity(_wilhelm, args.Performer, AudioParams.Default.WithVolume(Volume));
}
else
{
switch (humanoid.Sex)
{
case Sex.Male:
if (_male == null) break;
EntitySystem.Get<AudioSystem>().PlayFromEntity(_random.Pick(_male), args.Performer,
AudioHelpers.WithVariation(Variation).WithVolume(Volume));
break;
case Sex.Female:
if (_female == null) break;
EntitySystem.Get<AudioSystem>().PlayFromEntity(_random.Pick(_female), args.Performer,
AudioHelpers.WithVariation(Variation).WithVolume(Volume));
break;
default:
throw new ArgumentOutOfRangeException();
}
}
actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
}
}
}

View File

@@ -0,0 +1,24 @@
using Content.Server.GameObjects.Components.Atmos;
using Content.Shared.Alert;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Alert.Click
{
/// <summary>
/// Resist fire
/// </summary>
[UsedImplicitly]
public class ResistFire : IAlertClick
{
public void ExposeData(ObjectSerializer serializer) { }
public void AlertClicked(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out FlammableComponent flammable))
{
flammable.Resist();
}
}
}
}

View File

@@ -0,0 +1,24 @@
using Content.Server.GameObjects.Components.Movement;
using Content.Shared.Alert;
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Server.Alert.Click
{
/// <summary>
/// Stop piloting shuttle
/// </summary>
[UsedImplicitly]
public class StopPiloting : IAlertClick
{
public void ExposeData(ObjectSerializer serializer) { }
public void AlertClicked(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out ShuttleControllerComponent controller))
{
controller.RemoveController();
}
}
}
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Pulling;
using Content.Shared.GameObjects.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Serialization;
namespace Content.Server.Alert.Click
{
/// <summary>
/// Stop pulling something
/// </summary>
[UsedImplicitly]
public class StopPulling : IAlertClick
{
public void ExposeData(ObjectSerializer serializer) { }
public void AlertClicked(ClickAlertEventArgs args)
{
EntitySystem
.Get<SharedPullingSystem>()
.GetPulled(args.Player)?
.GetComponentOrNull<SharedPullableComponent>()?
.TryStopPull();
}
}
}

View File

@@ -0,0 +1,24 @@
using Content.Server.GameObjects.Components.Buckle;
using Content.Shared.Alert;
using Robust.Shared.Serialization;
using JetBrains.Annotations;
namespace Content.Server.Alert.Click
{
/// <summary>
/// Unbuckles if player is currently buckled.
/// </summary>
[UsedImplicitly]
public class Unbuckle : IAlertClick
{
public void ExposeData(ObjectSerializer serializer) { }
public void AlertClicked(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out BuckleComponent buckle))
{
buckle.TryUnbuckle(args.Player);
}
}
}
}

View File

@@ -0,0 +1,65 @@
#nullable enable
using System;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
namespace Content.Server.Commands.Actions
{
[AdminCommand(AdminFlags.Debug)]
public sealed class CooldownAction : IClientCommand
{
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, IPlayerSession? player, string[] args)
{
if (player == null) return;
var attachedEntity = player.AttachedEntity;
if (args.Length > 2)
{
var target = args[2];
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
}
if (attachedEntity == null) return;
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
{
shell.SendText(player, "user has no actions component");
return;
}
var actionTypeRaw = args[0];
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
{
shell.SendText(player, "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.SendText(player, "unrecognized actionType " + actionType);
return;
}
var cooldownStart = IoCManager.Resolve<IGameTiming>().CurTime;
if (!uint.TryParse(args[1], out var seconds))
{
shell.SendText(player, "cannot parse seconds: " + args[1]);
return;
}
var cooldownEnd = cooldownStart.Add(TimeSpan.FromSeconds(seconds));
actionsComponent.Cooldown(action.ActionType, (cooldownStart, cooldownEnd));
}
}
}

View File

@@ -0,0 +1,52 @@
#nullable enable
using System;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
namespace Content.Server.Commands.Actions
{
[AdminCommand(AdminFlags.Debug)]
public sealed class GrantAction : IClientCommand
{
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, IPlayerSession? player, string[] args)
{
if (player == null) return;
var attachedEntity = player.AttachedEntity;
if (args.Length > 1)
{
var target = args[1];
if (!Commands.CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
}
if (attachedEntity == null) return;
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
{
shell.SendText(player, "user has no actions component");
return;
}
var actionTypeRaw = args[0];
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
{
shell.SendText(player, "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.SendText(player, "unrecognized actionType " + actionType);
return;
}
actionsComponent.Grant(action.ActionType);
}
}
}

View File

@@ -0,0 +1,53 @@
#nullable enable
using System;
using Content.Server.Administration;
using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions;
using Content.Shared.Administration;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player;
using Robust.Shared.IoC;
namespace Content.Server.Commands.Actions
{
[AdminCommand(AdminFlags.Debug)]
public sealed class RevokeAction : IClientCommand
{
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, IPlayerSession? player, string[] args)
{
if (player == null) return;
var attachedEntity = player.AttachedEntity;
if (args.Length > 1)
{
var target = args[1];
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
}
if (attachedEntity == null) return;
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
{
shell.SendText(player, "user has no actions component");
return;
}
var actionTypeRaw = args[0];
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
{
shell.SendText(player, "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.SendText(player, "unrecognized actionType " + actionType);
return;
}
actionsComponent.Revoke(action.ActionType);
}
}
}

View File

@@ -19,22 +19,20 @@ namespace Content.Server.Commands.Alerts
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
{ {
var attachedEntity = player?.AttachedEntity; if (player?.AttachedEntity == null)
if (attachedEntity == null)
{ {
shell.SendText(player, "You don't have an entity."); shell.SendText(player, "You don't have an entity.");
return; return;
} }
var attachedEntity = player.AttachedEntity;
if (args.Length > 1) if (args.Length > 1)
{ {
var target = args[1]; var target = args[1];
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return; if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
} }
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity)) return;
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent)) if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent))
{ {
shell.SendText(player, "user has no alerts component"); shell.SendText(player, "user has no alerts component");

View File

@@ -39,9 +39,6 @@ namespace Content.Server.Commands.Alerts
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return; if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
} }
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity))
return;
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent)) if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent))
{ {
shell.SendText(player, "user has no alerts component"); shell.SendText(player, "user has no alerts component");

View File

@@ -1,4 +1,6 @@
using System; #nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Robust.Server.Interfaces.Console; using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
@@ -17,7 +19,7 @@ namespace Content.Server.Commands
/// sending a failure to the performer if unable to. /// sending a failure to the performer if unable to.
/// </summary> /// </summary>
public static bool TryGetSessionByUsernameOrId(IConsoleShell shell, public static bool TryGetSessionByUsernameOrId(IConsoleShell shell,
string usernameOrId, IPlayerSession performer, out IPlayerSession session) string usernameOrId, IPlayerSession performer, [NotNullWhen(true)] out IPlayerSession? session)
{ {
var plyMgr = IoCManager.Resolve<IPlayerManager>(); var plyMgr = IoCManager.Resolve<IPlayerManager>();
if (plyMgr.TryGetSessionByUsername(usernameOrId, out session)) return true; if (plyMgr.TryGetSessionByUsername(usernameOrId, out session)) return true;
@@ -37,7 +39,7 @@ namespace Content.Server.Commands
/// sending a failure to the performer if unable to. /// sending a failure to the performer if unable to.
/// </summary> /// </summary>
public static bool TryGetAttachedEntityByUsernameOrId(IConsoleShell shell, public static bool TryGetAttachedEntityByUsernameOrId(IConsoleShell shell,
string usernameOrId, IPlayerSession performer, out IEntity attachedEntity) string usernameOrId, IPlayerSession performer, [NotNullWhen(true)] out IEntity? attachedEntity)
{ {
attachedEntity = null; attachedEntity = null;
if (!TryGetSessionByUsernameOrId(shell, usernameOrId, performer, out var session)) return false; if (!TryGetSessionByUsernameOrId(shell, usernameOrId, performer, out var session)) return false;
@@ -50,17 +52,5 @@ namespace Content.Server.Commands
attachedEntity = session.AttachedEntity; attachedEntity = session.AttachedEntity;
return true; return true;
} }
/// <summary>
/// Checks if attached entity is null, returning false and sending a message
/// to performer if not.
/// </summary>
public static bool ValidateAttachedEntity(IConsoleShell shell, IPlayerSession performer, IEntity attachedEntity)
{
if (attachedEntity != null) return true;
shell.SendText(performer, "User has no attached entity.");
return false;
}
} }
} }

View File

@@ -10,6 +10,7 @@ using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.GameTicking;
using Content.Server.Interfaces.PDA; using Content.Server.Interfaces.PDA;
using Content.Server.Sandbox; using Content.Server.Sandbox;
using Content.Shared.Actions;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Alert; using Content.Shared.Alert;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -81,6 +82,7 @@ namespace Content.Server
_gameTicker.Initialize(); _gameTicker.Initialize();
IoCManager.Resolve<RecipeManager>().Initialize(); IoCManager.Resolve<RecipeManager>().Initialize();
IoCManager.Resolve<AlertManager>().Initialize(); IoCManager.Resolve<AlertManager>().Initialize();
IoCManager.Resolve<ActionManager>().Initialize();
IoCManager.Resolve<BlackboardManager>().Initialize(); IoCManager.Resolve<BlackboardManager>().Initialize();
IoCManager.Resolve<ConsiderationsManager>().Initialize(); IoCManager.Resolve<ConsiderationsManager>().Initialize();
IoCManager.Resolve<IPDAUplinkManager>().Initialize(); IoCManager.Resolve<IPDAUplinkManager>().Initialize();

View File

@@ -100,7 +100,7 @@ namespace Content.Server.GameObjects.Components.Atmos
return; return;
} }
status?.ShowAlert(AlertType.Fire, onClickAlert: OnClickAlert); status?.ShowAlert(AlertType.Fire);
if (FireStacks > 0) if (FireStacks > 0)
{ {
@@ -152,14 +152,6 @@ namespace Content.Server.GameObjects.Components.Atmos
} }
} }
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out FlammableComponent flammable))
{
flammable.Resist();
}
}
public void CollideWith(IEntity collidedWith) public void CollideWith(IEntity collidedWith)
{ {
if (!collidedWith.TryGetComponent(out FlammableComponent otherFlammable)) if (!collidedWith.TryGetComponent(out FlammableComponent otherFlammable))

View File

@@ -5,18 +5,22 @@ using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Body.Respiratory; using Content.Server.GameObjects.Components.Body.Respiratory;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Actions;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Audio; using Content.Shared.Audio;
using Content.Shared.GameObjects.Components.Atmos.GasTank; using Content.Shared.GameObjects.Components.Atmos.GasTank;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects.Components.UserInterface; using Robust.Server.GameObjects.Components.UserInterface;
using Robust.Server.GameObjects.EntitySystems; using Robust.Server.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.GameObjects;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization; using Robust.Shared.Localization;
@@ -37,6 +41,8 @@ namespace Content.Server.GameObjects.Components.Atmos
private int _integrity = 3; private int _integrity = 3;
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
[ViewVariables] private BoundUserInterface? _userInterface; [ViewVariables] private BoundUserInterface? _userInterface;
[ViewVariables] public GasMixture? Air { get; set; } [ViewVariables] public GasMixture? Air { get; set; }
@@ -191,14 +197,18 @@ namespace Content.Server.GameObjects.Components.Atmos
private void UpdateUserInterface(bool initialUpdate = false) private void UpdateUserInterface(bool initialUpdate = false)
{ {
var internals = GetInternalsComponent();
_userInterface?.SetState( _userInterface?.SetState(
new GasTankBoundUserInterfaceState new GasTankBoundUserInterfaceState
{ {
TankPressure = Air?.Pressure ?? 0, TankPressure = Air?.Pressure ?? 0,
OutputPressure = initialUpdate ? OutputPressure : (float?) null, OutputPressure = initialUpdate ? OutputPressure : (float?) null,
InternalsConnected = IsConnected, InternalsConnected = IsConnected,
CanConnectInternals = IsFunctional && GetInternalsComponent() != null CanConnectInternals = IsFunctional && internals != null
}); });
if (internals == null) return;
_itemActions?.GrantOrUpdate(ItemActionType.ToggleInternals, IsFunctional, IsConnected);
} }
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message) private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
@@ -214,8 +224,9 @@ namespace Content.Server.GameObjects.Components.Atmos
} }
} }
private void ToggleInternals() internal void ToggleInternals()
{ {
if (!ActionBlockerSystem.CanUse(GetInternalsComponent()?.Owner)) return;
if (IsConnected) if (IsConnected)
{ {
DisconnectFromInternals(); DisconnectFromInternals();
@@ -311,6 +322,11 @@ namespace Content.Server.GameObjects.Components.Atmos
_integrity++; _integrity++;
} }
public void Dropped(DroppedEventArgs eventArgs)
{
DisconnectFromInternals(eventArgs.User);
}
/// <summary> /// <summary>
/// Open interaction window /// Open interaction window
/// </summary> /// </summary>
@@ -341,10 +357,21 @@ namespace Content.Server.GameObjects.Components.Atmos
component.OpenInterface(actor.playerSession); component.OpenInterface(actor.playerSession);
} }
} }
}
public void Dropped(DroppedEventArgs eventArgs) [UsedImplicitly]
public class ToggleInternalsAction : IToggleItemAction
{ {
DisconnectFromInternals(eventArgs.User); public void ExposeData(ObjectSerializer serializer) {}
public bool DoToggleAction(ToggleItemActionEventArgs args)
{
if (!args.Item.TryGetComponent<GasTankComponent>(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;
} }
} }
} }

View File

@@ -108,8 +108,7 @@ namespace Content.Server.GameObjects.Components.Buckle
if (Buckled) if (Buckled)
{ {
_serverAlertsComponent.ShowAlert(BuckledTo != null ? BuckledTo.BuckledAlertType : AlertType.Buckled, _serverAlertsComponent.ShowAlert(BuckledTo?.BuckledAlertType ?? AlertType.Buckled);
onClickAlert: OnClickAlert);
} }
else else
{ {
@@ -117,14 +116,6 @@ namespace Content.Server.GameObjects.Components.Buckle
} }
} }
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out BuckleComponent? buckle))
{
buckle.TryUnbuckle(args.Player);
}
}
/// <summary> /// <summary>
/// Reattaches this entity to the strap, modifying its position and rotation. /// Reattaches this entity to the strap, modifying its position and rotation.

View File

@@ -30,6 +30,7 @@ namespace Content.Server.GameObjects.Components.GUI
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(IHandsComponent))] [ComponentReference(typeof(IHandsComponent))]
[ComponentReference(typeof(ISharedHandsComponent))] [ComponentReference(typeof(ISharedHandsComponent))]
[ComponentReference(typeof(SharedHandsComponent))]
public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
@@ -82,7 +83,7 @@ namespace Content.Server.GameObjects.Components.GUI
} }
} }
public bool IsHolding(IEntity entity) public override bool IsHolding(IEntity entity)
{ {
foreach (var hand in _hands) foreach (var hand in _hands)
{ {
@@ -165,6 +166,7 @@ namespace Content.Server.GameObjects.Components.GUI
} }
Dirty(); Dirty();
var success = hand.Container.Insert(item.Owner); var success = hand.Container.Insert(item.Owner);
if (success) if (success)
{ {
@@ -172,6 +174,9 @@ namespace Content.Server.GameObjects.Components.GUI
OnItemChanged?.Invoke(); OnItemChanged?.Invoke();
} }
_entitySystemManager.GetEntitySystem<InteractionSystem>().EquippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
_entitySystemManager.GetEntitySystem<InteractionSystem>().HandSelectedInteraction(Owner, item.Owner); _entitySystemManager.GetEntitySystem<InteractionSystem>().HandSelectedInteraction(Owner, item.Owner);
return success; return success;
@@ -266,6 +271,9 @@ namespace Content.Server.GameObjects.Components.GUI
return false; return false;
} }
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
if (doDropInteraction && !DroppedInteraction(item, false)) if (doDropInteraction && !DroppedInteraction(item, false))
return false; return false;
@@ -288,6 +296,61 @@ namespace Content.Server.GameObjects.Components.GUI
return true; return true;
} }
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (slot == null)
{
throw new ArgumentNullException(nameof(slot));
}
if (targetContainer == null)
{
throw new ArgumentNullException(nameof(targetContainer));
}
var hand = GetHand(slot);
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
{
return false;
}
if (!hand.Container.CanRemove(hand.Entity))
{
return false;
}
if (!targetContainer.CanInsert(hand.Entity))
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!hand.Container.Remove(hand.Entity))
{
throw new InvalidOperationException();
}
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
ToSharedHand(hand));
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
return false;
item.RemovedFromSlot();
if (!targetContainer.Insert(item.Owner))
{
throw new InvalidOperationException();
}
OnItemChanged?.Invoke();
Dirty();
return true;
}
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true) public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
{ {
if (entity == null) if (entity == null)
@@ -323,57 +386,6 @@ namespace Content.Server.GameObjects.Components.GUI
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction); return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
} }
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{
if (slot == null)
{
throw new ArgumentNullException(nameof(slot));
}
if (targetContainer == null)
{
throw new ArgumentNullException(nameof(targetContainer));
}
var hand = GetHand(slot);
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
{
return false;
}
if (!hand.Container.CanRemove(hand.Entity))
{
return false;
}
if (!targetContainer.CanInsert(hand.Entity))
{
return false;
}
var item = hand.Entity.GetComponent<ItemComponent>();
if (!hand.Container.Remove(hand.Entity))
{
throw new InvalidOperationException();
}
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
return false;
item.RemovedFromSlot();
if (!targetContainer.Insert(item.Owner))
{
throw new InvalidOperationException();
}
OnItemChanged?.Invoke();
Dirty();
return true;
}
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true) public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
{ {
if (entity == null) if (entity == null)
@@ -463,19 +475,28 @@ namespace Content.Server.GameObjects.Components.GUI
for (var i = 0; i < _hands.Count; i++) for (var i = 0; i < _hands.Count; i++)
{ {
var location = i == 0 var hand = _hands[i].ToShared(i, IndexToHandLocation(i));
? HandLocation.Right
: i == _hands.Count - 1
? HandLocation.Left
: HandLocation.Middle;
var hand = _hands[i].ToShared(i, location);
hands[i] = hand; hands[i] = hand;
} }
return new HandsComponentState(hands, ActiveHand); return new HandsComponentState(hands, ActiveHand);
} }
private HandLocation IndexToHandLocation(int index)
{
return index == 0
? HandLocation.Right
: index == _hands.Count - 1
? HandLocation.Left
: HandLocation.Middle;
}
private SharedHand ToSharedHand(Hand hand)
{
var index = _hands.IndexOf(hand);
return hand.ToShared(index, IndexToHandLocation(index));
}
public void SwapHands() public void SwapHands()
{ {
if (ActiveHand == null) if (ActiveHand == null)

View File

@@ -25,6 +25,7 @@ using static Content.Shared.GameObjects.Components.Inventory.SharedInventoryComp
namespace Content.Server.GameObjects.Components.GUI namespace Content.Server.GameObjects.Components.GUI
{ {
[RegisterComponent] [RegisterComponent]
[ComponentReference(typeof(SharedInventoryComponent))]
public class InventoryComponent : SharedInventoryComponent, IExAct, IEffectBlocker, IPressureProtection public class InventoryComponent : SharedInventoryComponent, IExAct, IEffectBlocker, IPressureProtection
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
@@ -572,5 +573,20 @@ namespace Content.Server.GameObjects.Components.GUI
} }
} }
} }
public override bool IsEquipped(IEntity item)
{
if (item == null) return false;
foreach (var containerSlot in _slotContainers.Values)
{
// we don't want a recursive check here
if (containerSlot.Contains(item))
{
return true;
}
}
return false;
}
} }
} }

View File

@@ -1,18 +1,26 @@
#nullable enable #nullable enable
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.Atmos;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Clothing; using Content.Server.GameObjects.Components.Items.Clothing;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power; using Content.Server.GameObjects.Components.Power;
using Content.Shared.Actions;
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels; using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.Verbs; using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Utility; using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Server.GameObjects.EntitySystems; using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.ComponentDependencies;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Localization; using Robust.Shared.Localization;
@@ -45,6 +53,8 @@ namespace Content.Server.GameObjects.Components.Interactable
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOnFailSound; [ViewVariables(VVAccess.ReadWrite)] public string? TurnOnFailSound;
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOffSound; [ViewVariables(VVAccess.ReadWrite)] public string? TurnOffSound;
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
/// <summary> /// <summary>
/// Client-side ItemStatus level /// Client-side ItemStatus level
/// </summary> /// </summary>
@@ -98,8 +108,9 @@ namespace Content.Server.GameObjects.Components.Interactable
/// Illuminates the light if it is not active, extinguishes it if it is active. /// Illuminates the light if it is not active, extinguishes it if it is active.
/// </summary> /// </summary>
/// <returns>True if the light's status was toggled, false otherwise.</returns> /// <returns>True if the light's status was toggled, false otherwise.</returns>
private bool ToggleStatus(IEntity user) public bool ToggleStatus(IEntity user)
{ {
if (!ActionBlockerSystem.CanUse(user)) return false;
return Activated ? TurnOff() : TurnOn(user); return Activated ? TurnOff() : TurnOn(user);
} }
@@ -112,6 +123,7 @@ namespace Content.Server.GameObjects.Components.Interactable
SetState(false); SetState(false);
Activated = false; Activated = false;
UpdateLightAction();
if (makeNoise) if (makeNoise)
{ {
@@ -132,6 +144,7 @@ namespace Content.Server.GameObjects.Components.Interactable
{ {
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner); if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
Owner.PopupMessage(user, Loc.GetString("Cell missing...")); Owner.PopupMessage(user, Loc.GetString("Cell missing..."));
UpdateLightAction();
return false; return false;
} }
@@ -142,10 +155,12 @@ namespace Content.Server.GameObjects.Components.Interactable
{ {
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner); if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
Owner.PopupMessage(user, Loc.GetString("Dead cell...")); Owner.PopupMessage(user, Loc.GetString("Dead cell..."));
UpdateLightAction();
return false; return false;
} }
Activated = true; Activated = true;
UpdateLightAction();
SetState(true); SetState(true);
if (TurnOnSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnSound, Owner); if (TurnOnSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnSound, Owner);
@@ -175,6 +190,11 @@ namespace Content.Server.GameObjects.Components.Interactable
} }
} }
private void UpdateLightAction()
{
_itemActions?.Toggle(ItemActionType.ToggleLight, Activated);
}
public void OnUpdate(float frameTime) public void OnUpdate(float frameTime)
{ {
if (Cell == null) if (Cell == null)
@@ -249,4 +269,17 @@ namespace Content.Server.GameObjects.Components.Interactable
} }
} }
} }
[UsedImplicitly]
public class ToggleLightAction : IToggleItemAction
{
public void ExposeData(ObjectSerializer serializer) {}
public bool DoToggleAction(ToggleItemActionEventArgs args)
{
if (!args.Item.TryGetComponent<HandheldLightComponent>(out var lightComponent)) return false;
if (lightComponent.Activated == args.ToggledOn) return false;
return lightComponent.ToggleStatus(args.Performer);
}
}
} }

View File

@@ -0,0 +1,35 @@
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.GameObjects.Components.Items
{
/// <summary>
/// Pops up a message when equipped / unequipped (including hands).
/// For debugging purposes.
/// </summary>
[RegisterComponent]
public class DebugEquipComponent : Component, IEquipped, IEquippedHand, IUnequipped, IUnequippedHand
{
public override string Name => "DebugEquip";
public void Equipped(EquippedEventArgs eventArgs)
{
eventArgs.User.PopupMessage("equipped " + Owner.Name);
}
public void EquippedHand(EquippedHandEventArgs eventArgs)
{
eventArgs.User.PopupMessage("equipped hand " + Owner.Name);
}
public void Unequipped(UnequippedEventArgs eventArgs)
{
eventArgs.User.PopupMessage("unequipped " + Owner.Name);
}
public void UnequippedHand(UnequippedHandEventArgs eventArgs)
{
eventArgs.User.PopupMessage("unequipped hand" + Owner.Name);
}
}
}

View File

@@ -0,0 +1,199 @@
#nullable enable
using System;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.Interfaces.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Network;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Players;
namespace Content.Server.GameObjects.Components.Mobs
{
[RegisterComponent]
[ComponentReference(typeof(SharedActionsComponent))]
public sealed class ServerActionsComponent : SharedActionsComponent
{
[Dependency] private readonly IServerEntityManager _entityManager = default!;
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));
}
var player = session.AttachedEntity;
if (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", player.Name,
attempt);
return;
}
if (actionState.IsOnCooldown(GameTiming))
{
Logger.DebugS("action", "user {0} attempted to use" +
" action {1} which is on cooldown", player.Name,
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}", player.Name,
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.TryGetEntity(targetEntityMsg.Target, out var entity))
{
Logger.DebugS("action", "user {0} attempted to" +
" perform target entity action {1} but could not find entity with " +
"provided uid {2}", player.Name, attempt.Action.Name,
targetEntityMsg.Target);
return;
}
if (!CheckRangeAndSetFacing(entity.Transform.Coordinates, player)) return;
attempt.DoTargetEntityAction(player, entity);
break;
case BehaviorType.None:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private IActionAttempt? ActionAttempt(BasePerformActionMessage message, ICommonSession session)
{
IActionAttempt? attempt;
switch (message)
{
case PerformActionMessage performActionMessage:
if (!ActionManager.TryGet(performActionMessage.ActionType, out var action))
{
Logger.DebugS("action", "user {0} attempted to perform" +
" unrecognized action {1}", session.AttachedEntity,
performActionMessage.ActionType);
return null;
}
attempt = new ActionAttempt(action);
break;
case PerformItemActionMessage performItemActionMessage:
if (!ActionManager.TryGet(performItemActionMessage.ActionType, out var itemAction))
{
Logger.DebugS("action", "user {0} attempted to perform" +
" unrecognized item action {1}",
session.AttachedEntity, performItemActionMessage.ActionType);
return null;
}
if (!EntityManager.TryGetEntity(performItemActionMessage.Item, out var item))
{
Logger.DebugS("action", "user {0} attempted to perform" +
" item action {1} for unknown item {2}",
session.AttachedEntity, performItemActionMessage.ActionType, performItemActionMessage.Item);
return null;
}
if (!item.TryGetComponent<ItemActionsComponent>(out var actionsComponent))
{
Logger.DebugS("action", "user {0} attempted to perform" +
" item action {1} for item {2} which has no ItemActionsComponent",
session.AttachedEntity, performItemActionMessage.ActionType, item);
return null;
}
if (actionsComponent.Holder != session.AttachedEntity)
{
Logger.DebugS("action", "user {0} attempted to perform" +
" item action {1} for item {2} which they are not holding",
session.AttachedEntity, performItemActionMessage.ActionType, 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", session.AttachedEntity, attempt, message.BehaviorType,
attempt.Action.BehaviorType);
return null;
}
return attempt;
}
private bool CheckRangeAndSetFacing(EntityCoordinates target, IEntity player)
{
// ensure it's within their clickable range
var targetWorldPos = target.ToMapPos(EntityManager);
var rangeBox = new Box2(player.Transform.WorldPosition, player.Transform.WorldPosition)
.Enlarged(_entityManager.MaxUpdateRange);
if (!rangeBox.Contains(targetWorldPos))
{
Logger.DebugS("action", "user {0} attempted to" +
" perform target action further than allowed range",
player.Name);
return false;
}
if (!ActionBlockerSystem.CanChangeDirection(player)) return true;
// don't set facing unless they clicked far enough away
var diff = targetWorldPos - player.Transform.WorldPosition;
if (diff.LengthSquared > 0.01f)
{
player.Transform.LocalRotation = new Angle(diff);
}
return true;
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
@@ -42,11 +43,6 @@ namespace Content.Server.GameObjects.Components.Mobs
base.OnRemove(); base.OnRemove();
} }
public override ComponentState GetComponentState()
{
return new AlertsComponentState(CreateAlertStatesArray());
}
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null) public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null)
{ {
base.HandleNetworkMessage(message, netChannel, session); base.HandleNetworkMessage(message, netChannel, session);
@@ -67,14 +63,21 @@ namespace Content.Server.GameObjects.Components.Mobs
break; break;
} }
// TODO: Implement clicking other status effects in the HUD if (!IsShowingAlert(msg.AlertType))
if (AlertManager.TryDecode(msg.EncodedAlert, out var alert))
{ {
PerformAlertClickCallback(alert, player); Logger.DebugS("alert", "user {0} attempted to" +
" click alert {1} which is not currently showing for them",
player.Name, msg.AlertType);
break;
}
if (AlertManager.TryGet(msg.AlertType, out var alert))
{
alert.OnClick.AlertClicked(new ClickAlertEventArgs(player, alert));
} }
else else
{ {
Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.EncodedAlert); Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.AlertType);
} }
break; break;

View File

@@ -145,15 +145,7 @@ namespace Content.Server.GameObjects.Components.Movement
mind.Mind.Visit(Owner); mind.Mind.Visit(Owner);
_controller = entity; _controller = entity;
status.ShowAlert(_pilotingAlertType, onClickAlert: OnClickAlert); status.ShowAlert(_pilotingAlertType);
}
private void OnClickAlert(ClickAlertEventArgs args)
{
if (args.Player.TryGetComponent(out ShuttleControllerComponent? controller))
{
controller.RemoveController();
}
} }
/// <summary> /// <summary>

View File

@@ -1,12 +1,14 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Pulling; using Content.Server.GameObjects.Components.Pulling;
using Content.Server.GameObjects.Components.Timing; using Content.Server.GameObjects.Components.Timing;
using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Server.Interfaces.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.GameObjects.EntitySystemMessages; using Content.Shared.GameObjects.EntitySystemMessages;
using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Input; using Content.Shared.Input;
@@ -113,11 +115,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
/// <summary> /// <summary>
/// Activates the Activate behavior of an object /// Activates the IActivate behavior of an object
/// Verifies that the user is capable of doing the use interaction first /// Verifies that the user is capable of doing the use interaction first
/// </summary> /// </summary>
/// <param name="user"></param>
/// <param name="used"></param>
public void TryInteractionActivate(IEntity user, IEntity used) public void TryInteractionActivate(IEntity user, IEntity used)
{ {
if (user != null && used != null && ActionBlockerSystem.CanUse(user)) if (user != null && used != null && ActionBlockerSystem.CanUse(user))
@@ -504,7 +504,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
/// <summary> /// <summary>
/// Activates the Use behavior of an object /// Activates the IUse behaviors of an entity
/// Verifies that the user is capable of doing the use interaction first /// Verifies that the user is capable of doing the use interaction first
/// </summary> /// </summary>
/// <param name="user"></param> /// <param name="user"></param>
@@ -518,8 +518,8 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
/// <summary> /// <summary>
/// Activates/Uses an object in control/possession of a user /// Activates the IUse behaviors of an entity without first checking
/// If the item has the IUse interface on one of its components we use the object in our hand /// if the user is capable of doing the use interaction.
/// </summary> /// </summary>
public void UseInteraction(IEntity user, IEntity used) public void UseInteraction(IEntity user, IEntity used)
{ {
@@ -679,6 +679,48 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
} }
/// <summary>
/// Calls EquippedHand on all components that implement the IEquippedHand interface
/// on an item.
/// </summary>
public void EquippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
{
var equippedHandMessage = new EquippedHandMessage(user, item, hand);
RaiseLocalEvent(equippedHandMessage);
if (equippedHandMessage.Handled)
{
return;
}
var comps = item.GetAllComponents<IEquippedHand>().ToList();
foreach (var comp in comps)
{
comp.EquippedHand(new EquippedHandEventArgs(user, hand));
}
}
/// <summary>
/// Calls UnequippedHand on all components that implement the IUnequippedHand interface
/// on an item.
/// </summary>
public void UnequippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
{
var unequippedHandMessage = new UnequippedHandMessage(user, item, hand);
RaiseLocalEvent(unequippedHandMessage);
if (unequippedHandMessage.Handled)
{
return;
}
var comps = item.GetAllComponents<IUnequippedHand>().ToList();
foreach (var comp in comps)
{
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
}
}
/// <summary> /// <summary>
/// Activates the Dropped behavior of an object /// Activates the Dropped behavior of an object
/// Verifies that the user is capable of doing the drop interaction first /// Verifies that the user is capable of doing the drop interaction first
@@ -757,7 +799,6 @@ namespace Content.Server.GameObjects.EntitySystems.Click
} }
} }
/// <summary> /// <summary>
/// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action /// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action
/// Or it will use the weapon itself on the position clicked, regardless of what was there /// Or it will use the weapon itself on the position clicked, regardless of what was there

View File

@@ -20,6 +20,7 @@ using Content.Server.PDA;
using Content.Server.Preferences; using Content.Server.Preferences;
using Content.Server.Sandbox; using Content.Server.Sandbox;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Actions;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
using Content.Shared.Alert; using Content.Shared.Alert;
@@ -43,6 +44,7 @@ namespace Content.Server
IoCManager.Register<IServerDbManager, ServerDbManager>(); IoCManager.Register<IServerDbManager, ServerDbManager>();
IoCManager.Register<RecipeManager, RecipeManager>(); IoCManager.Register<RecipeManager, RecipeManager>();
IoCManager.Register<AlertManager, AlertManager>(); IoCManager.Register<AlertManager, AlertManager>();
IoCManager.Register<ActionManager, ActionManager>();
IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>(); IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>();
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>(); IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
IoCManager.Register<INodeGroupManager, NodeGroupManager>(); IoCManager.Register<INodeGroupManager, NodeGroupManager>();

View File

@@ -0,0 +1,70 @@
using System.Collections.Generic;
using System.Linq;
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 class ActionManager
{
[Dependency]
private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<ActionType, ActionPrototype> _typeToAction;
private Dictionary<ItemActionType, ItemActionPrototype> _typeToItemAction;
public void Initialize()
{
_typeToAction = new Dictionary<ActionType, ActionPrototype>();
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);
}
}
_typeToItemAction = new Dictionary<ItemActionType, ItemActionPrototype>();
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, 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, out ItemActionPrototype action)
{
return _typeToItemAction.TryGetValue(actionType, out action);
}
}
}

View File

@@ -0,0 +1,100 @@
using Content.Shared.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel;
using Robust.Shared.Log;
namespace Content.Shared.Actions
{
/// <summary>
/// An action which is granted directly to an entity (such as an innate ability
/// or skill).
/// </summary>
[Prototype("action")]
public class ActionPrototype : BaseActionPrototype
{
/// <summary>
/// Type of action, no 2 action prototypes should have the same one.
/// </summary>
public ActionType ActionType { get; private 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; }
/// <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; }
/// <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; }
/// <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; }
public override void LoadFrom(YamlMappingNode mapping)
{
base.LoadFrom(mapping);
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(this, x => x.ActionType, "actionType", ActionType.Error);
if (ActionType == ActionType.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;
IActionBehavior behavior = null;
serializer.DataField(ref behavior, "behavior", null);
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;
}
}
}
}

View File

@@ -0,0 +1,33 @@
namespace Content.Shared.Actions
{
/// <summary>
/// Every possible action. Corresponds to actionType in action prototypes.
/// </summary>
public enum ActionType : byte
{
Error,
HumanScream,
DebugInstant,
DebugToggle,
DebugTargetPoint,
DebugTargetPointRepeat,
DebugTargetEntity,
DebugTargetEntityRepeat
}
/// <summary>
/// Every possible item action. Corresponds to actionType in itemAction prototypes.
/// </summary>
public enum ItemActionType : byte
{
Error,
ToggleInternals,
ToggleLight,
DebugInstant,
DebugToggle,
DebugTargetPoint,
DebugTargetPointRepeat,
DebugTargetEntity,
DebugTargetEntityRepeat
}
}

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Actions
{
/// <summary>
/// Base class for action prototypes.
/// </summary>
public abstract class BaseActionPrototype : IPrototype
{
/// <summary>
/// Icon representing this action in the UI.
/// </summary>
[ViewVariables]
public SpriteSpecifier Icon { get; private set; }
/// <summary>
/// For toggle actions only, icon to show when toggled on. If omitted,
/// the action will simply be highlighted when turned on.
/// </summary>
[ViewVariables]
public SpriteSpecifier IconOn { get; private set; }
/// <summary>
/// Name to show in UI. Accepts formatting.
/// </summary>
public FormattedMessage Name { get; private set; }
/// <summary>
/// Description to show in UI. Accepts formatting.
/// </summary>
public FormattedMessage Description { get; private set; }
/// <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>
public string Requires { get; private set; }
/// <summary>
/// The type of behavior this action has. This is valid clientside and serverside.
/// </summary>
public BehaviorType BehaviorType { get; protected set; }
/// <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>
public bool Repeat { get; private set; }
/// <summary>
/// Filters that can be used to filter this item in action menu.
/// </summary>
public IEnumerable<string> Filters { get; private set; }
/// <summary>
/// Keywords that can be used to search this item in action menu.
/// </summary>
public IEnumerable<string> Keywords { get; private set; }
public virtual void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataReadFunction("name", string.Empty,
s => Name = FormattedMessage.FromMarkup(s));
serializer.DataReadFunction("description", string.Empty,
s => Description = FormattedMessage.FromMarkup(s));
serializer.DataField(this, x => x.Requires,"requires", null);
serializer.DataField(this, x => x.Icon,"icon", SpriteSpecifier.Invalid);
serializer.DataField(this, x => x.IconOn,"iconOn", SpriteSpecifier.Invalid);
// client needs to know what type of behavior it is even if the actual implementation is only
// on server side. If we wanted to avoid this we'd need to always add a shared or clientside interface
// for each action even if there was only server-side logic, which would be cumbersome
serializer.DataField(this, x => x.BehaviorType, "behaviorType", BehaviorType.None);
if (BehaviorType == BehaviorType.None)
{
Logger.ErrorS("action", "Missing behaviorType for action with name {0}", Name);
}
if (BehaviorType != BehaviorType.Toggle && IconOn != SpriteSpecifier.Invalid)
{
Logger.ErrorS("action", "for action {0}, iconOn was specified but behavior" +
" type was {1}. iconOn is only supported for Toggle behavior type.", Name);
}
serializer.DataField(this, x => x.Repeat, "repeat", false);
if (Repeat && BehaviorType != BehaviorType.TargetEntity && BehaviorType != BehaviorType.TargetPoint)
{
Logger.ErrorS("action", " action named {0} used repeat: true, but this is only supported for" +
" TargetEntity and TargetPoint behaviorType and its behaviorType is {1}",
Name, BehaviorType);
}
serializer.DataReadFunction("filters", new List<string>(),
rawTags =>
{
Filters = rawTags.Select(rawTag => rawTag.Trim()).ToList();
});
serializer.DataReadFunction("keywords", new List<string>(),
rawTags =>
{
Keywords = rawTags.Select(rawTag => rawTag.Trim()).ToList();
});
}
protected void ValidateBehaviorType(BehaviorType expected, Type actualInterface)
{
if (BehaviorType != expected)
{
Logger.ErrorS("action", "for action named {0}, behavior implements " +
"{1}, so behaviorType should be {2} but was {3}", Name, actualInterface.Name, expected, 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
}
}

View File

@@ -0,0 +1,44 @@
using System;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Actions
{
/// <summary>
/// Currently just a marker interface delineating the different possible
/// types of action behaviors.
/// </summary>
public interface IActionBehavior : IExposeData { }
/// <summary>
/// Base class for all action event args
/// </summary>
public abstract class ActionEventArgs : EventArgs
{
/// <summary>
/// Entity performing the action.
/// </summary>
public readonly IEntity Performer;
/// <summary>
/// Action being performed
/// </summary>
public readonly ActionType ActionType;
/// <summary>
/// Actions component of the performer.
/// </summary>
public readonly SharedActionsComponent PerformerActions;
public ActionEventArgs(IEntity performer, ActionType actionType)
{
Performer = performer;
ActionType = actionType;
if (!Performer.TryGetComponent(out PerformerActions))
{
throw new InvalidOperationException($"performer {performer.Name} tried to perform action {actionType} " +
$" but the performer had no actions component," +
" which should never occur");
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Actions
{
/// <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 class InstantActionEventArgs : ActionEventArgs
{
public InstantActionEventArgs(IEntity performer, ActionType actionType) : base(performer, actionType)
{
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Actions
{
/// <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 class InstantItemActionEventArgs : ItemActionEventArgs
{
public InstantItemActionEventArgs(IEntity performer, IEntity item, ItemActionType actionType) :
base(performer, item, actionType)
{
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Actions
{
/// <summary>
/// Currently just a marker interface delineating the different possible
/// types of item action behaviors.
/// </summary>
public interface IItemActionBehavior : IExposeData
{
}
/// <summary>
/// Base class for all item action event args
/// </summary>
public abstract class ItemActionEventArgs : EventArgs
{
/// <summary>
/// Entity performing the action.
/// </summary>
public readonly IEntity Performer;
/// <summary>
/// Item being used to perform the action
/// </summary>
public readonly IEntity Item;
/// <summary>
/// Action being performed
/// </summary>
public readonly ItemActionType ActionType;
/// <summary>
/// Item actions component of the item.
/// </summary>
public readonly ItemActionsComponent ItemActions;
public ItemActionEventArgs(IEntity performer, IEntity item, ItemActionType actionType)
{
Performer = performer;
ActionType = actionType;
Item = item;
if (!Item.TryGetComponent(out ItemActions))
{
throw new InvalidOperationException($"performer {performer.Name} tried to perform item action {actionType} " +
$" for item {Item.Name} but the item had no ItemActionsComponent," +
" which should never occur");
}
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Map;
namespace Content.Shared.Actions
{
/// <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 class TargetEntityActionEventArgs : ActionEventArgs
{
/// <summary>
/// Entity being targeted
/// </summary>
public readonly IEntity Target;
public TargetEntityActionEventArgs(IEntity performer, ActionType actionType, IEntity target) :
base(performer, actionType)
{
Target = target;
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Map;
namespace Content.Shared.Actions
{
/// <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 class TargetEntityItemActionEventArgs : ItemActionEventArgs
{
/// <summary>
/// Entity being targeted
/// </summary>
public readonly IEntity Target;
public TargetEntityItemActionEventArgs(IEntity performer, IEntity target, IEntity item,
ItemActionType actionType) : base(performer, item, actionType)
{
Target = target;
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
namespace Content.Shared.Actions
{
/// <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 class TargetPointActionEventArgs : ActionEventArgs
{
/// <summary>
/// Local coordinates of the targeted position.
/// </summary>
public readonly EntityCoordinates Target;
public TargetPointActionEventArgs(IEntity performer, EntityCoordinates target, ActionType actionType)
: base(performer, actionType)
{
Target = target;
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
namespace Content.Shared.Actions
{
/// <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 class TargetPointItemActionEventArgs : ItemActionEventArgs
{
/// <summary>
/// Local coordinates of the targeted position.
/// </summary>
public readonly EntityCoordinates Target;
public TargetPointItemActionEventArgs(IEntity performer, EntityCoordinates target, IEntity item,
ItemActionType actionType) : base(performer, item, actionType)
{
Target = target;
}
}
}

View File

@@ -0,0 +1,41 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Actions
{
/// <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 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(IEntity performer, ActionType actionType, bool toggledOn) : base(performer, actionType)
{
ToggledOn = toggledOn;
}
}
}

View File

@@ -0,0 +1,42 @@
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Actions
{
/// <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 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(IEntity performer, bool toggledOn, IEntity item,
ItemActionType actionType) : base(performer, item, actionType)
{
ToggledOn = toggledOn;
}
}
}

View File

@@ -0,0 +1,122 @@
using Content.Shared.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel;
namespace Content.Shared.Actions
{
/// <summary>
/// An action which is granted to an entity via an item (such as toggling a flashlight).
/// </summary>
[Prototype("itemAction")]
public class ItemActionPrototype : BaseActionPrototype
{
/// <summary>
/// Type of item action, no 2 itemAction prototypes should have the same one.
/// </summary>
public ItemActionType ActionType { get; private set; }
/// <see cref="ItemActionIconStyle"/>
public ItemActionIconStyle IconStyle { get; private set; }
/// <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; }
/// <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; }
/// <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; }
/// <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; }
public override void LoadFrom(YamlMappingNode mapping)
{
base.LoadFrom(mapping);
var serializer = YamlObjectSerializer.NewReader(mapping);
serializer.DataField(this, x => x.ActionType, "actionType", ItemActionType.Error);
if (ActionType == ItemActionType.Error)
{
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
}
serializer.DataField(this, x => x.IconStyle, "iconStyle", ItemActionIconStyle.BigItem);
// TODO: Split this class into server/client after RobustToolbox#1405
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
IItemActionBehavior behavior = null;
serializer.DataField(ref behavior, "behavior", null);
switch (behavior)
{
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
}
}

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Shared.Prototypes.Kitchen;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -8,41 +6,28 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Alert namespace Content.Shared.Alert
{ {
/// <summary> /// <summary>
/// Provides access to all configured alerts. Ability to encode/decode a given state /// Provides access to all configured alerts by alert type.
/// to an int.
/// </summary> /// </summary>
public class AlertManager public class AlertManager
{ {
[Dependency] [Dependency]
private readonly IPrototypeManager _prototypeManager = default!; private readonly IPrototypeManager _prototypeManager = default!;
private AlertPrototype[] _orderedAlerts; private Dictionary<AlertType, AlertPrototype> _typeToAlert;
private Dictionary<AlertType, byte> _typeToIndex;
public void Initialize() public void Initialize()
{ {
// order by type value so we can map between the id and an integer index and use _typeToAlert = new Dictionary<AlertType, AlertPrototype>();
// the index for compact alert change messages
_orderedAlerts =
_prototypeManager.EnumeratePrototypes<AlertPrototype>()
.OrderBy(prototype => prototype.AlertType).ToArray();
_typeToIndex = new Dictionary<AlertType, byte>();
for (var i = 0; i < _orderedAlerts.Length; i++) foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
{ {
if (i > byte.MaxValue) if (!_typeToAlert.TryAdd(alert.AlertType, alert))
{
Logger.ErrorS("alert", "too many alerts for byte encoding ({0})! encoding will need" +
" to be changed to use a ushort rather than byte", _typeToIndex.Count);
break;
}
if (!_typeToIndex.TryAdd(_orderedAlerts[i].AlertType, (byte) i))
{ {
Logger.ErrorS("alert", Logger.ErrorS("alert",
"Found alert with duplicate id {0}", _orderedAlerts[i].AlertType); "Found alert with duplicate alertType {0} - all alerts must have" +
" a unique alerttype, this one will be skipped", alert.AlertType);
} }
} }
} }
/// <summary> /// <summary>
@@ -51,74 +36,7 @@ namespace Content.Shared.Alert
/// <returns>true if found</returns> /// <returns>true if found</returns>
public bool TryGet(AlertType alertType, out AlertPrototype alert) public bool TryGet(AlertType alertType, out AlertPrototype alert)
{ {
if (_typeToIndex.TryGetValue(alertType, out var idx)) return _typeToAlert.TryGetValue(alertType, out alert);
{
alert = _orderedAlerts[idx];
return true;
}
alert = null;
return false;
}
/// <summary>
/// Tries to get the alert of the indicated type along with its encoding
/// </summary>
/// <returns>true if found</returns>
public bool TryGetWithEncoded(AlertType alertType, out AlertPrototype alert, out byte encoded)
{
if (_typeToIndex.TryGetValue(alertType, out var idx))
{
alert = _orderedAlerts[idx];
encoded = (byte) idx;
return true;
}
alert = null;
encoded = 0;
return false;
}
/// <summary>
/// Tries to get the compact encoded representation of this alert
/// </summary>
/// <returns>true if successful</returns>
public bool TryEncode(AlertPrototype alert, out byte encoded)
{
return TryEncode(alert.AlertType, out encoded);
}
/// <summary>
/// Tries to get the compact encoded representation of the alert with
/// the indicated id
/// </summary>
/// <returns>true if successful</returns>
public bool TryEncode(AlertType alertType, out byte encoded)
{
if (_typeToIndex.TryGetValue(alertType, out var idx))
{
encoded = idx;
return true;
}
encoded = 0;
return false;
}
/// <summary>
/// Tries to get the alert from the encoded representation
/// </summary>
/// <returns>true if successful</returns>
public bool TryDecode(byte encodedAlert, out AlertPrototype alert)
{
if (encodedAlert >= _orderedAlerts.Length)
{
alert = null;
return false;
}
alert = _orderedAlerts[encodedAlert];
return true;
} }
} }
} }

View File

@@ -1,4 +1,6 @@
using System; using System;
using Content.Shared.Interfaces;
using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -70,6 +72,11 @@ namespace Content.Shared.Alert
/// </summary> /// </summary>
public bool SupportsSeverity => MaxSeverity != -1; public bool SupportsSeverity => MaxSeverity != -1;
/// <summary>
/// Defines what to do when the alert is clicked.
/// </summary>
public IAlertClick OnClick { get; private set; }
public void LoadFrom(YamlMappingNode mapping) public void LoadFrom(YamlMappingNode mapping)
{ {
var serializer = YamlObjectSerializer.NewReader(mapping); var serializer = YamlObjectSerializer.NewReader(mapping);
@@ -94,6 +101,9 @@ namespace Content.Shared.Alert
Category = alertCategory; Category = alertCategory;
} }
AlertKey = new AlertKey(AlertType, Category); AlertKey = new AlertKey(AlertType, Category);
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
serializer.DataField(this, x => x.OnClick, "onClick", null);
} }
/// <param name="severity">severity level, if supported by this alert</param> /// <param name="severity">severity level, if supported by this alert</param>
@@ -143,30 +153,26 @@ namespace Content.Shared.Alert
[Serializable, NetSerializable] [Serializable, NetSerializable]
public struct AlertKey public struct AlertKey
{ {
private readonly AlertType? _alertType; public readonly AlertType? AlertType;
private readonly AlertCategory? _alertCategory; public readonly AlertCategory? AlertCategory;
/// NOTE: if the alert has a category you must pass the category for this to work /// NOTE: if the alert has a category you must pass the category for this to work
/// properly as a key. I.e. if the alert has a category and you pass only the ID, and you /// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you
/// compare this to another AlertKey that has both the category and the same ID, it will not consider them equal. /// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal.
public AlertKey(AlertType? alertType, AlertCategory? alertCategory) public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
{ {
// if there is a category, ignore the alerttype. AlertCategory = alertCategory;
if (alertCategory != null) AlertType = alertType;
{
_alertCategory = alertCategory;
_alertType = null;
}
else
{
_alertCategory = null;
_alertType = alertType;
}
} }
public bool Equals(AlertKey other) public bool Equals(AlertKey other)
{ {
return _alertType == other._alertType && _alertCategory == other._alertCategory; // compare only on alert category if we have one
if (AlertCategory.HasValue)
{
return other.AlertCategory == AlertCategory;
}
return AlertType == other.AlertType && AlertCategory == other.AlertCategory;
} }
public override bool Equals(object obj) public override bool Equals(object obj)
@@ -176,11 +182,14 @@ namespace Content.Shared.Alert
public override int GetHashCode() public override int GetHashCode()
{ {
return HashCode.Combine(_alertType, _alertCategory); // use only alert category if we have one
if (AlertCategory.HasValue) return AlertCategory.GetHashCode();
return AlertType.GetHashCode();
} }
/// <param name="category">alert category, must not be null</param> /// <param name="category">alert category, must not be null</param>
/// <returns>An alert key for the provided alert category</returns> /// <returns>An alert key for the provided alert category. This must only be used for
/// queries and never storage, as it is lacking an alert type.</returns>
public static AlertKey ForCategory(AlertCategory category) public static AlertKey ForCategory(AlertCategory category)
{ {
return new(null, category); return new(null, category);

View File

@@ -16,8 +16,10 @@
/// <summary> /// <summary>
/// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML /// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML
/// NOTE: Using byte for a compact encoding when sending this in messages, can upgrade
/// to ushort
/// </summary> /// </summary>
public enum AlertType public enum AlertType : byte
{ {
Error, Error,
LowPressure, LowPressure,

View File

@@ -0,0 +1,37 @@
using System;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
namespace Content.Shared.Alert
{
/// <summary>
/// Defines what should happen when an alert is clicked.
/// </summary>
public interface IAlertClick : IExposeData
{
/// <summary>
/// Invoked on server side when user clicks an alert.
/// </summary>
/// <param name="args"></param>
void AlertClicked(ClickAlertEventArgs args);
}
public class ClickAlertEventArgs : EventArgs
{
/// <summary>
/// Player clicking the alert
/// </summary>
public readonly IEntity Player;
/// <summary>
/// Alert that was clicked
/// </summary>
public readonly AlertPrototype Alert;
public ClickAlertEventArgs(IEntity player, AlertPrototype alert)
{
Player = player;
Alert = alert;
}
}
}

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Content.Shared.GameObjects.Components.Items;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -26,7 +27,7 @@ namespace Content.Shared.GameObjects.Components.Inventory
/// Uniquely identifies a single slot in an inventory. /// Uniquely identifies a single slot in an inventory.
/// </summary> /// </summary>
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum Slots public enum Slots : byte
{ {
NONE = 0, NONE = 0,
HEAD, HEAD,
@@ -148,6 +149,5 @@ namespace Content.Shared.GameObjects.Components.Inventory
"Hands_left", "Hands_left",
"Hands_right", "Hands_right",
}; };
} }
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Reflection; using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -47,6 +48,10 @@ namespace Content.Shared.GameObjects.Components.Inventory
InventoryInstance = DynamicTypeFactory.CreateInstance<Inventory>(type); InventoryInstance = DynamicTypeFactory.CreateInstance<Inventory>(type);
} }
/// <returns>true if the item is equipped to an equip slot (NOT inside an equipped container
/// like inside a backpack)</returns>
public abstract bool IsEquipped(IEntity item);
[Serializable, NetSerializable] [Serializable, NetSerializable]
protected class InventoryComponentState : ComponentState protected class InventoryComponentState : ComponentState
{ {

View File

@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using System; using System;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Items namespace Content.Shared.GameObjects.Components.Items
@@ -9,6 +10,9 @@ namespace Content.Shared.GameObjects.Components.Items
{ {
public sealed override string Name => "Hands"; public sealed override string Name => "Hands";
public sealed override uint? NetID => ContentNetIDs.HANDS; public sealed override uint? NetID => ContentNetIDs.HANDS;
/// <returns>true if the item is in one of the hands</returns>
public abstract bool IsHolding(IEntity item);
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -0,0 +1,208 @@
#nullable enable
using Content.Shared.Actions;
using Robust.Shared.GameObjects;
using Robust.Shared.Input.Binding;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Map;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <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; }
ComponentMessage PerformInstantActionMessage();
ComponentMessage PerformToggleActionMessage(bool on);
ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
/// <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(IEntity 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(IEntity player, bool on);
/// <summary>
/// Perform the server-side logic of the target point action
/// </summary>
void DoTargetPointAction(IEntity player, EntityCoordinates target);
/// <summary>
/// Perform the server-side logic of the target entity action
/// </summary>
void DoTargetEntityAction(IEntity player, IEntity target);
}
public 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(IEntity player)
{
_action.InstantAction.DoInstantAction(new InstantActionEventArgs(player, _action.ActionType));
}
public bool DoToggleAction(IEntity player, bool on)
{
return _action.ToggleAction.DoToggleAction(new ToggleActionEventArgs(player, _action.ActionType, on));
}
public void DoTargetPointAction(IEntity player, EntityCoordinates target)
{
_action.TargetPointAction.DoTargetPointAction(new TargetPointActionEventArgs(player, target, _action.ActionType));
}
public void DoTargetEntityAction(IEntity player, IEntity target)
{
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityActionEventArgs(player, _action.ActionType,
target));
}
public ComponentMessage PerformInstantActionMessage()
{
return new PerformInstantActionMessage(_action.ActionType);
}
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
{
if (toggleOn)
{
return new PerformToggleOnActionMessage(_action.ActionType);
}
return new PerformToggleOffActionMessage(_action.ActionType);
}
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
{
return new PerformTargetPointActionMessage(_action.ActionType, args.Coordinates);
}
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
{
return new PerformTargetEntityActionMessage(_action.ActionType, args.EntityUid);
}
public override string ToString()
{
return $"{nameof(_action)}: {_action.ActionType}";
}
}
public class ItemActionAttempt : IActionAttempt
{
private readonly ItemActionPrototype _action;
private readonly IEntity _item;
private readonly ItemActionsComponent _itemActions;
public BaseActionPrototype Action => _action;
public ItemActionAttempt(ItemActionPrototype action, IEntity item, ItemActionsComponent itemActions)
{
_action = action;
_item = item;
_itemActions = itemActions;
}
public void DoInstantAction(IEntity player)
{
_action.InstantAction.DoInstantAction(new InstantItemActionEventArgs(player, _item, _action.ActionType));
}
public bool DoToggleAction(IEntity player, bool on)
{
return _action.ToggleAction.DoToggleAction(new ToggleItemActionEventArgs(player, on, _item, _action.ActionType));
}
public void DoTargetPointAction(IEntity player, EntityCoordinates target)
{
_action.TargetPointAction.DoTargetPointAction(new TargetPointItemActionEventArgs(player, target, _item,
_action.ActionType));
}
public void DoTargetEntityAction(IEntity player, IEntity 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);
}
public ComponentMessage PerformInstantActionMessage()
{
return new PerformInstantItemActionMessage(_action.ActionType, _item.Uid);
}
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
{
if (toggleOn)
{
return new PerformToggleOnItemActionMessage(_action.ActionType, _item.Uid);
}
return new PerformToggleOffItemActionMessage(_action.ActionType, _item.Uid);
}
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
{
return new PerformTargetPointItemActionMessage(_action.ActionType, _item.Uid, args.Coordinates);
}
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
{
return new PerformTargetEntityItemActionMessage(_action.ActionType, _item.Uid, args.EntityUid);
}
public override string ToString()
{
return $"{nameof(_action)}: {_action.ActionType}, {nameof(_item)}: {_item}";
}
}
}

View File

@@ -0,0 +1,249 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <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 class ItemActionsComponent : Component, IEquippedHand, IEquipped, IUnequipped, IUnequippedHand
{
public override string Name => "ItemActions";
/// <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 => InSlot != EquipmentSlotDefines.Slots.NONE || InHand != null;
/// <summary>
/// Slot currently equipped to, NONE if not equipped to an equip slot.
/// </summary>
public EquipmentSlotDefines.Slots InSlot { get; private set; }
/// <summary>
/// hand it's currently in, null if not in a hand.
/// </summary>
public SharedHand? InHand { get; private set; }
/// <summary>
/// Entity currently holding this in hand or equip slot. Null if not held.
/// </summary>
public IEntity? Holder { get; private set; }
// cached actions component of the holder, since we'll need to access it frequently
private SharedActionsComponent? _holderActionsComponent;
private List<ItemActionConfig> _actionConfigs = new();
// State of all actions provided by this item.
private readonly Dictionary<ItemActionType, ActionState> _actions = new();
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _actionConfigs,"actions", new List<ItemActionConfig>());
foreach (var actionConfig in _actionConfigs)
{
GrantOrUpdate(actionConfig.ActionType, actionConfig.Enabled, false, null);
}
}
protected override void Startup()
{
base.Startup();
GrantOrUpdateAllToHolder();
}
protected override void Shutdown()
{
base.Shutdown();
RevokeAllFromHolder();
}
private void GrantOrUpdateAllToHolder()
{
if (_holderActionsComponent == null) return;
foreach (var (actionType, state) in _actions)
{
_holderActionsComponent.GrantOrUpdateItemAction(actionType, Owner.Uid, state);
}
}
private void RevokeAllFromHolder()
{
if (_holderActionsComponent == null) return;
foreach (var (actionType, state) in _actions)
{
_holderActionsComponent.RevokeItemAction(actionType, Owner.Uid);
}
}
/// <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.Uid, 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);
}
public void EquippedHand(EquippedHandEventArgs eventArgs)
{
// this entity cannot be granted actions if no actions component
if (!eventArgs.User.TryGetComponent<SharedActionsComponent>(out var actionsComponent))
return;
Holder = eventArgs.User;
_holderActionsComponent = actionsComponent;
InSlot = EquipmentSlotDefines.Slots.NONE;
InHand = eventArgs.Hand;
GrantOrUpdateAllToHolder();
}
public void Equipped(EquippedEventArgs eventArgs)
{
// this entity cannot be granted actions if no actions component
if (!eventArgs.User.TryGetComponent<SharedActionsComponent>(out var actionsComponent))
return;
Holder = eventArgs.User;
_holderActionsComponent = actionsComponent;
InSlot = eventArgs.Slot;
InHand = null;
GrantOrUpdateAllToHolder();
}
public void Unequipped(UnequippedEventArgs eventArgs)
{
RevokeAllFromHolder();
Holder = null;
_holderActionsComponent = null;
InSlot = EquipmentSlotDefines.Slots.NONE;
InHand = null;
}
public void UnequippedHand(UnequippedHandEventArgs eventArgs)
{
RevokeAllFromHolder();
Holder = null;
_holderActionsComponent = null;
InSlot = EquipmentSlotDefines.Slots.NONE;
InHand = null;
}
}
/// <summary>
/// Configuration for an item action provided by an item.
/// </summary>
public class ItemActionConfig : IExposeData
{
public ItemActionType ActionType { get; private set; }
/// <summary>
/// Whether action is initially enabled on this item. Defaults to true.
/// </summary>
public bool Enabled { get; private set; }
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(this, x => x.ActionType, "actionType", ItemActionType.Error);
if (ActionType == ItemActionType.Error)
{
Logger.ErrorS("action", "invalid or missing actionType");
}
serializer.DataField(this, x => x.Enabled, "enabled", true);
}
}
}

View File

@@ -0,0 +1,652 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Actions;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Mobs
{
/// <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>
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!;
public override string Name => "Actions";
public override uint? NetID => ContentNetIDs.ACTIONS;
/// <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>();
private List<ActionType>? _innateActions;
// 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();
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _innateActions,"innateActions", null);
}
protected override void 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;
}
/// <seealso cref="TryGetItemActionStates(Robust.Shared.GameObjects.EntityUid,out System.Collections.Generic.IReadOnlyDictionary{Content.Shared.Actions.ItemActionType,Content.Shared.GameObjects.Components.Mobs.ActionState}?)"/>
public bool TryGetItemActionStates(IEntity item,
[NotNullWhen((true))] out IReadOnlyDictionary<ItemActionType, ActionState>? itemActionStates)
{
return TryGetItemActionStates(item.Uid, out itemActionStates);
}
/// <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);
}
/// <seealso cref="TryGetItemActionState(Content.Shared.Actions.ItemActionType,Robust.Shared.GameObjects.EntityUid,out Content.Shared.GameObjects.Components.Mobs.ActionState)"/>
public bool TryGetItemActionState(ItemActionType actionType, IEntity item, out ActionState actionState)
{
return TryGetItemActionState(actionType, item.Uid, out actionState);
}
/// <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);
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 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) : base(ContentNetIDs.ACTIONS)
{
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]
public abstract class BasePerformActionMessage : ComponentMessage
{
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 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 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 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 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 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 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 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 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 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 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;
}
}
}

View File

@@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Content.Shared.Alert; using Content.Shared.Alert;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.Mobs namespace Content.Shared.GameObjects.Components.Mobs
@@ -18,16 +15,30 @@ namespace Content.Shared.GameObjects.Components.Mobs
/// </summary> /// </summary>
public abstract class SharedAlertsComponent : Component public abstract class SharedAlertsComponent : Component
{ {
private static readonly AlertState[] NO_ALERTS = new AlertState[0];
[Dependency] [Dependency]
protected readonly AlertManager AlertManager = default!; protected readonly AlertManager AlertManager = default!;
public override string Name => "AlertsUI"; public override string Name => "Alerts";
public override uint? NetID => ContentNetIDs.ALERTS; public override uint? NetID => ContentNetIDs.ALERTS;
[ViewVariables] [ViewVariables] private Dictionary<AlertKey, AlertState> _alerts = new();
private Dictionary<AlertKey, ClickableAlertState> _alerts = new();
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not AlertsComponentState state)
{
return;
}
_alerts = state.Alerts;
}
public override ComponentState GetComponentState()
{
return new AlertsComponentState(_alerts);
}
/// <returns>true iff an alert of the indicated alert category is currently showing</returns> /// <returns>true iff an alert of the indicated alert category is currently showing</returns>
public bool IsShowingAlertCategory(AlertCategory alertCategory) public bool IsShowingAlertCategory(AlertCategory alertCategory)
@@ -53,82 +64,14 @@ namespace Content.Shared.GameObjects.Components.Mobs
return _alerts.ContainsKey(alertKey); return _alerts.ContainsKey(alertKey);
} }
protected IEnumerable<AlertState> EnumerateAlertStates() protected IEnumerable<KeyValuePair<AlertKey, AlertState>> EnumerateAlertStates()
{ {
return _alerts.Values.Select(alertData => alertData.AlertState); return _alerts;
}
/// <summary>
/// Invokes the alert's specified callback if there is one.
/// Not intended to be used on clientside.
/// </summary>
protected void PerformAlertClickCallback(AlertPrototype alert, IEntity owner)
{
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback))
{
alertStateCallback.OnClickAlert?.Invoke(new ClickAlertEventArgs(owner, alert));
}
else
{
Logger.DebugS("alert", "player {0} attempted to invoke" +
" alert click for {1} but that alert is not currently" +
" showing", owner.Name, alert.AlertType);
}
}
/// <summary>
/// Creates a new array containing all of the current alert states.
/// </summary>
/// <returns></returns>
protected AlertState[] CreateAlertStatesArray()
{
if (_alerts.Count == 0) return NO_ALERTS;
var states = new AlertState[_alerts.Count];
// because I don't trust LINQ
var idx = 0;
foreach (var alertData in _alerts.Values)
{
states[idx++] = alertData.AlertState;
}
return states;
} }
protected bool TryGetAlertState(AlertKey key, out AlertState alertState) protected bool TryGetAlertState(AlertKey key, out AlertState alertState)
{ {
if (_alerts.TryGetValue(key, out var alertData)) return _alerts.TryGetValue(key, out alertState);
{
alertState = alertData.AlertState;
return true;
}
alertState = default;
return false;
}
/// <summary>
/// Replace the current active alerts with the specified alerts. Any
/// OnClickAlert callbacks on the active alerts will be erased.
/// </summary>
protected void SetAlerts(AlertState[] alerts)
{
var newAlerts = new Dictionary<AlertKey, ClickableAlertState>();
foreach (var alertState in alerts)
{
if (AlertManager.TryDecode(alertState.AlertEncoded, out var alert))
{
newAlerts[alert.AlertKey] = new ClickableAlertState
{
AlertState = alertState
};
}
else
{
Logger.ErrorS("alert", "unrecognized encoded alert {0}", alertState.AlertEncoded);
}
}
_alerts = newAlerts;
} }
/// <summary> /// <summary>
@@ -136,30 +79,24 @@ namespace Content.Shared.GameObjects.Components.Mobs
/// it will be updated / replaced with the specified values. /// it will be updated / replaced with the specified values.
/// </summary> /// </summary>
/// <param name="alertType">type of the alert to set</param> /// <param name="alertType">type of the alert to set</param>
/// <param name="onClickAlert">callback to invoke when ClickAlertMessage is received by the server
/// after being clicked by client. Has no effect when specified on the clientside.</param>
/// <param name="severity">severity, if supported by the alert</param> /// <param name="severity">severity, if supported by the alert</param>
/// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will /// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will
/// be erased if there is currently a cooldown for the alert)</param> /// be erased if there is currently a cooldown for the alert)</param>
public void ShowAlert(AlertType alertType, short? severity = null, OnClickAlert onClickAlert = null, public void ShowAlert(AlertType alertType, short? severity = null, ValueTuple<TimeSpan, TimeSpan>? cooldown = null)
ValueTuple<TimeSpan, TimeSpan>? cooldown = null)
{ {
if (AlertManager.TryGetWithEncoded(alertType, out var alert, out var encoded)) if (AlertManager.TryGet(alertType, out var alert))
{ {
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) && if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
alertStateCallback.AlertState.AlertEncoded == encoded && alert.AlertType == alertType &&
alertStateCallback.AlertState.Severity == severity && alertStateCallback.AlertState.Cooldown == cooldown) alertStateCallback.Severity == severity && alertStateCallback.Cooldown == cooldown)
{ {
alertStateCallback.OnClickAlert = onClickAlert;
return; return;
} }
_alerts[alert.AlertKey] = new ClickableAlertState _alerts[alert.AlertKey] = new AlertState
{ {Cooldown = cooldown, Severity = severity};
AlertState = new AlertState
{Cooldown = cooldown, AlertEncoded = encoded, Severity = severity}, AfterShowAlert();
OnClickAlert = onClickAlert
};
Dirty(); Dirty();
@@ -212,7 +149,12 @@ namespace Content.Shared.GameObjects.Components.Mobs
} }
/// <summary> /// <summary>
/// Invoked after clearing an alert prior to dirtying the control /// Invoked after showing an alert prior to dirtying the component
/// </summary>
protected virtual void AfterShowAlert() { }
/// <summary>
/// Invoked after clearing an alert prior to dirtying the component
/// </summary> /// </summary>
protected virtual void AfterClearAlert() { } protected virtual void AfterClearAlert() { }
} }
@@ -220,9 +162,9 @@ namespace Content.Shared.GameObjects.Components.Mobs
[Serializable, NetSerializable] [Serializable, NetSerializable]
public class AlertsComponentState : ComponentState public class AlertsComponentState : ComponentState
{ {
public AlertState[] Alerts; public Dictionary<AlertKey, AlertState> Alerts;
public AlertsComponentState(AlertState[] alerts) : base(ContentNetIDs.ALERTS) public AlertsComponentState(Dictionary<AlertKey, AlertState> alerts) : base(ContentNetIDs.ALERTS)
{ {
Alerts = alerts; Alerts = alerts;
} }
@@ -234,46 +176,19 @@ namespace Content.Shared.GameObjects.Components.Mobs
[Serializable, NetSerializable] [Serializable, NetSerializable]
public class ClickAlertMessage : ComponentMessage public class ClickAlertMessage : ComponentMessage
{ {
public readonly byte EncodedAlert; public readonly AlertType AlertType;
public ClickAlertMessage(byte encodedAlert) public ClickAlertMessage(AlertType alertType)
{ {
Directed = true; Directed = true;
EncodedAlert = encodedAlert; AlertType = alertType;
} }
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]
public struct AlertState public struct AlertState
{ {
public byte AlertEncoded;
public short? Severity; public short? Severity;
public ValueTuple<TimeSpan, TimeSpan>? Cooldown; public ValueTuple<TimeSpan, TimeSpan>? Cooldown;
} }
public struct ClickableAlertState
{
public AlertState AlertState;
public OnClickAlert OnClickAlert;
}
public delegate void OnClickAlert(ClickAlertEventArgs args);
public class ClickAlertEventArgs : EventArgs
{
/// <summary>
/// Player clicking the alert
/// </summary>
public readonly IEntity Player;
/// <summary>
/// Alert that was clicked
/// </summary>
public readonly AlertPrototype Alert;
public ClickAlertEventArgs(IEntity player, AlertPrototype alert)
{
Player = player;
Alert = alert;
}
}
} }

View File

@@ -69,17 +69,11 @@ namespace Content.Shared.GameObjects.Components.Pulling
{ {
case PullStartedMessage msg: case PullStartedMessage msg:
Pulling = msg.Pulled.Owner; Pulling = msg.Pulled.Owner;
if (ownerStatus != null) ownerStatus?.ShowAlert(AlertType.Pulling);
{
ownerStatus.ShowAlert(AlertType.Pulling, onClickAlert: OnClickAlert);
}
break; break;
case PullStoppedMessage _: case PullStoppedMessage _:
Pulling = null; Pulling = null;
if (ownerStatus != null) ownerStatus?.ClearAlert(AlertType.Pulling);
{
ownerStatus.ClearAlert(AlertType.Pulling);
}
break; break;
} }
} }

View File

@@ -86,7 +86,8 @@
public const uint SINGULARITY = 1080; public const uint SINGULARITY = 1080;
public const uint CHARACTERINFO = 1081; public const uint CHARACTERINFO = 1081;
public const uint REAGENT_GRINDER = 1082; public const uint REAGENT_GRINDER = 1082;
public const uint DAMAGEABLE = 1083; public const uint ACTIONS = 1083;
public const uint DAMAGEABLE = 1084;
// Net IDs for integration tests. // Net IDs for integration tests.
public const uint PREDICTION_TEST = 10001; public const uint PREDICTION_TEST = 10001;

View File

@@ -0,0 +1,30 @@
using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects.Systems;
namespace Content.Shared.GameObjects.EntitySystems
{
/// <summary>
/// Evicts action states with expired cooldowns.
/// </summary>
public class SharedActionSystem : EntitySystem
{
private const float CooldownCheckIntervalSeconds = 10;
private float _timeSinceCooldownCheck;
public override void Update(float frameTime)
{
base.Update(frameTime);
_timeSinceCooldownCheck += frameTime;
if (_timeSinceCooldownCheck < CooldownCheckIntervalSeconds) return;
foreach (var comp in ComponentManager.EntityQuery<SharedActionsComponent>(false))
{
comp.ExpireCooldowns();
}
_timeSinceCooldownCheck -= CooldownCheckIntervalSeconds;
}
}
}

View File

@@ -41,5 +41,16 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction Arcade1 = "Arcade1"; public static readonly BoundKeyFunction Arcade1 = "Arcade1";
public static readonly BoundKeyFunction Arcade2 = "Arcade2"; public static readonly BoundKeyFunction Arcade2 = "Arcade2";
public static readonly BoundKeyFunction Arcade3 = "Arcade3"; public static readonly BoundKeyFunction Arcade3 = "Arcade3";
public static readonly BoundKeyFunction OpenActionsMenu = "OpenAbilitiesMenu";
public static readonly BoundKeyFunction Hotbar0 = "Hotbar0";
public static readonly BoundKeyFunction Hotbar1 = "Hotbar1";
public static readonly BoundKeyFunction Hotbar2 = "Hotbar2";
public static readonly BoundKeyFunction Hotbar3 = "Hotbar3";
public static readonly BoundKeyFunction Hotbar4 = "Hotbar4";
public static readonly BoundKeyFunction Hotbar5 = "Hotbar5";
public static readonly BoundKeyFunction Hotbar6 = "Hotbar6";
public static readonly BoundKeyFunction Hotbar7 = "Hotbar7";
public static readonly BoundKeyFunction Hotbar8 = "Hotbar8";
public static readonly BoundKeyFunction Hotbar9 = "Hotbar9";
} }
} }

View File

@@ -6,8 +6,11 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components behavior when being activated in the world when the user /// This interface gives components behavior when being activated (by default,
/// is in range and has unobstructed access to the target entity (allows inside blockers). /// this is done via the "E" key) when the user is in range and has unobstructed access to the target entity
/// (allows inside blockers). This includes activating an object in the world as well as activating an
/// object in inventory. Unlike IUse, this can be performed on entities that aren't in the active hand,
/// even when the active hand is currently holding something else.
/// </summary> /// </summary>
public interface IActivate public interface IActivate
{ {

View File

@@ -7,8 +7,9 @@ using Robust.Shared.Map;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components a behavior when clicking on another object and no interaction occurs, /// This interface gives components a behavior when their entity is in the active hand, when
/// at any range. /// clicking on another object and no interaction occurs, at any range. This includes
/// clicking on an object in the world as well as clicking on an object in inventory.
/// </summary> /// </summary>
public interface IAfterInteract public interface IAfterInteract
{ {

View File

@@ -7,22 +7,35 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components behavior when their owner is put in an inventory slot. /// This interface gives components behavior when their entity is put in a non-hand inventory slot,
/// regardless of where it came from. This includes moving the entity from a hand slot into a non-hand slot
/// (which would also fire <see cref="IUnequippedHand"/>).
///
/// This DOES NOT fire when putting the entity into a hand slot (<see cref="IEquippedHand"/>), nor
/// does it fire when putting the entity into held/equipped storage.
/// </summary> /// </summary>
public interface IEquipped public interface IEquipped
{ {
void Equipped(EquippedEventArgs eventArgs); void Equipped(EquippedEventArgs eventArgs);
} }
public class EquippedEventArgs : EventArgs public abstract class UserEventArgs : EventArgs
{ {
public EquippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) public IEntity User { get; }
protected UserEventArgs(IEntity user)
{ {
User = user; User = user;
}
}
public class EquippedEventArgs : UserEventArgs
{
public EquippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) : base(user)
{
Slot = slot; Slot = slot;
} }
public IEntity User { get; }
public EquipmentSlotDefines.Slots Slot { get; } public EquipmentSlotDefines.Slots Slot { get; }
} }

View File

@@ -0,0 +1,64 @@
using System;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components
{
/// <summary>
/// This interface gives components behavior when their entity is put in a hand inventory slot,
/// even if it came from another hand slot (which would also fire <see cref="IUnequippedHand"/>).
/// This includes moving the entity from a non-hand slot into a hand slot
/// (which would also fire <see cref="IUnequipped"/>).
/// </summary>
public interface IEquippedHand
{
void EquippedHand(EquippedHandEventArgs eventArgs);
}
public class EquippedHandEventArgs : UserEventArgs
{
public EquippedHandEventArgs(IEntity user, SharedHand hand) : base(user)
{
Hand = hand;
}
public SharedHand Hand { get; }
}
/// <summary>
/// Raised when putting the entity into a hand slot
/// </summary>
[PublicAPI]
public class EquippedHandMessage : EntitySystemMessage
{
/// <summary>
/// If this message has already been "handled" by a previous system.
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// Entity that equipped the item.
/// </summary>
public IEntity User { get; }
/// <summary>
/// Item that was equipped.
/// </summary>
public IEntity Equipped { get; }
/// <summary>
/// Hand the item is going into.
/// </summary>
public SharedHand Hand { get; }
public EquippedHandMessage(IEntity user, IEntity equipped, SharedHand hand)
{
User = user;
Equipped = equipped;
Hand = hand;
}
}
}

View File

@@ -8,8 +8,9 @@ using Robust.Shared.Map;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components behavior when being clicked on by a user with an object in their hand /// This interface gives components behavior when their entity is clicked on by a user with an object in their hand
/// who is in range and has unobstructed reach of the target entity (allows inside blockers). /// who is in range and has unobstructed reach of the target entity (allows inside blockers). This includes
/// clicking on an object in the world as well as clicking on an object in inventory.
/// </summary> /// </summary>
public interface IInteractUsing public interface IInteractUsing
{ {

View File

@@ -7,22 +7,25 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components behavior when their owner is removed from an inventory slot. /// This interface gives components behavior when their entity is removed from a non-hand inventory slot,
/// regardless of where it's going to. This includes moving the entity from a non-hand slot into a hand slot
/// (which would also fire <see cref="IEquippedHand"/>).
///
/// This DOES NOT fire when removing the entity from a hand slot (<see cref="IUnequippedHand"/>), nor
/// does it fire when removing the entity from held/equipped storage.
/// </summary> /// </summary>
public interface IUnequipped public interface IUnequipped
{ {
void Unequipped(UnequippedEventArgs eventArgs); void Unequipped(UnequippedEventArgs eventArgs);
} }
public class UnequippedEventArgs : EventArgs public class UnequippedEventArgs : UserEventArgs
{ {
public UnequippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) public UnequippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) : base(user)
{ {
User = user;
Slot = slot; Slot = slot;
} }
public IEntity User { get; }
public EquipmentSlotDefines.Slots Slot { get; } public EquipmentSlotDefines.Slots Slot { get; }
} }
@@ -43,19 +46,19 @@ namespace Content.Shared.Interfaces.GameObjects.Components
public IEntity User { get; } public IEntity User { get; }
/// <summary> /// <summary>
/// Item that was equipped. /// Item that was unequipped.
/// </summary> /// </summary>
public IEntity Equipped { get; } public IEntity Unequipped { get; }
/// <summary> /// <summary>
/// Slot where the item was removed from. /// Slot where the item was removed from.
/// </summary> /// </summary>
public EquipmentSlotDefines.Slots Slot { get; } public EquipmentSlotDefines.Slots Slot { get; }
public UnequippedMessage(IEntity user, IEntity equipped, EquipmentSlotDefines.Slots slot) public UnequippedMessage(IEntity user, IEntity unequipped, EquipmentSlotDefines.Slots slot)
{ {
User = user; User = user;
Equipped = equipped; Unequipped = unequipped;
Slot = slot; Slot = slot;
} }
} }

View File

@@ -0,0 +1,63 @@
using System;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Items;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components
{
/// <summary>
/// This interface gives components behavior when their entity is removed from a hand slot,
/// even if it is going into another hand slot (which would also fire <see cref="IEquippedHand"/>).
/// This includes moving the entity from a hand slot into a non-hand slot (which would also fire <see cref="IEquipped"/>).
/// </summary>
public interface IUnequippedHand
{
void UnequippedHand(UnequippedHandEventArgs eventArgs);
}
public class UnequippedHandEventArgs : UserEventArgs
{
public UnequippedHandEventArgs(IEntity user, SharedHand hand) : base(user)
{
Hand = hand;
}
public SharedHand Hand { get; }
}
/// <summary>
/// Raised when removing the entity from an inventory slot.
/// </summary>
[PublicAPI]
public class UnequippedHandMessage : EntitySystemMessage
{
/// <summary>
/// If this message has already been "handled" by a previous system.
/// </summary>
public bool Handled { get; set; }
/// <summary>
/// Entity that equipped the item.
/// </summary>
public IEntity User { get; }
/// <summary>
/// Item that was unequipped.
/// </summary>
public IEntity Unequipped { get; }
/// <summary>
/// Hand the item is removed from.
/// </summary>
public SharedHand Hand { get; }
public UnequippedHandMessage(IEntity user, IEntity unequipped, SharedHand hand)
{
User = user;
Unequipped = unequipped;
Hand = hand;
}
}
}

View File

@@ -6,7 +6,8 @@ using Robust.Shared.Interfaces.GameObjects;
namespace Content.Shared.Interfaces.GameObjects.Components namespace Content.Shared.Interfaces.GameObjects.Components
{ {
/// <summary> /// <summary>
/// This interface gives components behavior when using the entity in your hands /// This interface gives components behavior when using the entity in your active hand
/// (done by clicking the entity in the active hand or pressing the keybind that defaults to Z).
/// </summary> /// </summary>
public interface IUse public interface IUse
{ {

View File

@@ -0,0 +1,28 @@
using System;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
namespace Content.Shared.Utility
{
/// <summary>
/// Utilities for working with cooldowns.
/// </summary>
public static class Cooldowns
{
/// <param name="gameTiming">game timing to use, otherwise will resolve using IoCManager.</param>
/// <returns>a cooldown interval starting at GameTiming.Curtime and ending at (offset) from CurTime.
/// For example, passing TimeSpan.FromSeconds(5) will create an interval
/// from now to 5 seconds from now.</returns>
public static (TimeSpan start, TimeSpan end) FromNow(TimeSpan offset, IGameTiming gameTiming = null)
{
var now = (gameTiming ?? IoCManager.Resolve<IGameTiming>()).CurTime;
return (now, now + offset);
}
/// <see cref="FromNow"/>
public static (TimeSpan start, TimeSpan end) SecondsFromNow(double seconds, IGameTiming gameTiming = null)
{
return FromNow(TimeSpan.FromSeconds(seconds), gameTiming);
}
}
}

View File

@@ -1,14 +1,10 @@
using System.IO; using System.IO;
using System.Linq;
using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.Components.Mobs;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.Utility;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
namespace Content.Tests.Server.GameObjects.Components.Mobs namespace Content.Tests.Server.GameObjects.Components.Mobs
@@ -48,23 +44,23 @@ namespace Content.Tests.Server.GameObjects.Components.Mobs
var alertsComponent = new ServerAlertsComponent(); var alertsComponent = new ServerAlertsComponent();
alertsComponent = IoCManager.InjectDependencies(alertsComponent); alertsComponent = IoCManager.InjectDependencies(alertsComponent);
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out var lowpressure, out var lpencoded)); Assert.That(alertManager.TryGet(AlertType.LowPressure, out var lowpressure));
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out var highpressure, out var hpencoded)); Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highpressure));
alertsComponent.ShowAlert(AlertType.LowPressure); alertsComponent.ShowAlert(AlertType.LowPressure);
var alertState = alertsComponent.GetComponentState() as AlertsComponentState; var alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.NotNull(alertState); Assert.NotNull(alertState);
Assert.That(alertState.Alerts.Length, Is.EqualTo(1)); Assert.That(alertState.Alerts.Count, Is.EqualTo(1));
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = lpencoded})); Assert.That(alertState.Alerts.ContainsKey(lowpressure.AlertKey));
alertsComponent.ShowAlert(AlertType.HighPressure); alertsComponent.ShowAlert(AlertType.HighPressure);
alertState = alertsComponent.GetComponentState() as AlertsComponentState; alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.That(alertState.Alerts.Length, Is.EqualTo(1)); Assert.That(alertState.Alerts.Count, Is.EqualTo(1));
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = hpencoded})); Assert.That(alertState.Alerts.ContainsKey(highpressure.AlertKey));
alertsComponent.ClearAlertCategory(AlertCategory.Pressure); alertsComponent.ClearAlertCategory(AlertCategory.Pressure);
alertState = alertsComponent.GetComponentState() as AlertsComponentState; alertState = alertsComponent.GetComponentState() as AlertsComponentState;
Assert.That(alertState.Alerts.Length, Is.EqualTo(0)); Assert.That(alertState.Alerts.Count, Is.EqualTo(0));
} }
} }
} }

View File

@@ -37,25 +37,10 @@ namespace Content.Tests.Shared.Alert
Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highPressure)); Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highPressure));
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png")); Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out lowPressure, out var encodedLowPressure)); Assert.That(alertManager.TryGet(AlertType.LowPressure, out lowPressure));
Assert.That(lowPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/lowpressure.png")); Assert.That(lowPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/lowpressure.png"));
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out highPressure, out var encodedHighPressure)); Assert.That(alertManager.TryGet(AlertType.HighPressure, out highPressure));
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png")); Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
Assert.That(alertManager.TryEncode(lowPressure, out var encodedLowPressure2));
Assert.That(encodedLowPressure2, Is.EqualTo(encodedLowPressure));
Assert.That(alertManager.TryEncode(highPressure, out var encodedHighPressure2));
Assert.That(encodedHighPressure2, Is.EqualTo(encodedHighPressure));
Assert.That(encodedLowPressure, Is.Not.EqualTo(encodedHighPressure));
Assert.That(alertManager.TryDecode(encodedLowPressure, out var decodedLowPressure));
Assert.That(decodedLowPressure, Is.EqualTo(lowPressure));
Assert.That(alertManager.TryDecode(encodedHighPressure, out var decodedHighPressure));
Assert.That(decodedHighPressure, Is.EqualTo(highPressure));
Assert.False(alertManager.TryEncode(AlertType.Debug1, out _));
Assert.False(alertManager.TryGetWithEncoded(AlertType.Debug1, out _, out _));
} }
} }
} }

View File

@@ -1,9 +1,13 @@
using System.IO; using System.IO;
using Content.Server.Utility;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Interfaces;
using NUnit.Framework; using NUnit.Framework;
using Robust.Shared.Interfaces.Log; using Robust.Shared.Interfaces.Log;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Log; using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Robust.UnitTesting; using Robust.UnitTesting;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
@@ -22,7 +26,6 @@ namespace Content.Tests.Shared.Alert
minSeverity: 0 minSeverity: 0
maxSeverity: 6"; maxSeverity: 6";
[Test] [Test]
public void TestAlertKey() public void TestAlertKey()
{ {

Some files were not shown because too many files have changed in this diff Show More