1048 lines
31 KiB
C#
1048 lines
31 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using Content.Client.Actions;
|
|
using Content.Client.Construction;
|
|
using Content.Client.DragDrop;
|
|
using Content.Client.Gameplay;
|
|
using Content.Client.Hands;
|
|
using Content.Client.Outline;
|
|
using Content.Client.UserInterface.Controls;
|
|
using Content.Client.UserInterface.Systems.Actions.Controls;
|
|
using Content.Client.UserInterface.Systems.Actions.Widgets;
|
|
using Content.Client.UserInterface.Systems.Actions.Windows;
|
|
using Content.Client.UserInterface.Systems.Gameplay;
|
|
using Content.Shared.Actions;
|
|
using Content.Shared.Input;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.Player;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controllers;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Input.Binding;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using static Content.Client.Actions.ActionsSystem;
|
|
using static Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow;
|
|
using static Robust.Client.UserInterface.Control;
|
|
using static Robust.Client.UserInterface.Controls.BaseButton;
|
|
using static Robust.Client.UserInterface.Controls.LineEdit;
|
|
using static Robust.Client.UserInterface.Controls.MultiselectOptionButton<
|
|
Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow.Filters>;
|
|
using static Robust.Client.UserInterface.Controls.TextureRect;
|
|
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
|
|
|
|
namespace Content.Client.UserInterface.Systems.Actions;
|
|
|
|
public sealed class ActionUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<ActionsSystem>
|
|
{
|
|
[Dependency] private readonly IOverlayManager _overlays = default!;
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
|
|
[UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
|
|
[UISystemDependency] private readonly InteractionOutlineSystem? _interactionOutline = default;
|
|
[UISystemDependency] private readonly TargetOutlineSystem? _targetOutline = default;
|
|
[UISystemDependency] private readonly SpriteSystem _spriteSystem = default!;
|
|
|
|
private const int DefaultPageIndex = 0;
|
|
private ActionButtonContainer? _container;
|
|
private readonly List<ActionPage> _pages = new();
|
|
private int _currentPageIndex = DefaultPageIndex;
|
|
private readonly DragDropHelper<ActionButton> _menuDragHelper;
|
|
private readonly TextureRect _dragShadow;
|
|
private ActionsWindow? _window;
|
|
|
|
private ActionsBar? ActionsBar => UIManager.GetActiveUIWidgetOrNull<ActionsBar>();
|
|
private MenuButton? ActionButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.ActionButton;
|
|
private ActionPage CurrentPage => _pages[_currentPageIndex];
|
|
|
|
public bool IsDragging => _menuDragHelper.IsDragging;
|
|
|
|
/// <summary>
|
|
/// Action slot we are currently selecting a target for.
|
|
/// </summary>
|
|
public EntityUid? SelectingTargetFor { get; private set; }
|
|
|
|
public ActionUIController()
|
|
{
|
|
_menuDragHelper = new DragDropHelper<ActionButton>(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag);
|
|
_dragShadow = new TextureRect
|
|
{
|
|
MinSize = new Vector2(64, 64),
|
|
Stretch = StretchMode.Scale,
|
|
Visible = false,
|
|
SetSize = new Vector2(64, 64),
|
|
MouseFilter = MouseFilterMode.Ignore
|
|
};
|
|
|
|
var pageCount = ContentKeyFunctions.GetLoadoutBoundKeys().Length;
|
|
var buttonCount = ContentKeyFunctions.GetHotbarBoundKeys().Length;
|
|
for (var i = 0; i < pageCount; i++)
|
|
{
|
|
var page = new ActionPage(buttonCount);
|
|
_pages.Add(page);
|
|
}
|
|
}
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
var gameplayStateLoad = UIManager.GetUIController<GameplayStateLoadController>();
|
|
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
|
|
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
|
|
}
|
|
|
|
private void OnScreenLoad()
|
|
{
|
|
LoadGui();
|
|
}
|
|
|
|
private void OnScreenUnload()
|
|
{
|
|
UnloadGui();
|
|
}
|
|
|
|
public void OnStateEntered(GameplayState state)
|
|
{
|
|
if (_actionsSystem != null)
|
|
{
|
|
_actionsSystem.ActionAdded += OnActionAdded;
|
|
_actionsSystem.ActionRemoved += OnActionRemoved;
|
|
_actionsSystem.ActionReplaced += OnActionReplaced;
|
|
_actionsSystem.ActionsUpdated += OnActionsUpdated;
|
|
}
|
|
|
|
UpdateFilterLabel();
|
|
SearchAndDisplay();
|
|
|
|
_dragShadow.Orphan();
|
|
UIManager.PopupRoot.AddChild(_dragShadow);
|
|
|
|
var builder = CommandBinds.Builder;
|
|
var hotbarKeys = ContentKeyFunctions.GetHotbarBoundKeys();
|
|
for (var i = 0; i < hotbarKeys.Length; i++)
|
|
{
|
|
var boundId = i; // This is needed, because the lambda captures it.
|
|
var boundKey = hotbarKeys[i];
|
|
builder = builder.Bind(boundKey, new PointerInputCmdHandler((in PointerInputCmdArgs args) =>
|
|
{
|
|
if (args.State != BoundKeyState.Up)
|
|
return false;
|
|
|
|
TriggerAction(boundId);
|
|
return true;
|
|
}, false, true));
|
|
}
|
|
|
|
var loadoutKeys = ContentKeyFunctions.GetLoadoutBoundKeys();
|
|
for (var i = 0; i < loadoutKeys.Length; i++)
|
|
{
|
|
var boundId = i; // This is needed, because the lambda captures it.
|
|
var boundKey = loadoutKeys[i];
|
|
builder = builder.Bind(boundKey,
|
|
InputCmdHandler.FromDelegate(_ => ChangePage(boundId)));
|
|
}
|
|
|
|
builder
|
|
.Bind(ContentKeyFunctions.OpenActionsMenu,
|
|
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
|
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
|
|
typeof(ConstructionSystem), typeof(DragDropSystem))
|
|
.BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
|
|
.Register<ActionUIController>();
|
|
}
|
|
|
|
private bool TargetingCancel(in PointerInputCmdArgs args)
|
|
{
|
|
if (!_timing.IsFirstTimePredicted)
|
|
return false;
|
|
|
|
// only do something for actual target-based actions
|
|
if (SelectingTargetFor == null)
|
|
return false;
|
|
|
|
StopTargeting();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
|
|
/// </summary>
|
|
private bool TargetingOnUse(in PointerInputCmdArgs args)
|
|
{
|
|
if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } actionId)
|
|
return false;
|
|
|
|
if (_playerManager.LocalPlayer?.ControlledEntity is not { } user)
|
|
return false;
|
|
|
|
if (!EntityManager.TryGetComponent(user, out ActionsComponent? comp))
|
|
return false;
|
|
|
|
if (!_actionsSystem.TryGetActionData(actionId, out var baseAction) ||
|
|
baseAction is not BaseTargetActionComponent action)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Is the action currently valid?
|
|
if (!action.Enabled
|
|
|| action.Charges is 0
|
|
|| action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
|
|
{
|
|
// The user is targeting with this action, but it is not valid. Maybe mark this click as
|
|
// handled and prevent further interactions.
|
|
return !action.InteractOnMiss;
|
|
}
|
|
|
|
switch (action)
|
|
{
|
|
case WorldTargetActionComponent mapTarget:
|
|
return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
|
|
|
|
case EntityTargetActionComponent entTarget:
|
|
return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
|
|
|
|
default:
|
|
Logger.Error($"Unknown targeting action: {actionId.GetType()}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool TryTargetWorld(in PointerInputCmdArgs args, EntityUid actionId, WorldTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
|
|
{
|
|
if (_actionsSystem == null)
|
|
return false;
|
|
|
|
var coords = args.Coordinates;
|
|
|
|
if (!_actionsSystem.ValidateWorldTarget(user, coords, action))
|
|
{
|
|
// Invalid target.
|
|
if (action.DeselectOnMiss)
|
|
StopTargeting();
|
|
|
|
return false;
|
|
}
|
|
|
|
if (action.ClientExclusive)
|
|
{
|
|
if (action.Event != null)
|
|
{
|
|
action.Event.Target = coords;
|
|
action.Event.Performer = user;
|
|
}
|
|
|
|
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
|
|
}
|
|
else
|
|
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetCoordinates(coords)));
|
|
|
|
if (!action.Repeat)
|
|
StopTargeting();
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryTargetEntity(in PointerInputCmdArgs args, EntityUid actionId, EntityTargetActionComponent action, EntityUid user, ActionsComponent actionComp)
|
|
{
|
|
if (_actionsSystem == null)
|
|
return false;
|
|
|
|
var entity = args.EntityUid;
|
|
|
|
if (!_actionsSystem.ValidateEntityTarget(user, entity, action))
|
|
{
|
|
if (action.DeselectOnMiss)
|
|
StopTargeting();
|
|
|
|
return false;
|
|
}
|
|
|
|
if (action.ClientExclusive)
|
|
{
|
|
if (action.Event != null)
|
|
{
|
|
action.Event.Target = entity;
|
|
action.Event.Performer = user;
|
|
}
|
|
|
|
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
|
|
}
|
|
else
|
|
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid)));
|
|
|
|
if (!action.Repeat)
|
|
StopTargeting();
|
|
|
|
return true;
|
|
}
|
|
|
|
public void UnloadButton()
|
|
{
|
|
if (ActionButton == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActionButton.OnPressed -= ActionButtonPressed;
|
|
}
|
|
|
|
public void LoadButton()
|
|
{
|
|
if (ActionButton == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActionButton.OnPressed += ActionButtonPressed;
|
|
}
|
|
|
|
private void OnWindowOpened()
|
|
{
|
|
if (ActionButton != null)
|
|
ActionButton.Pressed = true;
|
|
}
|
|
|
|
private void OnWindowClosed()
|
|
{
|
|
if (ActionButton != null)
|
|
ActionButton.Pressed = false;
|
|
}
|
|
|
|
public void OnStateExited(GameplayState state)
|
|
{
|
|
if (_actionsSystem != null)
|
|
{
|
|
_actionsSystem.ActionAdded -= OnActionAdded;
|
|
_actionsSystem.ActionRemoved -= OnActionRemoved;
|
|
_actionsSystem.ActionReplaced -= OnActionReplaced;
|
|
_actionsSystem.ActionsUpdated -= OnActionsUpdated;
|
|
}
|
|
|
|
CommandBinds.Unregister<ActionUIController>();
|
|
}
|
|
|
|
private void TriggerAction(int index)
|
|
{
|
|
if (_actionsSystem == null ||
|
|
CurrentPage[index] is not { } actionId ||
|
|
!_actionsSystem.TryGetActionData(actionId, out var baseAction))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (baseAction is BaseTargetActionComponent action)
|
|
ToggleTargeting(actionId, action);
|
|
else
|
|
_actionsSystem?.TriggerAction(actionId, baseAction);
|
|
}
|
|
|
|
private void ChangePage(int index)
|
|
{
|
|
var lastPage = _pages.Count - 1;
|
|
if (index < 0)
|
|
{
|
|
index = lastPage;
|
|
}
|
|
else if (index > lastPage)
|
|
{
|
|
index = 0;
|
|
}
|
|
|
|
_currentPageIndex = index;
|
|
var page = _pages[_currentPageIndex];
|
|
_container?.SetActionData(page);
|
|
|
|
ActionsBar!.PageButtons.Label.Text = $"{_currentPageIndex + 1}";
|
|
}
|
|
|
|
private void OnLeftArrowPressed(ButtonEventArgs args)
|
|
{
|
|
ChangePage(_currentPageIndex - 1);
|
|
}
|
|
|
|
private void OnRightArrowPressed(ButtonEventArgs args)
|
|
{
|
|
ChangePage(_currentPageIndex + 1);
|
|
}
|
|
|
|
private void AppendAction(EntityUid action)
|
|
{
|
|
if (_container == null)
|
|
return;
|
|
|
|
foreach (var button in _container.GetButtons())
|
|
{
|
|
if (button.ActionId != null)
|
|
continue;
|
|
|
|
SetAction(button, action);
|
|
return;
|
|
}
|
|
|
|
foreach (var page in _pages)
|
|
{
|
|
for (var i = 0; i < page.Size; i++)
|
|
{
|
|
var pageAction = page[i];
|
|
if (pageAction != null)
|
|
continue;
|
|
|
|
page[i] = action;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnActionAdded(EntityUid actionId)
|
|
{
|
|
if (_actionsSystem == null ||
|
|
!_actionsSystem.TryGetActionData(actionId, out var action))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// if the action is toggled when we add it, start targeting
|
|
if (action is BaseTargetActionComponent targetAction && action.Toggled)
|
|
StartTargeting(actionId, targetAction);
|
|
|
|
foreach (var page in _pages)
|
|
{
|
|
for (var i = 0; i < page.Size; i++)
|
|
{
|
|
if (page[i] == actionId)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
AppendAction(actionId);
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnActionRemoved(EntityUid actionId)
|
|
{
|
|
if (_container == null)
|
|
return;
|
|
|
|
// stop targeting if the action is removed
|
|
if (actionId == SelectingTargetFor)
|
|
StopTargeting();
|
|
|
|
foreach (var button in _container.GetButtons())
|
|
{
|
|
if (button.ActionId == actionId)
|
|
{
|
|
SetAction(button, null);
|
|
}
|
|
}
|
|
|
|
foreach (var page in _pages)
|
|
{
|
|
for (var i = 0; i < page.Size; i++)
|
|
{
|
|
if (page[i] == actionId)
|
|
{
|
|
page[i] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnActionReplaced(EntityUid actionId)
|
|
{
|
|
if (_container == null)
|
|
return;
|
|
|
|
foreach (var button in _container.GetButtons())
|
|
{
|
|
if (button.ActionId == actionId)
|
|
button.UpdateData(actionId);
|
|
}
|
|
}
|
|
|
|
private void OnActionsUpdated()
|
|
{
|
|
if (_container == null)
|
|
return;
|
|
|
|
foreach (var button in _container.GetButtons())
|
|
{
|
|
button.UpdateIcons();
|
|
}
|
|
}
|
|
|
|
private void ActionButtonPressed(ButtonEventArgs args)
|
|
{
|
|
ToggleWindow();
|
|
}
|
|
|
|
private void ToggleWindow()
|
|
{
|
|
if (_window == null)
|
|
return;
|
|
|
|
if (_window.IsOpen)
|
|
{
|
|
_window.Close();
|
|
return;
|
|
}
|
|
|
|
_window.Open();
|
|
}
|
|
|
|
private void UpdateFilterLabel()
|
|
{
|
|
if (_window == null)
|
|
return;
|
|
|
|
if (_window.FilterButton.SelectedKeys.Count == 0)
|
|
{
|
|
_window.FilterLabel.Visible = false;
|
|
}
|
|
else
|
|
{
|
|
_window.FilterLabel.Visible = true;
|
|
_window.FilterLabel.Text = Loc.GetString("ui-actionmenu-filter-label",
|
|
("selectedLabels", string.Join(", ", _window.FilterButton.SelectedLabels)));
|
|
}
|
|
}
|
|
|
|
private bool MatchesFilter(BaseActionComponent action, Filters filter)
|
|
{
|
|
return filter switch
|
|
{
|
|
Filters.Enabled => action.Enabled,
|
|
Filters.Item => action.Provider != null && action.Provider != _playerManager.LocalPlayer?.ControlledEntity,
|
|
Filters.Innate => action.Provider == null || action.Provider == _playerManager.LocalPlayer?.ControlledEntity,
|
|
Filters.Instant => action is InstantActionComponent,
|
|
Filters.Targeted => action is BaseTargetActionComponent,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
|
|
};
|
|
}
|
|
|
|
private void ClearList()
|
|
{
|
|
if (_window?.Disposed == false)
|
|
_window.ResultsGrid.RemoveAllChildren();
|
|
}
|
|
|
|
private void PopulateActions(IEnumerable<(EntityUid Id, BaseActionComponent Comp)> actions)
|
|
{
|
|
if (_window == null)
|
|
return;
|
|
|
|
ClearList();
|
|
|
|
foreach (var action in actions)
|
|
{
|
|
var button = new ActionButton {Locked = true};
|
|
|
|
button.UpdateData(action.Id);
|
|
button.ActionPressed += OnWindowActionPressed;
|
|
button.ActionUnpressed += OnWindowActionUnPressed;
|
|
button.ActionFocusExited += OnWindowActionFocusExisted;
|
|
|
|
_window.ResultsGrid.AddChild(button);
|
|
}
|
|
}
|
|
|
|
private void SearchAndDisplay()
|
|
{
|
|
if (_window is not { Disposed: false } || _actionsSystem == null)
|
|
return;
|
|
|
|
var search = _window.SearchBar.Text;
|
|
var filters = _window.FilterButton.SelectedKeys;
|
|
var actions = _actionsSystem.GetClientActions();
|
|
|
|
if (filters.Count == 0 && string.IsNullOrWhiteSpace(search))
|
|
{
|
|
PopulateActions(actions);
|
|
return;
|
|
}
|
|
|
|
actions = actions.Where(action =>
|
|
{
|
|
if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action.Comp, filter)))
|
|
return false;
|
|
|
|
if (action.Comp.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
|
|
return true;
|
|
|
|
var name = EntityManager.GetComponent<MetaDataComponent>(action.Id).EntityName;
|
|
if (name.Contains(search, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
|
|
if (action.Comp.Provider == null || action.Comp.Provider == _playerManager.LocalPlayer?.ControlledEntity)
|
|
return false;
|
|
|
|
var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Provider.Value).EntityName;
|
|
return providerName.Contains(search, StringComparison.OrdinalIgnoreCase);
|
|
});
|
|
|
|
PopulateActions(actions);
|
|
}
|
|
|
|
private void SetAction(ActionButton button, EntityUid? actionId)
|
|
{
|
|
int position;
|
|
|
|
if (actionId == null)
|
|
{
|
|
button.ClearData();
|
|
if (_container?.TryGetButtonIndex(button, out position) ?? false)
|
|
{
|
|
CurrentPage[position] = actionId;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (button.TryReplaceWith(actionId.Value) &&
|
|
_container != null &&
|
|
_container.TryGetButtonIndex(button, out position))
|
|
{
|
|
CurrentPage[position] = actionId;
|
|
}
|
|
}
|
|
|
|
private void DragAction()
|
|
{
|
|
if (UIManager.CurrentlyHovered is ActionButton button)
|
|
{
|
|
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.ActionId is not { } type)
|
|
{
|
|
_menuDragHelper.EndDrag();
|
|
return;
|
|
}
|
|
|
|
SetAction(button, type);
|
|
}
|
|
|
|
if (_menuDragHelper.Dragged is {Parent: ActionButtonContainer} old)
|
|
{
|
|
SetAction(old, null);
|
|
}
|
|
|
|
_menuDragHelper.EndDrag();
|
|
}
|
|
|
|
private void OnClearPressed(ButtonEventArgs args)
|
|
{
|
|
if (_window == null)
|
|
return;
|
|
|
|
_window.SearchBar.Clear();
|
|
_window.FilterButton.DeselectAll();
|
|
UpdateFilterLabel();
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnSearchChanged(LineEditEventArgs args)
|
|
{
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnFilterSelected(ItemPressedEventArgs args)
|
|
{
|
|
UpdateFilterLabel();
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnWindowActionPressed(GUIBoundKeyEventArgs args, ActionButton action)
|
|
{
|
|
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
|
return;
|
|
|
|
_menuDragHelper.MouseDown(action);
|
|
args.Handle();
|
|
}
|
|
|
|
private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
|
|
{
|
|
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
|
return;
|
|
|
|
DragAction();
|
|
args.Handle();
|
|
}
|
|
|
|
private void OnWindowActionFocusExisted(ActionButton button)
|
|
{
|
|
_menuDragHelper.EndDrag();
|
|
}
|
|
|
|
private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
|
|
{
|
|
if (args.Function == EngineKeyFunctions.UIClick)
|
|
{
|
|
if (button.ActionId == null)
|
|
{
|
|
var ev = new FillActionSlotEvent();
|
|
EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
|
|
if (ev.Action != null)
|
|
SetAction(button, ev.Action);
|
|
}
|
|
else
|
|
{
|
|
_menuDragHelper.MouseDown(button);
|
|
}
|
|
|
|
args.Handle();
|
|
}
|
|
else if (args.Function == EngineKeyFunctions.UIRightClick)
|
|
{
|
|
SetAction(button, null);
|
|
args.Handle();
|
|
}
|
|
}
|
|
|
|
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
|
|
{
|
|
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
|
|
return;
|
|
|
|
if (UIManager.CurrentlyHovered == button)
|
|
{
|
|
_menuDragHelper.EndDrag();
|
|
|
|
if (_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
|
|
{
|
|
if (baseAction is BaseTargetActionComponent action)
|
|
{
|
|
// 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
|
|
ToggleTargeting(button.ActionId.Value, action);
|
|
return;
|
|
}
|
|
|
|
_actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DragAction();
|
|
}
|
|
|
|
args.Handle();
|
|
}
|
|
|
|
private bool OnMenuBeginDrag()
|
|
{
|
|
if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
|
|
{
|
|
var entIcon = action.EntityIcon;
|
|
|
|
if (entIcon != null)
|
|
{
|
|
_dragShadow.Texture = EntityManager.GetComponent<SpriteComponent>(entIcon.Value).Icon?
|
|
.GetFrame(RSI.State.Direction.South, 0);
|
|
}
|
|
else if (action.Icon != null)
|
|
{
|
|
_dragShadow.Texture = _spriteSystem.Frame0(action.Icon);
|
|
}
|
|
else
|
|
{
|
|
_dragShadow.Texture = null;
|
|
}
|
|
}
|
|
|
|
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - new Vector2(32, 32));
|
|
return true;
|
|
}
|
|
|
|
private bool OnMenuContinueDrag(float frameTime)
|
|
{
|
|
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - new Vector2(32, 32));
|
|
_dragShadow.Visible = true;
|
|
return true;
|
|
}
|
|
|
|
private void OnMenuEndDrag()
|
|
{
|
|
_dragShadow.Texture = null;
|
|
_dragShadow.Visible = false;
|
|
}
|
|
|
|
private void UnloadGui()
|
|
{
|
|
_actionsSystem?.UnlinkAllActions();
|
|
|
|
if (ActionsBar == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActionsBar.PageButtons.LeftArrow.OnPressed -= OnLeftArrowPressed;
|
|
ActionsBar.PageButtons.RightArrow.OnPressed -= OnRightArrowPressed;
|
|
|
|
if (_window != null)
|
|
{
|
|
_window.OnOpen -= OnWindowOpened;
|
|
_window.OnClose -= OnWindowClosed;
|
|
_window.ClearButton.OnPressed -= OnClearPressed;
|
|
_window.SearchBar.OnTextChanged -= OnSearchChanged;
|
|
_window.FilterButton.OnItemSelected -= OnFilterSelected;
|
|
|
|
_window.Dispose();
|
|
_window = null;
|
|
}
|
|
}
|
|
|
|
private void LoadGui()
|
|
{
|
|
DebugTools.Assert(_window == null);
|
|
_window = UIManager.CreateWindow<ActionsWindow>();
|
|
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
|
|
|
|
_window.OnOpen += OnWindowOpened;
|
|
_window.OnClose += OnWindowClosed;
|
|
_window.ClearButton.OnPressed += OnClearPressed;
|
|
_window.SearchBar.OnTextChanged += OnSearchChanged;
|
|
_window.FilterButton.OnItemSelected += OnFilterSelected;
|
|
|
|
if (ActionsBar == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ActionsBar.PageButtons.LeftArrow.OnPressed += OnLeftArrowPressed;
|
|
ActionsBar.PageButtons.RightArrow.OnPressed += OnRightArrowPressed;
|
|
|
|
RegisterActionContainer(ActionsBar.ActionsContainer);
|
|
|
|
_actionsSystem?.LinkAllActions();
|
|
}
|
|
|
|
public void RegisterActionContainer(ActionButtonContainer container)
|
|
{
|
|
if (_container != null)
|
|
{
|
|
_container.ActionPressed -= OnActionPressed;
|
|
_container.ActionUnpressed -= OnActionPressed;
|
|
}
|
|
|
|
_container = container;
|
|
_container.ActionPressed += OnActionPressed;
|
|
_container.ActionUnpressed += OnActionUnpressed;
|
|
}
|
|
|
|
private void ClearActions()
|
|
{
|
|
_container?.ClearActionData();
|
|
}
|
|
|
|
private void AssignSlots(List<SlotAssignment> assignments)
|
|
{
|
|
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
|
|
{
|
|
_pages[assignment.Hotbar][assignment.Slot] = assignment.ActionId;
|
|
}
|
|
|
|
_container?.SetActionData(_pages[_currentPageIndex]);
|
|
}
|
|
|
|
public void RemoveActionContainer()
|
|
{
|
|
_container = null;
|
|
}
|
|
|
|
public void OnSystemLoaded(ActionsSystem system)
|
|
{
|
|
system.LinkActions += OnComponentLinked;
|
|
system.UnlinkActions += OnComponentUnlinked;
|
|
system.ClearAssignments += ClearActions;
|
|
system.AssignSlot += AssignSlots;
|
|
}
|
|
|
|
public void OnSystemUnloaded(ActionsSystem system)
|
|
{
|
|
system.LinkActions -= OnComponentLinked;
|
|
system.UnlinkActions -= OnComponentUnlinked;
|
|
system.ClearAssignments -= ClearActions;
|
|
system.AssignSlot -= AssignSlots;
|
|
}
|
|
|
|
public override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
_menuDragHelper.Update(args.DeltaSeconds);
|
|
}
|
|
|
|
private void OnComponentLinked(ActionsComponent component)
|
|
{
|
|
LoadDefaultActions(component);
|
|
_container?.SetActionData(_pages[DefaultPageIndex]);
|
|
SearchAndDisplay();
|
|
}
|
|
|
|
private void OnComponentUnlinked()
|
|
{
|
|
_container?.ClearActionData();
|
|
SearchAndDisplay();
|
|
StopTargeting();
|
|
}
|
|
|
|
private void LoadDefaultActions(ActionsComponent component)
|
|
{
|
|
if (_actionsSystem == null)
|
|
return;
|
|
|
|
var actions = _actionsSystem.GetClientActions().Where(action => action.Comp.AutoPopulate).ToList();
|
|
|
|
var offset = 0;
|
|
var totalPages = _pages.Count;
|
|
var pagesLeft = totalPages;
|
|
var currentPage = DefaultPageIndex;
|
|
while (pagesLeft > 0)
|
|
{
|
|
var page = _pages[currentPage];
|
|
var pageSize = page.Size;
|
|
|
|
for (var slot = 0; slot < pageSize; slot++)
|
|
{
|
|
var actionIndex = slot + offset;
|
|
if (actionIndex < actions.Count)
|
|
{
|
|
page[slot] = actions[slot + offset].Id;
|
|
}
|
|
else
|
|
{
|
|
page[slot] = null;
|
|
}
|
|
}
|
|
|
|
offset += pageSize;
|
|
currentPage++;
|
|
if (currentPage == totalPages)
|
|
{
|
|
currentPage = 0;
|
|
}
|
|
|
|
pagesLeft--;
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
private void ToggleTargeting(EntityUid actionId, BaseTargetActionComponent action)
|
|
{
|
|
if (SelectingTargetFor == actionId)
|
|
{
|
|
StopTargeting();
|
|
return;
|
|
}
|
|
|
|
StartTargeting(actionId, action);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Puts us in targeting mode, where we need to pick either a target point or entity
|
|
/// </summary>
|
|
private void StartTargeting(EntityUid actionId, BaseTargetActionComponent action)
|
|
{
|
|
// If we were targeting something else we should stop
|
|
StopTargeting();
|
|
|
|
SelectingTargetFor = actionId;
|
|
|
|
// override "held-item" overlay
|
|
var provider = action.Provider;
|
|
|
|
if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
|
|
{
|
|
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
|
|
{
|
|
handOverlay.EntityOverride = provider;
|
|
}
|
|
else if (action.Toggled && action.IconOn != null)
|
|
handOverlay.IconOverride = _spriteSystem.Frame0(action.IconOn);
|
|
else if (action.Icon != null)
|
|
handOverlay.IconOverride = _spriteSystem.Frame0(action.Icon);
|
|
}
|
|
|
|
// TODO: allow world-targets to check valid positions. E.g., maybe:
|
|
// - Draw a red/green ghost entity
|
|
// - Add a yes/no checkmark where the HandItemOverlay usually is
|
|
|
|
// Highlight valid entity targets
|
|
if (action is not EntityTargetActionComponent entityAction)
|
|
return;
|
|
|
|
Func<EntityUid, bool>? predicate = null;
|
|
var attachedEnt = entityAction.AttachedEntity;
|
|
|
|
if (!entityAction.CanTargetSelf)
|
|
predicate = e => e != attachedEnt;
|
|
|
|
var range = entityAction.CheckCanAccess ? action.Range : -1;
|
|
|
|
_interactionOutline?.SetEnabled(false);
|
|
_targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switch out of targeting mode if currently selecting target for an action
|
|
/// </summary>
|
|
private void StopTargeting()
|
|
{
|
|
if (SelectingTargetFor == null)
|
|
return;
|
|
|
|
SelectingTargetFor = null;
|
|
_targetOutline?.Disable();
|
|
_interactionOutline?.SetEnabled(true);
|
|
|
|
if (!_overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
|
|
return;
|
|
|
|
handOverlay.IconOverride = null;
|
|
handOverlay.EntityOverride = null;
|
|
}
|
|
|
|
|
|
//TODO: Serialize this shit
|
|
private sealed class ActionPage
|
|
{
|
|
private readonly EntityUid?[] _data;
|
|
|
|
public ActionPage(int size)
|
|
{
|
|
_data = new EntityUid?[size];
|
|
}
|
|
|
|
public EntityUid? this[int index]
|
|
{
|
|
get => _data[index];
|
|
set => _data[index] = value;
|
|
}
|
|
|
|
public static implicit operator EntityUid?[](ActionPage p)
|
|
{
|
|
return p._data.ToArray();
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
Array.Fill(_data, null);
|
|
}
|
|
|
|
public int Size => _data.Length;
|
|
}
|
|
}
|