2021-10-28 18:21:19 +13:00
using System.Collections.Generic ;
using System.Linq ;
using Content.Client.Examine ;
using Content.Client.Verbs ;
using Content.Client.Viewport ;
using Content.Shared.CCVar ;
using Content.Shared.Input ;
using Robust.Client.GameObjects ;
using Robust.Client.Graphics ;
using Robust.Client.Input ;
using Robust.Client.Player ;
using Robust.Client.State ;
using Robust.Client.UserInterface ;
using Robust.Shared.Configuration ;
using Robust.Shared.GameObjects ;
using Robust.Shared.Input ;
using Robust.Shared.Input.Binding ;
using Robust.Shared.IoC ;
using Robust.Shared.Log ;
using Robust.Shared.Maths ;
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>
/// In addition to the normal <see cref="ContextMenuPresenter"/> functionality, this also provides functions get
/// 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>
public sealed partial class EntityMenuPresenter : ContextMenuPresenter
{
[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 ! ;
private readonly VerbSystem _verbSystem ;
2021-11-22 09:40:09 +13:00
private readonly ExamineSystem _examineSystem ;
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
public EntityMenuPresenter ( VerbSystem verbSystem ) : base ( )
{
IoCManager . InjectDependencies ( this ) ;
_verbSystem = verbSystem ;
2021-11-22 09:40:09 +13:00
_examineSystem = EntitySystem . Get < ExamineSystem > ( ) ;
2021-10-28 18:21:19 +13:00
_cfg . OnValueChanged ( CCVars . EntityMenuGroupingType , OnGroupingChanged , true ) ;
CommandBinds . Builder
. Bind ( ContentKeyFunctions . OpenContextMenu , new PointerInputCmdHandler ( HandleOpenEntityMenu ) )
. Register < EntityMenuPresenter > ( ) ;
}
public override void Dispose ( )
{
base . Dispose ( ) ;
Elements . Clear ( ) ;
CommandBinds . Unregister < EntityMenuPresenter > ( ) ;
}
/// <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.
if ( RootMenu . Visible )
Close ( ) ;
2021-10-28 18:21:19 +13:00
var entitySpriteStates = GroupEntities ( entities ) ;
var orderedStates = entitySpriteStates . ToList ( ) ;
2021-12-03 15:53:09 +01:00
orderedStates . Sort ( ( x , y ) = > string . CompareOrdinal ( IoCManager . Resolve < IEntityManager > ( ) . GetComponent < MetaDataComponent > ( x . First ( ) ) . EntityPrototype ? . Name , IoCManager . Resolve < IEntityManager > ( ) . GetComponent < MetaDataComponent > ( y . First ( ) ) . EntityPrototype ? . Name ) ) ;
2021-10-28 18:21:19 +13:00
Elements . Clear ( ) ;
AddToUI ( orderedStates ) ;
var box = UIBox2 . FromDimensions ( _userInterfaceManager . MousePositionScaled . Position , ( 1 , 1 ) ) ;
RootMenu . Open ( box ) ;
}
public override void OnKeyBindDown ( ContextMenuElement element , GUIBoundKeyEventArgs args )
{
base . OnKeyBindDown ( element , args ) ;
if ( element is not EntityMenuElement entityElement )
return ;
// get an entity associated with this element
var entity = entityElement . Entity ;
2021-12-05 18:09:01 +01:00
if ( ! entity . Valid )
{
entity = GetFirstEntityOrNull ( element . SubMenu ) ;
}
if ( ! entity . Valid )
2021-10-28 18:21:19 +13:00
return ;
// open verb menu?
if ( args . Function = = ContentKeyFunctions . OpenContextMenu )
{
_verbSystem . VerbMenu . OpenVerbMenu ( entity ) ;
args . Handle ( ) ;
return ;
}
// do examination?
if ( args . Function = = ContentKeyFunctions . ExamineEntity )
{
_systemManager . GetEntitySystem < ExamineSystem > ( ) . DoExamine ( entity ) ;
args . Handle ( ) ;
return ;
}
// 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 ) ;
var message = new FullInputCmdMessage ( _gameTiming . CurTick , _gameTiming . TickFraction , funcId ,
2021-12-03 15:53:09 +01:00
BoundKeyState . Down , IoCManager . Resolve < IEntityManager > ( ) . GetComponent < TransformComponent > ( entity ) . Coordinates , args . PointerLocation , entity ) ;
2021-10-28 18:21:19 +13:00
var session = _playerManager . LocalPlayer ? . Session ;
if ( session ! = null )
{
inputSys . HandleInputCommand ( session , func , message ) ;
}
_verbSystem . CloseAllMenus ( ) ;
args . Handle ( ) ;
return ;
}
}
private bool HandleOpenEntityMenu ( in PointerInputCmdHandler . PointerInputCmdArgs args )
{
if ( args . State ! = BoundKeyState . Down )
return false ;
if ( _stateManager . CurrentState is not GameScreenBase )
return false ;
var coords = args . Coordinates . ToMap ( _entityManager ) ;
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>
public void Update ( )
{
if ( ! RootMenu . Visible )
return ;
2021-12-05 18:09:01 +01:00
if ( _playerManager . LocalPlayer ? . ControlledEntity is not { } player | |
! 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 ;
foreach ( var entity in Elements . Keys . ToList ( ) )
{
2021-12-03 15:53:09 +01:00
if ( ( ! IoCManager . Resolve < IEntityManager > ( ) . EntityExists ( entity ) ? EntityLifeStage . Deleted : IoCManager . Resolve < IEntityManager > ( ) . GetComponent < MetaDataComponent > ( entity ) . EntityLifeStage ) > = EntityLifeStage . Deleted | | ! ignoreFov & & ! _examineSystem . CanExamine ( player , entity ) )
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 )
{
foreach ( var entity in entityGroups [ 0 ] )
{
var element = new EntityMenuElement ( entity ) ;
AddElement ( RootMenu , element ) ;
Elements . TryAdd ( entity , element ) ;
}
return ;
}
foreach ( var group in entityGroups )
{
if ( group . Count > 1 )
{
AddGroupToUI ( group ) ;
continue ;
}
// this group only has a single entity, add a simple menu element
var element = new EntityMenuElement ( group [ 0 ] ) ;
AddElement ( RootMenu , element ) ;
Elements . TryAdd ( group [ 0 ] , element ) ;
}
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 ( ) ;
ContextMenuPopup subMenu = new ( this , element ) ;
foreach ( var entity in group )
{
var subElement = new EntityMenuElement ( entity ) ;
AddElement ( subMenu , subElement ) ;
Elements . TryAdd ( entity , subElement ) ;
}
UpdateElement ( element ) ;
AddElement ( RootMenu , element ) ;
}
/// <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-03 15:53:09 +01:00
Logger . Error ( $"Attempted to remove unknown entity from the entity menu: {IoCManager.Resolve<IEntityManager>().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 the verb menu is open and targeting this entity, close it.
2021-12-03 15:53:09 +01:00
if ( _verbSystem . VerbMenu . CurrentTarget = = entity )
2021-10-28 18:21:19 +13:00
_verbSystem . VerbMenu . Close ( ) ;
// If this was the last entity, close the entity menu
if ( RootMenu . MenuBody . ChildCount = = 0 )
Close ( ) ;
}
/// <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 ) ;
if ( entity = = null )
{
// This whole element has no associated entities. We should remove it
element . Dispose ( ) ;
return ;
}
element . UpdateEntity ( entity ) ;
// Update the entity count & count label
element . Count = 0 ;
foreach ( var subElement in element . SubMenu . MenuBody . Children )
{
if ( subElement is EntityMenuElement entityElement )
element . Count + = entityElement . Count ;
}
element . CountLabel . Text = element . Count . ToString ( ) ;
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 ;
element . CountLabel . Visible = false ;
Elements [ entity ] = element ;
}
// 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-05 18:09:01 +01:00
private EntityUid GetFirstEntityOrNull ( ContextMenuPopup ? menu )
2021-10-28 18:21:19 +13:00
{
if ( menu = = null )
2021-12-05 18:09:01 +01:00
return default ;
2021-10-28 18:21:19 +13:00
foreach ( var element in menu . MenuBody . Children )
{
if ( element is not EntityMenuElement entityElement )
continue ;
2021-12-05 18:09:01 +01:00
if ( entityElement . Entity ! = default )
2021-11-26 18:00:28 +13:00
{
2021-12-03 15:53:09 +01:00
if ( ! ( ( ! IoCManager . Resolve < IEntityManager > ( ) . EntityExists ( entityElement . Entity ) ? EntityLifeStage . Deleted : IoCManager . Resolve < IEntityManager > ( ) . GetComponent < MetaDataComponent > ( entityElement . Entity ) . EntityLifeStage ) > = EntityLifeStage . Deleted ) )
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-05 18:09:01 +01:00
if ( entity ! = default )
2021-10-28 18:21:19 +13:00
return entity ;
}
2021-12-05 18:09:01 +01:00
return default ;
2021-10-28 18:21:19 +13:00
}
public override void OpenSubMenu ( ContextMenuElement element )
{
base . OpenSubMenu ( element ) ;
// In case the verb menu is currently open, ensure that it is shown ABOVE the entity menu.
if ( _verbSystem . VerbMenu . Menus . TryPeek ( out var menu ) & & menu . Visible )
{
menu . ParentElement ? . ParentMenu ? . SetPositionLast ( ) ;
menu . SetPositionLast ( ) ;
}
}
}
}