2025-04-19 10:40:49 -04:00
using System.Linq ;
2023-12-04 18:04:39 -05:00
using System.Numerics ;
using Content.Client.Examine ;
using Content.Client.Hands.Systems ;
using Content.Client.Interaction ;
2025-02-08 17:17:55 +11:00
using Content.Client.Storage ;
2023-12-04 18:04:39 -05:00
using Content.Client.Storage.Systems ;
using Content.Client.UserInterface.Systems.Hotbar.Widgets ;
2025-02-25 11:40:39 -05:00
using Content.Client.UserInterface.Systems.Info ;
2023-12-04 18:04:39 -05:00
using Content.Client.UserInterface.Systems.Storage.Controls ;
using Content.Client.Verbs.UI ;
using Content.Shared.CCVar ;
using Content.Shared.Input ;
using Content.Shared.Interaction ;
using Content.Shared.Storage ;
2025-02-17 19:24:34 +11:00
using Robust.Client.GameObjects ;
2023-12-04 18:04:39 -05:00
using Robust.Client.Input ;
2025-02-08 17:17:55 +11:00
using Robust.Client.Player ;
2023-12-04 18:04:39 -05:00
using Robust.Client.UserInterface ;
using Robust.Client.UserInterface.Controllers ;
using Robust.Client.UserInterface.Controls ;
using Robust.Shared.Configuration ;
using Robust.Shared.Input ;
using Robust.Shared.Timing ;
namespace Content.Client.UserInterface.Systems.Storage ;
public sealed class StorageUIController : UIController , IOnSystemChanged < StorageSystem >
{
2025-02-08 17:17:55 +11:00
/ *
* Things are a bit over the shop but essentially
* - Clicking into storagewindow is handled via storagewindow
* - Clicking out of it is via ItemGridPiece
* - Dragging around is handled here
* - Drawing is handled via ItemGridPiece
* - StorageSystem handles any sim stuff around open windows .
* /
2023-12-04 18:04:39 -05:00
[Dependency] private readonly IConfigurationManager _configuration = default ! ;
[Dependency] private readonly IInputManager _input = default ! ;
2025-02-08 17:17:55 +11:00
[Dependency] private readonly IPlayerManager _player = default ! ;
2025-02-25 11:40:39 -05:00
[Dependency] private readonly CloseRecentWindowUIController _closeRecentWindowUIController = default ! ;
2025-02-08 17:17:55 +11:00
[UISystemDependency] private readonly StorageSystem _storage = default ! ;
2025-02-17 19:24:34 +11:00
[UISystemDependency] private readonly UserInterfaceSystem _ui = default ! ;
2023-12-04 18:04:39 -05:00
2025-01-27 21:29:51 +11:00
private readonly DragDropHelper < ItemGridPiece > _menuDragHelper ;
2025-02-05 14:13:06 +01:00
2025-02-08 17:17:55 +11:00
public ItemGridPiece ? DraggingGhost = > _menuDragHelper . Dragged ;
2023-12-04 18:04:39 -05:00
public Angle DraggingRotation = Angle . Zero ;
public bool StaticStorageUIEnabled ;
2023-12-05 18:38:10 -05:00
public bool OpaqueStorageWindow ;
2025-04-19 10:40:49 -04:00
private int _openStorageLimit = - 1 ;
2023-12-04 18:04:39 -05:00
public bool IsDragging = > _menuDragHelper . IsDragging ;
public ItemGridPiece ? CurrentlyDragging = > _menuDragHelper . Dragged ;
2025-02-08 17:17:55 +11:00
public bool WindowTitle { get ; private set ; } = false ;
2023-12-04 18:04:39 -05:00
public StorageUIController ( )
{
_menuDragHelper = new DragDropHelper < ItemGridPiece > ( OnMenuBeginDrag , OnMenuContinueDrag , OnMenuEndDrag ) ;
}
public override void Initialize ( )
{
base . Initialize ( ) ;
_configuration . OnValueChanged ( CCVars . StaticStorageUI , OnStaticStorageChanged , true ) ;
2023-12-05 18:38:10 -05:00
_configuration . OnValueChanged ( CCVars . OpaqueStorageWindow , OnOpaqueWindowChanged , true ) ;
2025-02-08 17:17:55 +11:00
_configuration . OnValueChanged ( CCVars . StorageWindowTitle , OnStorageWindowTitle , true ) ;
2025-04-19 10:40:49 -04:00
_configuration . OnValueChanged ( CCVars . StorageLimit , OnStorageLimitChanged , true ) ;
}
private void OnStorageLimitChanged ( int obj )
{
_openStorageLimit = obj ;
2023-12-04 18:04:39 -05:00
}
2025-02-08 17:17:55 +11:00
private void OnStorageWindowTitle ( bool obj )
{
WindowTitle = obj ;
}
2023-12-04 18:04:39 -05:00
2025-02-08 17:17:55 +11:00
private void OnOpaqueWindowChanged ( bool obj )
{
OpaqueStorageWindow = obj ;
2023-12-04 18:04:39 -05:00
}
private void OnStaticStorageChanged ( bool obj )
{
StaticStorageUIEnabled = obj ;
2025-02-08 17:17:55 +11:00
}
2023-12-04 18:04:39 -05:00
2025-02-17 19:24:34 +11:00
public StorageWindow CreateStorageWindow ( StorageBoundUserInterface sBui )
2025-02-08 17:17:55 +11:00
{
var window = new StorageWindow ( ) ;
window . MouseFilter = Control . MouseFilterMode . Pass ;
2023-12-04 18:04:39 -05:00
2025-02-08 17:17:55 +11:00
window . OnPiecePressed + = ( args , piece ) = >
{
OnPiecePressed ( args , window , piece ) ;
} ;
window . OnPieceUnpressed + = ( args , piece ) = >
{
OnPieceUnpressed ( args , window , piece ) ;
} ;
2023-12-04 18:04:39 -05:00
if ( StaticStorageUIEnabled )
{
2025-04-19 10:40:49 -04:00
var hotbar = UIManager . GetActiveUIWidgetOrNull < HotbarGui > ( ) ;
// this lambda handles the nested storage case
// during nested storage, a parent window hides and a child window is
// immediately inserted to the end of the list
// we can reorder the newly inserted to the same index as the invisible
// window in order to prevent an invisible window from being replaced
// with a visible one in a different position
Action < Control ? , Control > reorder = ( parent , child ) = >
{
if ( parent is null )
return ;
var parentChildren = parent . Children . ToList ( ) ;
var invisibleIndex = parentChildren . FindIndex ( c = > c . Visible = = false ) ;
if ( invisibleIndex = = - 1 )
return ;
child . SetPositionInParent ( invisibleIndex ) ;
} ;
if ( _openStorageLimit = = 2 )
{
if ( hotbar ? . LeftStorageContainer . Children . Any ( c = > c . Visible ) = = false ) // we're comparing booleans because it's bool? and not bool from the optional chaining
{
hotbar ? . LeftStorageContainer . AddChild ( window ) ;
reorder ( hotbar ? . LeftStorageContainer , window ) ;
}
else
{
hotbar ? . RightStorageContainer . AddChild ( window ) ;
reorder ( hotbar ? . RightStorageContainer , window ) ;
}
}
else
{
hotbar ? . SingleStorageContainer . AddChild ( window ) ;
reorder ( hotbar ? . SingleStorageContainer , window ) ;
}
2025-02-25 11:40:39 -05:00
_closeRecentWindowUIController . SetMostRecentlyInteractedWindow ( window ) ;
2023-12-04 18:04:39 -05:00
}
else
{
2025-02-17 19:24:34 +11:00
// Open at parent position if it's open.
if ( _ui . TryGetOpenUi < StorageBoundUserInterface > ( EntityManager . GetComponent < TransformComponent > ( sBui . Owner ) . ParentUid ,
StorageComponent . StorageUiKey . Key , out var bui ) & & bui . Position ! = null )
{
window . Open ( bui . Position . Value ) ;
}
// Open at the saved position if it exists.
else if ( _ui . TryGetPosition ( sBui . Owner , StorageComponent . StorageUiKey . Key , out var pos ) )
{
window . Open ( pos ) ;
}
// Open at the default position.
else
{
window . OpenCenteredLeft ( ) ;
}
2023-12-04 18:04:39 -05:00
}
2023-12-05 18:38:10 -05:00
2025-02-17 19:24:34 +11:00
_ui . RegisterControl ( sBui , window ) ;
2025-02-08 17:17:55 +11:00
return window ;
2025-01-27 21:29:51 +11:00
}
2025-02-08 17:17:55 +11:00
public void OnSystemLoaded ( StorageSystem system )
2025-01-27 21:29:51 +11:00
{
2025-02-08 17:17:55 +11:00
_input . FirstChanceOnKeyEvent + = OnMiddleMouse ;
}
public void OnSystemUnloaded ( StorageSystem system )
{
_input . FirstChanceOnKeyEvent - = OnMiddleMouse ;
2023-12-04 18:04:39 -05:00
}
/// One might ask, Hey Emo, why are you parsing raw keyboard input just to rotate a rectangle?
/// The answer is, that input bindings regarding mouse inputs are always intercepted by the UI,
/// thus, if i want to be able to rotate my damn piece anywhere on the screen,
/// I have to side-step all of the input handling. Cheers.
private void OnMiddleMouse ( KeyEventArgs keyEvent , KeyEventType type )
{
if ( keyEvent . Handled )
return ;
if ( type ! = KeyEventType . Down )
return ;
//todo there's gotta be a method for this in InputManager just expose it to content I BEG.
if ( ! _input . TryGetKeyBinding ( ContentKeyFunctions . RotateStoredItem , out var binding ) )
return ;
if ( binding . BaseKey ! = keyEvent . Key )
return ;
if ( keyEvent . Shift & &
! ( binding . Mod1 = = Keyboard . Key . Shift | |
binding . Mod2 = = Keyboard . Key . Shift | |
binding . Mod3 = = Keyboard . Key . Shift ) )
return ;
if ( keyEvent . Alt & &
! ( binding . Mod1 = = Keyboard . Key . Alt | |
binding . Mod2 = = Keyboard . Key . Alt | |
binding . Mod3 = = Keyboard . Key . Alt ) )
return ;
if ( keyEvent . Control & &
! ( binding . Mod1 = = Keyboard . Key . Control | |
binding . Mod2 = = Keyboard . Key . Control | |
binding . Mod3 = = Keyboard . Key . Control ) )
return ;
2025-02-08 17:17:55 +11:00
if ( ! IsDragging & & EntityManager . System < HandsSystem > ( ) . GetActiveHandEntity ( ) = = null )
2023-12-04 18:04:39 -05:00
return ;
//clamp it to a cardinal.
DraggingRotation = ( DraggingRotation + Math . PI / 2f ) . GetCardinalDir ( ) . ToAngle ( ) ;
if ( DraggingGhost ! = null )
DraggingGhost . Location . Rotation = DraggingRotation ;
2025-02-08 17:17:55 +11:00
if ( IsDragging | | UIManager . CurrentlyHovered is StorageWindow )
2023-12-04 18:04:39 -05:00
keyEvent . Handle ( ) ;
}
2025-02-08 17:17:55 +11:00
private void OnPiecePressed ( GUIBoundKeyEventArgs args , StorageWindow window , ItemGridPiece control )
2025-02-05 14:13:06 +01:00
{
2025-02-08 17:17:55 +11:00
if ( IsDragging | | ! window . IsOpen )
2023-12-04 18:04:39 -05:00
return ;
if ( args . Function = = ContentKeyFunctions . MoveStoredItem )
{
2024-02-25 18:24:21 -05:00
DraggingRotation = control . Location . Rotation ;
2023-12-04 18:04:39 -05:00
_menuDragHelper . MouseDown ( control ) ;
_menuDragHelper . Update ( 0f ) ;
args . Handle ( ) ;
}
2024-03-28 06:31:47 +00:00
else if ( args . Function = = ContentKeyFunctions . SaveItemLocation )
{
2025-02-08 17:17:55 +11:00
if ( window . StorageEntity is not { } storage )
2024-03-28 06:31:47 +00:00
return ;
2025-02-08 17:17:55 +11:00
EntityManager . RaisePredictiveEvent ( new StorageSaveItemLocationEvent (
EntityManager . GetNetEntity ( control . Entity ) ,
EntityManager . GetNetEntity ( storage ) ) ) ;
2024-03-28 06:31:47 +00:00
args . Handle ( ) ;
}
2023-12-04 18:04:39 -05:00
else if ( args . Function = = ContentKeyFunctions . ExamineEntity )
{
2025-02-08 17:17:55 +11:00
EntityManager . System < ExamineSystem > ( ) . DoExamine ( control . Entity ) ;
2023-12-04 18:04:39 -05:00
args . Handle ( ) ;
}
else if ( args . Function = = EngineKeyFunctions . UseSecondary )
{
UIManager . GetUIController < VerbMenuUIController > ( ) . OpenVerbMenu ( control . Entity ) ;
args . Handle ( ) ;
}
else if ( args . Function = = ContentKeyFunctions . ActivateItemInWorld )
{
2025-02-08 17:17:55 +11:00
EntityManager . RaisePredictiveEvent (
new InteractInventorySlotEvent ( EntityManager . GetNetEntity ( control . Entity ) , altInteract : false ) ) ;
2023-12-04 18:04:39 -05:00
args . Handle ( ) ;
}
else if ( args . Function = = ContentKeyFunctions . AltActivateItemInWorld )
{
2025-02-08 17:17:55 +11:00
EntityManager . RaisePredictiveEvent ( new InteractInventorySlotEvent ( EntityManager . GetNetEntity ( control . Entity ) , altInteract : true ) ) ;
2023-12-04 18:04:39 -05:00
args . Handle ( ) ;
}
2025-02-08 17:17:55 +11:00
window . FlagDirty ( ) ;
2023-12-04 18:04:39 -05:00
}
2025-02-08 17:17:55 +11:00
private void OnPieceUnpressed ( GUIBoundKeyEventArgs args , StorageWindow window , ItemGridPiece control )
2023-12-04 18:04:39 -05:00
{
2023-12-12 02:49:37 -05:00
if ( args . Function ! = ContentKeyFunctions . MoveStoredItem )
2023-12-04 18:04:39 -05:00
return ;
2025-02-08 17:17:55 +11:00
// Want to get the control under the dragged control.
// This means we can drag the original control around (and not hide the original).
control . MouseFilter = Control . MouseFilterMode . Ignore ;
var targetControl = UIManager . MouseGetControl ( args . PointerLocation ) ;
var targetStorage = targetControl as StorageWindow ;
control . MouseFilter = Control . MouseFilterMode . Pass ;
var localPlayer = _player . LocalEntity ;
window . RemoveGrid ( control ) ;
window . FlagDirty ( ) ;
// If we tried to drag it on top of another grid piece then cancel out.
if ( targetControl is ItemGridPiece | | window . StorageEntity is not { } sourceStorage | | localPlayer = = null )
{
window . Reclaim ( control . Location , control ) ;
args . Handle ( ) ;
_menuDragHelper . EndDrag ( ) ;
2023-12-12 02:49:37 -05:00
return ;
2025-02-08 17:17:55 +11:00
}
2023-12-12 02:49:37 -05:00
2025-02-08 17:17:55 +11:00
if ( _menuDragHelper . IsDragging & & DraggingGhost is { } draggingGhost )
2023-12-04 18:04:39 -05:00
{
2023-12-12 02:49:37 -05:00
var dragEnt = draggingGhost . Entity ;
var dragLoc = draggingGhost . Location ;
2025-02-08 17:17:55 +11:00
// Dragging in the same storage
// The existing ItemGridPiece just stops rendering but still exists so check if it's hovered.
if ( targetStorage = = window )
2023-12-04 18:04:39 -05:00
{
2025-02-08 17:17:55 +11:00
var position = targetStorage . GetMouseGridPieceLocation ( dragEnt , dragLoc ) ;
var newLocation = new ItemStorageLocation ( DraggingRotation , position ) ;
2025-04-18 12:11:31 +10:00
if ( ! _storage . ItemFitsInGridLocation ( dragEnt , sourceStorage , newLocation ) )
{
window . Reclaim ( control . Location , control ) ;
}
else
{
EntityManager . RaisePredictiveEvent ( new StorageSetItemLocationEvent (
EntityManager . GetNetEntity ( draggingGhost . Entity ) ,
EntityManager . GetNetEntity ( sourceStorage ) ,
newLocation ) ) ;
2025-02-08 17:17:55 +11:00
2025-04-18 12:11:31 +10:00
window . Reclaim ( newLocation , control ) ;
}
2025-02-08 17:17:55 +11:00
}
// Dragging to new storage
else if ( targetStorage ? . StorageEntity ! = null & & targetStorage ! = window )
{
var position = targetStorage . GetMouseGridPieceLocation ( dragEnt , dragLoc ) ;
var newLocation = new ItemStorageLocation ( DraggingRotation , position ) ;
// Check it fits and we can move to hand (no free transfers).
if ( _storage . ItemFitsInGridLocation (
( dragEnt , null ) ,
( targetStorage . StorageEntity . Value , null ) ,
newLocation ) )
{
// Can drop and move.
EntityManager . RaisePredictiveEvent ( new StorageTransferItemEvent (
EntityManager . GetNetEntity ( dragEnt ) ,
EntityManager . GetNetEntity ( targetStorage . StorageEntity . Value ) ,
newLocation ) ) ;
targetStorage . Reclaim ( newLocation , control ) ;
DraggingRotation = Angle . Zero ;
}
else
{
// Cancel it (rather than dropping).
window . Reclaim ( dragLoc , control ) ;
}
2023-12-04 18:04:39 -05:00
}
2023-12-12 02:49:37 -05:00
2025-02-08 17:17:55 +11:00
targetStorage ? . FlagDirty ( ) ;
2023-12-12 02:49:37 -05:00
}
2025-02-08 17:17:55 +11:00
// If we just clicked, then take it out of the bag.
else
2023-12-12 02:49:37 -05:00
{
2025-02-08 17:17:55 +11:00
EntityManager . RaisePredictiveEvent ( new StorageInteractWithItemEvent (
EntityManager . GetNetEntity ( control . Entity ) ,
EntityManager . GetNetEntity ( sourceStorage ) ) ) ;
2023-12-04 18:04:39 -05:00
}
2025-02-08 17:17:55 +11:00
_menuDragHelper . EndDrag ( ) ;
2023-12-12 02:49:37 -05:00
args . Handle ( ) ;
2023-12-04 18:04:39 -05:00
}
private bool OnMenuBeginDrag ( )
{
if ( _menuDragHelper . Dragged is not { } dragged )
return false ;
2025-02-08 17:17:55 +11:00
DraggingGhost ! . Orphan ( ) ;
2023-12-04 18:04:39 -05:00
DraggingRotation = dragged . Location . Rotation ;
UIManager . PopupRoot . AddChild ( DraggingGhost ) ;
SetDraggingRotation ( ) ;
return true ;
}
private bool OnMenuContinueDrag ( float frameTime )
{
if ( DraggingGhost = = null )
return false ;
2025-02-08 17:17:55 +11:00
2025-04-18 12:11:31 +10:00
var player = _player . LocalEntity ;
// If the attached storage is closed then stop dragging
if ( player = = null | |
! _storage . TryGetStorageLocation ( DraggingGhost . Entity , out var container , out _ , out _ ) | |
! _ui . IsUiOpen ( container . Owner , StorageComponent . StorageUiKey . Key , player . Value ) )
{
DraggingGhost . Orphan ( ) ;
return false ;
}
2023-12-04 18:04:39 -05:00
SetDraggingRotation ( ) ;
return true ;
}
private void SetDraggingRotation ( )
{
if ( DraggingGhost = = null )
return ;
var offset = ItemGridPiece . GetCenterOffset (
( DraggingGhost . Entity , null ) ,
new ItemStorageLocation ( DraggingRotation , Vector2i . Zero ) ,
2025-02-08 17:17:55 +11:00
EntityManager ) ;
2023-12-04 18:04:39 -05:00
// I don't know why it divides the position by 2. Hope this helps! -emo
LayoutContainer . SetPosition ( DraggingGhost , UIManager . MousePositionScaled . Position / 2 - offset ) ;
}
private void OnMenuEndDrag ( )
{
if ( DraggingGhost = = null )
return ;
2025-02-08 17:17:55 +11:00
2023-12-04 18:04:39 -05:00
DraggingRotation = Angle . Zero ;
}
public override void FrameUpdate ( FrameEventArgs args )
{
base . FrameUpdate ( args ) ;
_menuDragHelper . Update ( args . DeltaSeconds ) ;
}
}