Files
wwdpublic/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs
sleepyyapril 885ee5a831 Wizmerge for Station AI (#1351)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

the adding AI is now up to y'all because i'm not touching loadout code
for name datasets, but it shouldn't be too bad from here

---------

Signed-off-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Signed-off-by: SolStar <44028047+ewokswagger@users.noreply.github.com>
Signed-off-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: themias <89101928+themias@users.noreply.github.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: Sphiral <145869023+SphiraI@users.noreply.github.com>
Co-authored-by: Ed <96445749+TheShuEd@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Alzore <140123969+Blackern5000@users.noreply.github.com>
Co-authored-by: ravage <142820619+ravage123321@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Intoxicating-Innocence <188202277+Intoxicating-Innocence@users.noreply.github.com>
Co-authored-by: Saphire <lattice@saphi.re>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: CaasGit <87243814+CaasGit@users.noreply.github.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: NakataRin <45946146+NakataRin@users.noreply.github.com>
Co-authored-by: Kara <lunarautomaton6@gmail.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
Co-authored-by: Doomsdrayk <robotdoughnut@comcast.net>
Co-authored-by: Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: ElectroJr <leonsfriedrich@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Julian Giebel <juliangiebel@live.de>
Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
Co-authored-by: Repo <47093363+Titian3@users.noreply.github.com>
Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: AJCM-git <60196617+AJCM-git@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: no <165581243+pissdemon@users.noreply.github.com>
Co-authored-by: Tornado Tech <54727692+Tornado-Technology@users.noreply.github.com>
Co-authored-by: osjarw <62134478+osjarw@users.noreply.github.com>
Co-authored-by: Simon <63975668+Simyon264@users.noreply.github.com>
Co-authored-by: TGRCDev <tgrc@tgrc.dev>
Co-authored-by: Milon <milonpl.git@proton.me>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
Co-authored-by: chavonadelal <156101927+chavonadelal@users.noreply.github.com>
Co-authored-by: SolStar <44028047+ewokswagger@users.noreply.github.com>
Co-authored-by: K-Dynamic <20566341+K-Dynamic@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: ArchRBX <5040911+ArchRBX@users.noreply.github.com>
Co-authored-by: archrbx <punk.gear5260@fastmail.com>
Co-authored-by: Radezolid <snappednexus@gmail.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: EmoGarbage404 <retron404@gmail.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
Co-authored-by: neuPanda <chriseparton@gmail.com>
Co-authored-by: neuPanda <spainman0@yahoo.com>
Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>
Co-authored-by: Whatstone <whatston3@gmail.com>
Co-authored-by: VideoKompany <135313844+VlaDOS1408@users.noreply.github.com>

(cherry picked from commit 93ed70acfeda357133a701f637d3faeec02749bb)
2025-01-14 00:13:42 +03:00

1089 lines
34 KiB
C#

using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Content.Client.Actions;
using Content.Client.Construction;
using Content.Client.Gameplay;
using Content.Client.Hands;
using Content.Client.Interaction;
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.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Graphics.RSI;
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!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IInputManager _input = 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++)
_pages.Add(new ActionPage(buttonCount));
}
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.OnActionAdded += OnActionAdded;
_actionsSystem.OnActionRemoved += OnActionRemoved;
_actionsSystem.ActionsUpdated += OnActionsUpdated;
}
UpdateFilterLabel();
QueueWindowUpdate();
_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.Down)
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.LocalEntity 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 is { Charges: 0, RenewCharges: false }
|| 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;
case EntityWorldTargetActionComponent entMapTarget:
return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.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, (actionId, action)))
{
// Invalid target.
if (action.DeselectOnMiss)
StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Target = coords;
}
_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, (actionId, action)))
{
if (action.DeselectOnMiss)
StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Target = entity;
}
_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;
}
private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
EntityUid actionId,
EntityWorldTargetActionComponent action,
EntityUid user,
ActionsComponent actionComp)
{
if (_actionsSystem == null)
return false;
var entity = args.EntityUid;
var coords = args.Coordinates;
if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
{
if (action.DeselectOnMiss)
StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Entity = entity;
action.Event.Coords = coords;
}
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
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.SetClickPressed(true);
SearchAndDisplay();
}
private void OnWindowClosed()
{
if (ActionButton != null)
ActionButton.SetClickPressed(false);
}
public void OnStateExited(GameplayState state)
{
if (_actionsSystem != null)
{
_actionsSystem.OnActionAdded -= OnActionAdded;
_actionsSystem.OnActionRemoved -= OnActionRemoved;
_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)
{
if (_actionsSystem == null)
return;
var lastPage = _pages.Count - 1;
if (index < 0)
index = lastPage;
else if (index > lastPage)
index = 0;
_currentPageIndex = index;
var page = _pages[_currentPageIndex];
_container?.SetActionData(_actionsSystem, 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);
}
private void OnActionRemoved(EntityUid actionId)
{
if (_container == null)
return;
if (actionId == SelectingTargetFor)
StopTargeting();
foreach (var page in _pages)
for (var i = 0; i < page.Size; i++)
if (page[i] == actionId)
page[i] = null;
}
private void OnActionsUpdated()
{
QueueWindowUpdate();
if (_container == null)
return;
// TODO ACTIONS allow buttons to persist across state applications
// Then we don't have to interrupt drags any time the buttons get rebuilt.
_menuDragHelper.EndDrag();
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _pages[_currentPageIndex]);
}
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.Container != null && action.Container != _playerManager.LocalEntity,
Filters.Innate => action.Container == null || action.Container == _playerManager.LocalEntity,
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 is not { Disposed: false, IsOpen: true })
return;
if (_actionsSystem == null)
return;
_window.UpdateNeeded = false;
List<ActionButton> existing = new(_window.ResultsGrid.ChildCount);
foreach (var child in _window.ResultsGrid.Children)
{
if (child is ActionButton button)
existing.Add(button);
}
var i = 0;
foreach (var action in actions)
{
if (i < existing.Count)
{
existing[i++].UpdateData(action.Id, _actionsSystem);
continue;
}
var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
button.UpdateData(action.Id, _actionsSystem);
_window.ResultsGrid.AddChild(button);
}
for (; i < existing.Count; i++)
{
existing[i].Dispose();
}
}
public void QueueWindowUpdate()
{
if (_window != null)
_window.UpdateNeeded = true;
}
private void SearchAndDisplay()
{
if (_window is not { Disposed: false, IsOpen: true })
return;
if (_actionsSystem == null)
return;
if (_playerManager.LocalEntity is not { } player)
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.Container == null || action.Comp.Container == player)
return false;
var providerName = EntityManager.GetComponent<MetaDataComponent>(action.Comp.Container.Value).EntityName;
return providerName.Contains(search, StringComparison.OrdinalIgnoreCase);
});
PopulateActions(actions);
}
private void SetAction(ActionButton button, EntityUid? actionId)
{
if (_actionsSystem == null)
return;
int position;
if (actionId == null)
{
button.ClearData();
if (_container?.TryGetButtonIndex(button, out position) ?? false)
CurrentPage[position] = null;
}
else if (button.TryReplaceWith(actionId.Value, _actionsSystem) &&
_container != null &&
_container.TryGetButtonIndex(button, out position))
if (position >= 0 && position < CurrentPage.Size)
CurrentPage[position] = actionId;
else
{
if (_pages.Count <= _currentPageIndex)
return;
// Add the button to the next page if there's no space on the current one
var nextPage = _pages[_currentPageIndex + 1];
int i;
for (i = 0; i < nextPage.Size; i++)
if (nextPage[i] == null)
{
nextPage[i] = actionId;
break;
}
ChangePage(_currentPageIndex + 1); //TODO: Make this a client config?
}
}
private void DragAction()
{
if (_menuDragHelper.Dragged is not {ActionId: {} action} dragged)
{
_menuDragHelper.EndDrag();
return;
}
EntityUid? swapAction = null;
var currentlyHovered = UIManager.MouseGetControl(_input.MouseScreenPosition);
if (currentlyHovered is ActionButton button)
{
swapAction = button.ActionId;
SetAction(button, action);
}
if (dragged.Parent is ActionButtonContainer)
SetAction(dragged, swapAction);
if (_actionsSystem != null)
_container?.SetActionData(_actionsSystem, _pages[_currentPageIndex]);
_menuDragHelper.EndDrag();
}
private void OnClearPressed(ButtonEventArgs args)
{
if (_window == null)
return;
_window.SearchBar.Clear();
_window.FilterButton.DeselectAll();
UpdateFilterLabel();
QueueWindowUpdate();
}
private void OnSearchChanged(LineEditEventArgs args)
{
QueueWindowUpdate();
}
private void OnFilterSelected(ItemPressedEventArgs args)
{
UpdateFilterLabel();
QueueWindowUpdate();
}
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.UIRightClick)
{
SetAction(button, null);
args.Handle();
return;
}
if (args.Function != EngineKeyFunctions.UIClick)
return;
args.Handle();
if (button.ActionId != null)
{
_menuDragHelper.MouseDown(button);
return;
}
var ev = new FillActionSlotEvent();
EntityManager.EventBus.RaiseEvent(EventSource.Local, ev);
if (ev.Action != null)
SetAction(button, ev.Action);
}
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
return;
args.Handle();
if (_menuDragHelper.IsDragging)
{
DragAction();
return;
}
_menuDragHelper.EndDrag();
if (!_actionsSystem.TryGetActionData(button.ActionId, out var baseAction))
return;
if (baseAction is not BaseTargetActionComponent action)
{
_actionsSystem?.TriggerAction(button.ActionId.Value, baseAction);
return;
}
// 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);
}
private bool OnMenuBeginDrag()
{
// TODO ACTIONS
// The dragging icon shuld be based on the entity's icon style. I.e. if the action has a large icon texture,
// and a small item/provider sprite, then the dragged icon should be the big texture, not the provider.
if (_actionsSystem != null && _actionsSystem.TryGetActionData(_menuDragHelper.Dragged?.ActionId, out var action))
{
if (EntityManager.TryGetComponent(action.EntityIcon, out SpriteComponent? sprite)
&& sprite.Icon?.GetFrame(RsiDirection.South, 0) is {} frame)
_dragShadow.Texture = frame;
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()
{
UnloadGui();
_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 -= OnActionUnpressed;
}
_container = container;
_container.ActionPressed += OnActionPressed;
_container.ActionUnpressed += OnActionUnpressed;
}
private void ClearActions()
{
_container?.ClearActionData();
}
private void AssignSlots(List<SlotAssignment> assignments)
{
if (_actionsSystem == null)
return;
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
_pages[assignment.Hotbar][assignment.Slot] = assignment.ActionId;
_container?.SetActionData(_actionsSystem, _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);
if (_window is {UpdateNeeded: true})
SearchAndDisplay();
}
private void OnComponentLinked(ActionsComponent component)
{
if (_actionsSystem == null)
return;
LoadDefaultActions();
_container?.SetActionData(_actionsSystem, _pages[_currentPageIndex]);
QueueWindowUpdate();
}
private void OnComponentUnlinked()
{
_container?.ClearActionData();
QueueWindowUpdate();
StopTargeting();
}
private void LoadDefaultActions()
{
if (_actionsSystem == null)
return;
var actions = _actionsSystem.GetClientActions().Where(action => action.Comp.AutoPopulate).ToList();
actions.Sort(ActionComparer);
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++)
if (slot + offset < 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;
// TODO inform the server
action.Toggled = true;
// override "held-item" overlay
var provider = action.Container;
if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
{
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Container != 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);
}
if (_container != null)
{
foreach (var button in _container.GetButtons())
{
if (button.ActionId == actionId)
button.UpdateIcons();
}
}
// 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;
var oldAction = SelectingTargetFor;
if (_actionsSystem != null && _actionsSystem.TryGetActionData(oldAction, out var action))
{
// TODO inform the server
action.Toggled = false;
}
SelectingTargetFor = null;
_targetOutline?.Disable();
_interactionOutline?.SetEnabled(true);
if (_container != null)
{
foreach (var button in _container.GetButtons())
{
if (button.ActionId == oldAction)
button.UpdateIcons();
}
}
if (!_overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
return;
handOverlay.IconOverride = null;
handOverlay.EntityOverride = null;
}
//TODO: Serialize this shit
private sealed class ActionPage(int size)
{
public readonly EntityUid?[] Data = new EntityUid?[size];
public EntityUid? this[int index]
{
get => Data[index];
set => Data[index] = value;
}
public static implicit operator EntityUid?[](ActionPage p) => p.Data.ToArray();
public void Clear() => Array.Fill(Data, null);
public int Size => Data.Length;
}
}