2021-10-28 18:21:19 +13:00
using System.Linq ;
2023-07-08 14:08:32 +10:00
using System.Numerics ;
2024-08-04 13:43:55 +00:00
using Content.Client.Actions ;
2022-09-29 15:51:59 +10:00
using Content.Client.CombatMode ;
2021-10-28 18:21:19 +13:00
using Content.Client.Examine ;
2022-09-04 17:21:14 -07:00
using Content.Client.Gameplay ;
2024-08-03 15:23:46 +00:00
using Content.Client.Popups ;
2024-08-04 13:43:55 +00:00
using Content.Client.UserInterface.Systems.Actions ;
2021-10-28 18:21:19 +13:00
using Content.Client.Verbs ;
2023-01-07 21:24:52 -06:00
using Content.Client.Verbs.UI ;
2024-08-04 13:43:55 +00:00
using Content.Shared._White.Item ;
using Content.Shared.Actions ;
2021-10-28 18:21:19 +13:00
using Content.Shared.CCVar ;
2022-10-15 17:15:25 +13:00
using Content.Shared.Examine ;
2024-08-04 13:43:55 +00:00
using Content.Shared.Hands.Components ;
2024-01-19 02:18:53 +04:00
using Content.Shared.IdentityManagement ;
2021-10-28 18:21:19 +13:00
using Content.Shared.Input ;
2024-08-03 15:23:46 +00:00
using Content.Shared.Mobs.Components ;
2024-08-04 13:43:55 +00:00
using Content.Shared.Ninja.Components ;
2024-08-03 15:23:46 +00:00
using Content.Shared.Popups ;
2021-10-28 18:21:19 +13:00
using Robust.Client.GameObjects ;
using Robust.Client.Graphics ;
using Robust.Client.Input ;
using Robust.Client.Player ;
using Robust.Client.State ;
using Robust.Client.UserInterface ;
2023-01-07 21:24:52 -06:00
using Robust.Client.UserInterface.Controllers ;
2021-10-28 18:21:19 +13:00
using Robust.Shared.Configuration ;
using Robust.Shared.Input ;
using Robust.Shared.Input.Binding ;
2022-10-15 17:15:25 +13:00
using Robust.Shared.Map ;
2021-10-28 18:21:19 +13:00
using Robust.Shared.Timing ;
2021-12-05 18:09:01 +01:00
2021-10-28 18:21:19 +13:00
namespace Content.Client.ContextMenu.UI
{
/// <summary>
/// This class handles the displaying of the entity context menu.
/// </summary>
/// <remarks>
2023-01-07 21:24:52 -06:00
/// This also provides functions to get
2021-10-28 18:21:19 +13:00
/// a list of entities near the mouse position, add them to the context menu grouped by prototypes, and remove
/// them from the menu as they move out of sight.
/// </remarks>
2023-01-07 21:24:52 -06:00
public sealed partial class EntityMenuUIController : UIController , IOnStateEntered < GameplayState > , IOnStateExited < GameplayState >
2021-10-28 18:21:19 +13:00
{
[Dependency] private readonly IEntitySystemManager _systemManager = default ! ;
[Dependency] private readonly IEntityManager _entityManager = default ! ;
[Dependency] private readonly IPlayerManager _playerManager = default ! ;
[Dependency] private readonly IStateManager _stateManager = default ! ;
[Dependency] private readonly IInputManager _inputManager = default ! ;
[Dependency] private readonly IConfigurationManager _cfg = default ! ;
[Dependency] private readonly IGameTiming _gameTiming = default ! ;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default ! ;
[Dependency] private readonly IEyeManager _eyeManager = default ! ;
2023-01-07 21:24:52 -06:00
[Dependency] private readonly ContextMenuUIController _context = default ! ;
[Dependency] private readonly VerbMenuUIController _verb = default ! ;
2024-08-04 13:43:55 +00:00
[Dependency] private readonly ActionUIController _controller = default ! ; // WD EDIT
2021-10-28 18:21:19 +13:00
2023-01-07 21:24:52 -06:00
[UISystemDependency] private readonly VerbSystem _verbSystem = default ! ;
[UISystemDependency] private readonly ExamineSystem _examineSystem = default ! ;
[UISystemDependency] private readonly TransformSystem _xform = default ! ;
[UISystemDependency] private readonly CombatModeSystem _combatMode = default ! ;
2024-08-03 15:23:46 +00:00
[UISystemDependency] private readonly PopupSystem _popup = default ! ; // WD EDIT
2024-08-04 13:43:55 +00:00
[UISystemDependency] private readonly ActionsSystem _actions = default ! ; // WD EDIT
2023-01-07 21:24:52 -06:00
private bool _updating ;
2021-10-28 18:21:19 +13:00
/// <summary>
/// This maps the currently displayed entities to the actual GUI elements.
/// </summary>
/// <remarks>
/// This is used remove GUI elements when the entities are deleted. or leave the LOS.
/// </remarks>
2021-12-05 18:09:01 +01:00
public Dictionary < EntityUid , EntityMenuElement > Elements = new ( ) ;
2021-10-28 18:21:19 +13:00
2023-01-07 21:24:52 -06:00
public void OnStateEntered ( GameplayState state )
2021-10-28 18:21:19 +13:00
{
2023-01-07 21:24:52 -06:00
_updating = true ;
2021-10-28 18:21:19 +13:00
_cfg . OnValueChanged ( CCVars . EntityMenuGroupingType , OnGroupingChanged , true ) ;
2023-01-07 21:24:52 -06:00
_context . OnContextKeyEvent + = OnKeyBindDown ;
2021-10-28 18:21:19 +13:00
CommandBinds . Builder
2022-09-29 16:06:34 +10:00
. Bind ( EngineKeyFunctions . UseSecondary , new PointerInputCmdHandler ( HandleOpenEntityMenu , outsidePrediction : true ) )
2023-01-07 21:24:52 -06:00
. Register < EntityMenuUIController > ( ) ;
2021-10-28 18:21:19 +13:00
}
2023-01-07 21:24:52 -06:00
public void OnStateExited ( GameplayState state )
2021-10-28 18:21:19 +13:00
{
2023-01-07 21:24:52 -06:00
_updating = false ;
2021-10-28 18:21:19 +13:00
Elements . Clear ( ) ;
2023-01-07 21:24:52 -06:00
_cfg . UnsubValueChanged ( CCVars . EntityMenuGroupingType , OnGroupingChanged ) ;
_context . OnContextKeyEvent - = OnKeyBindDown ;
CommandBinds . Unregister < EntityMenuUIController > ( ) ;
2021-10-28 18:21:19 +13:00
}
/// <summary>
/// Given a list of entities, sort them into groups and them to a new entity menu.
/// </summary>
2021-12-05 18:09:01 +01:00
public void OpenRootMenu ( List < EntityUid > entities )
2021-10-28 18:21:19 +13:00
{
2021-12-03 14:37:52 +13:00
// close any old menus first.
2023-01-07 21:24:52 -06:00
if ( _context . RootMenu . Visible )
_context . Close ( ) ;
2021-12-03 14:37:52 +13:00
2021-10-28 18:21:19 +13:00
var entitySpriteStates = GroupEntities ( entities ) ;
var orderedStates = entitySpriteStates . ToList ( ) ;
2024-01-19 02:18:53 +04:00
orderedStates . Sort ( ( x , y ) = > string . Compare (
Identity . Name ( x . First ( ) , _entityManager ) ,
Identity . Name ( y . First ( ) , _entityManager ) ,
StringComparison . CurrentCulture ) ) ;
2021-10-28 18:21:19 +13:00
Elements . Clear ( ) ;
AddToUI ( orderedStates ) ;
2023-07-08 14:08:32 +10:00
var box = UIBox2 . FromDimensions ( _userInterfaceManager . MousePositionScaled . Position , new Vector2 ( 1 , 1 ) ) ;
2023-01-07 21:24:52 -06:00
_context . RootMenu . Open ( box ) ;
2021-10-28 18:21:19 +13:00
}
2023-01-07 21:24:52 -06:00
public void OnKeyBindDown ( ContextMenuElement element , GUIBoundKeyEventArgs args )
{
if ( element is not EntityMenuElement entityElement )
return ;
// get an entity associated with this element
var entity = entityElement . Entity ;
entity ? ? = GetFirstEntityOrNull ( element . SubMenu ) ;
// Deleted() automatically checks for null & existence.
if ( _entityManager . Deleted ( entity ) )
2021-10-28 18:21:19 +13:00
return ;
// do examination?
if ( args . Function = = ContentKeyFunctions . ExamineEntity )
{
2021-12-26 15:32:45 +13:00
_systemManager . GetEntitySystem < ExamineSystem > ( ) . DoExamine ( entity . Value ) ;
2021-10-28 18:21:19 +13:00
args . Handle ( ) ;
return ;
}
2024-08-03 15:23:46 +00:00
// WD START
2024-08-04 13:43:55 +00:00
if ( args . Function = = EngineKeyFunctions . Use & & ! CheckForUseBlocker ( entity . Value ) )
2024-08-03 15:23:46 +00:00
{
_context . Close ( ) ;
args . Handle ( ) ;
return ;
}
// WD END
2021-10-28 18:21:19 +13:00
// do some other server-side interaction?
2021-11-29 12:25:22 +13:00
if ( args . Function = = EngineKeyFunctions . Use | |
args . Function = = ContentKeyFunctions . ActivateItemInWorld | |
args . Function = = ContentKeyFunctions . AltActivateItemInWorld | |
args . Function = = ContentKeyFunctions . Point | |
args . Function = = ContentKeyFunctions . TryPullObject | |
args . Function = = ContentKeyFunctions . MovePulledObject )
2021-10-28 18:21:19 +13:00
{
var inputSys = _systemManager . GetEntitySystem < InputSystem > ( ) ;
var func = args . Function ;
var funcId = _inputManager . NetworkBindMap . KeyFunctionID ( func ) ;
2023-09-11 09:42:41 +10:00
var message = new ClientFullInputCmdMessage (
_gameTiming . CurTick ,
_gameTiming . TickFraction ,
funcId )
{
State = BoundKeyState . Down ,
Coordinates = _entityManager . GetComponent < TransformComponent > ( entity . Value ) . Coordinates ,
ScreenCoordinates = args . PointerLocation ,
Uid = entity . Value ,
} ;
2021-10-28 18:21:19 +13:00
2024-02-13 22:48:39 +01:00
var session = _playerManager . LocalSession ;
2021-10-28 18:21:19 +13:00
if ( session ! = null )
{
inputSys . HandleInputCommand ( session , func , message ) ;
}
2023-01-07 21:24:52 -06:00
_context . Close ( ) ;
2021-10-28 18:21:19 +13:00
args . Handle ( ) ;
}
}
2024-08-04 13:43:55 +00:00
// WD START
private bool CheckForUseBlocker ( EntityUid entity )
{
var localEntity = _playerManager . LocalEntity ;
if ( ! EntityManager . TryGetComponent ( localEntity , out HandsComponent ? hands ) )
return true ;
if ( ! EntityManager . HasComponent < MobStateComponent > ( entity ) | | entity = = localEntity . Value )
return true ;
if ( _controller . SelectingTargetFor is { } actionId & &
_actions . TryGetActionData ( actionId , out var baseAction ) & & baseAction is EntityTargetActionComponent )
{
InteractFailPopup ( entity , localEntity . Value ) ;
return false ;
}
var held = hands . ActiveHandEntity ;
if ( held ! = null )
{
if ( ! EntityManager . HasComponent < ContextMenuInteractionBlockerComponent > ( held . Value ) )
return true ;
}
else if ( ! EntityManager . HasComponent < StunProviderComponent > ( localEntity . Value ) )
return true ;
InteractFailPopup ( entity , localEntity . Value ) ;
return false ;
}
private void InteractFailPopup ( EntityUid entity , EntityUid localEntity )
{
_popup . PopupClient ( Loc . GetString ( "context-menu-cant-interact" ) , entity , localEntity ,
PopupType . MediumCaution ) ;
}
// WD END
2021-10-28 18:21:19 +13:00
private bool HandleOpenEntityMenu ( in PointerInputCmdHandler . PointerInputCmdArgs args )
{
if ( args . State ! = BoundKeyState . Down )
return false ;
2022-09-04 17:21:14 -07:00
if ( _stateManager . CurrentState is not GameplayStateBase )
2021-10-28 18:21:19 +13:00
return false ;
2022-09-29 15:51:59 +10:00
if ( _combatMode . IsInCombatMode ( args . Session ? . AttachedEntity ) )
return false ;
2024-03-20 21:59:56 -04:00
var coords = args . Coordinates . ToMap ( _entityManager , _xform ) ;
2021-10-28 18:21:19 +13:00
2021-11-22 09:40:09 +13:00
if ( _verbSystem . TryGetEntityMenuEntities ( coords , out var entities ) )
OpenRootMenu ( entities ) ;
2021-10-28 18:21:19 +13:00
return true ;
}
/// <summary>
/// Check that entities in the context menu are still visible. If not, remove them from the context menu.
/// </summary>
2023-01-07 21:24:52 -06:00
public override void FrameUpdate ( FrameEventArgs args )
2021-10-28 18:21:19 +13:00
{
2023-01-07 21:24:52 -06:00
if ( ! _updating | | _context . RootMenu = = null )
return ;
if ( ! _context . RootMenu . Visible )
2021-10-28 18:21:19 +13:00
return ;
2024-02-13 22:48:39 +01:00
if ( _playerManager . LocalEntity is not { } player | |
2021-12-05 18:09:01 +01:00
! player . IsValid ( ) )
2021-10-28 18:21:19 +13:00
return ;
// Do we need to do in-range unOccluded checks?
var ignoreFov = ! _eyeManager . CurrentEye . DrawFov | |
( _verbSystem . Visibility & MenuVisibility . NoFov ) = = MenuVisibility . NoFov ;
2022-10-15 17:15:25 +13:00
_entityManager . TryGetComponent ( player , out ExaminerComponent ? examiner ) ;
var xformQuery = _entityManager . GetEntityQuery < TransformComponent > ( ) ;
2021-10-28 18:21:19 +13:00
foreach ( var entity in Elements . Keys . ToList ( ) )
{
2022-10-15 17:15:25 +13:00
if ( ! xformQuery . TryGetComponent ( entity , out var xform ) )
{
// entity was deleted
RemoveEntity ( entity ) ;
continue ;
}
if ( ignoreFov )
continue ;
var pos = new MapCoordinates ( _xform . GetWorldPosition ( xform , xformQuery ) , xform . MapID ) ;
if ( ! _examineSystem . CanExamine ( player , pos , e = > e = = player | | e = = entity , entity , examiner ) )
2021-10-28 18:21:19 +13:00
RemoveEntity ( entity ) ;
}
}
/// <summary>
2021-12-03 14:20:34 +01:00
/// Add menu elements for a list of grouped entities;
2021-10-28 18:21:19 +13:00
/// </summary>
/// <param name="entityGroups"> A list of entity groups. Entities are grouped together based on prototype.</param>
2021-12-05 18:09:01 +01:00
private void AddToUI ( List < List < EntityUid > > entityGroups )
2021-10-28 18:21:19 +13:00
{
// If there is only a single group. We will just directly list individual entities
if ( entityGroups . Count = = 1 )
{
2023-02-21 15:08:42 +01:00
AddGroupToMenu ( entityGroups [ 0 ] , _context . RootMenu ) ;
2021-10-28 18:21:19 +13:00
return ;
}
foreach ( var group in entityGroups )
{
if ( group . Count > 1 )
{
AddGroupToUI ( group ) ;
}
2023-02-21 15:08:42 +01:00
else
{
// this group only has a single entity, add a simple menu element
AddEntityToMenu ( group [ 0 ] , _context . RootMenu ) ;
}
2021-10-28 18:21:19 +13:00
}
2021-12-03 14:20:34 +01:00
2021-10-28 18:21:19 +13:00
}
/// <summary>
/// Given a group of entities, add a menu element that has a pop-up sub-menu listing group members
/// </summary>
2021-12-05 18:09:01 +01:00
private void AddGroupToUI ( List < EntityUid > group )
2021-10-28 18:21:19 +13:00
{
EntityMenuElement element = new ( ) ;
2023-01-07 21:24:52 -06:00
ContextMenuPopup subMenu = new ( _context , element ) ;
2021-10-28 18:21:19 +13:00
2023-02-21 15:08:42 +01:00
AddGroupToMenu ( group , subMenu ) ;
UpdateElement ( element ) ;
_context . AddElement ( _context . RootMenu , element ) ;
}
/// <summary>
/// Add the group of entities to the menu
/// </summary>
private void AddGroupToMenu ( List < EntityUid > group , ContextMenuPopup menu )
{
2021-10-28 18:21:19 +13:00
foreach ( var entity in group )
{
2023-02-21 15:08:42 +01:00
AddEntityToMenu ( entity , menu ) ;
2021-10-28 18:21:19 +13:00
}
2023-02-21 15:08:42 +01:00
}
2021-10-28 18:21:19 +13:00
2023-02-21 15:08:42 +01:00
/// <summary>
/// Add the entity to the menu
/// </summary>
private void AddEntityToMenu ( EntityUid entity , ContextMenuPopup menu )
{
var element = new EntityMenuElement ( entity ) ;
element . SubMenu = new ContextMenuPopup ( _context , element ) ;
element . SubMenu . OnPopupOpen + = ( ) = > _verb . OpenVerbMenu ( entity , popup : element . SubMenu ) ;
element . SubMenu . OnPopupHide + = element . SubMenu . MenuBody . DisposeAllChildren ;
_context . AddElement ( menu , element ) ;
Elements . TryAdd ( entity , element ) ;
2021-10-28 18:21:19 +13:00
}
/// <summary>
/// Remove an entity from the entity context menu.
/// </summary>
2021-12-05 18:09:01 +01:00
private void RemoveEntity ( EntityUid entity )
2021-10-28 18:21:19 +13:00
{
// find the element associated with this entity
if ( ! Elements . TryGetValue ( entity , out var element ) )
{
2021-12-08 12:09:43 +01:00
Logger . Error ( $"Attempted to remove unknown entity from the entity menu: {_entityManager.GetComponent<MetaDataComponent>(entity).EntityName} ({entity})" ) ;
2021-10-28 18:21:19 +13:00
return ;
}
// remove the element
var parent = element . ParentMenu ? . ParentElement ;
element . Dispose ( ) ;
Elements . Remove ( entity ) ;
// update any parent elements
if ( parent is EntityMenuElement e )
UpdateElement ( e ) ;
// If this was the last entity, close the entity menu
2023-01-07 21:24:52 -06:00
if ( _context . RootMenu . MenuBody . ChildCount = = 0 )
_context . Close ( ) ;
2021-10-28 18:21:19 +13:00
}
/// <summary>
/// Update the information displayed by a menu element.
/// </summary>
/// <remarks>
/// This is called when initializing elements or after an element was removed from a sub-menu.
/// </remarks>
private void UpdateElement ( EntityMenuElement element )
{
if ( element . SubMenu = = null )
return ;
// Get the first entity in the sub-menus
var entity = GetFirstEntityOrNull ( element . SubMenu ) ;
2021-12-20 15:20:27 +01:00
if ( entity = = null )
2021-10-28 18:21:19 +13:00
{
// This whole element has no associated entities. We should remove it
element . Dispose ( ) ;
return ;
}
element . UpdateEntity ( entity ) ;
2023-03-13 19:51:36 +01:00
element . UpdateCount ( ) ;
2021-10-28 18:21:19 +13:00
if ( element . Count = = 1 )
{
// There was only one entity in the sub-menu. So we will just remove the sub-menu and point directly to
// that entity.
element . Entity = entity ;
element . SubMenu . Dispose ( ) ;
element . SubMenu = null ;
2021-12-26 15:32:45 +13:00
Elements [ entity . Value ] = element ;
2021-10-28 18:21:19 +13:00
}
// update the parent element, so that it's count and entity icon gets updated.
var parent = element . ParentMenu ? . ParentElement ;
if ( parent is EntityMenuElement e )
UpdateElement ( e ) ;
}
/// <summary>
2021-11-26 18:00:28 +13:00
/// Recursively look through a sub-menu and return the first entity.
2021-10-28 18:21:19 +13:00
/// </summary>
2021-12-26 15:32:45 +13:00
private EntityUid ? GetFirstEntityOrNull ( ContextMenuPopup ? menu )
2021-10-28 18:21:19 +13:00
{
if ( menu = = null )
2021-12-26 15:32:45 +13:00
return null ;
2021-10-28 18:21:19 +13:00
foreach ( var element in menu . MenuBody . Children )
{
if ( element is not EntityMenuElement entityElement )
continue ;
2021-12-26 15:32:45 +13:00
if ( entityElement . Entity ! = null )
2021-11-26 18:00:28 +13:00
{
2021-12-09 12:29:27 +01:00
if ( ! _entityManager . Deleted ( entityElement . Entity ) )
2021-11-26 18:00:28 +13:00
return entityElement . Entity ;
continue ;
}
2021-10-28 18:21:19 +13:00
2021-11-26 18:00:28 +13:00
// if the element has no entity, its a group of entities with another attached sub-menu.
2021-10-28 18:21:19 +13:00
var entity = GetFirstEntityOrNull ( entityElement . SubMenu ) ;
2021-12-26 15:32:45 +13:00
if ( entity ! = null )
2021-10-28 18:21:19 +13:00
return entity ;
}
2021-12-26 15:32:45 +13:00
return null ;
2021-10-28 18:21:19 +13:00
}
}
}