using NativeEngine; using Sandbox.Engine; namespace Sandbox; public static partial class Input { /// /// Was the last button pressed a game controller button? /// public static bool UsingController { get; internal set; } = false; static ulong Actions { get => CurrentContext.ActionsCurrent; set => CurrentContext.ActionsCurrent = value; } static ulong LastActions { get => CurrentContext.ActionsPrevious; set => CurrentContext.ActionsPrevious = value; } /// /// Missing action names that we've warned about already. /// This gets cleared when actions are initialized again. /// private static HashSet MissingActions { get; } = new( StringComparer.OrdinalIgnoreCase ); /// /// We pack actions bit-by-bit in CUserCmd, using the index (which is shared between realms) to map it to an action. /// This is an accessor to grab that index easily from its action. /// /// /// internal static int GetActionIndex( string actionName ) { return InputActions?.FindIndex( x => string.Equals( x.Name, actionName, StringComparison.OrdinalIgnoreCase ) ) ?? -1; } /// internal static int GetActionIndex( InputAction action ) { return InputActions?.IndexOf( action ) ?? -1; } /// /// True if escape was pressed /// public static bool EscapePressed { get => InputRouter.EscapeWasPressed; set => InputRouter.EscapeWasPressed = value; } /// /// Action is currently pressed down /// [ActionGraphNode( "input.down" ), Pure, Category( "Input" ), Icon( "gamepad" )] public static bool Down( [InputAction] string action, bool complainOnMissing = true ) { if ( Suppressed ) return false; if ( string.IsNullOrWhiteSpace( action ) ) return false; var index = GetActionIndex( action ); if ( index == -1 ) { if ( complainOnMissing && MissingActions.Add( action ) ) Log.Warning( $"Couldn't find Input Action called \"{action}\"" ); return false; } return (Actions & 1UL << index) != 0; } internal static bool WasDownLastCommand( string action ) { if ( Suppressed ) return false; if ( string.IsNullOrWhiteSpace( action ) ) return false; var index = GetActionIndex( action ); if ( index == -1 && MissingActions.Add( action ) ) { Log.Warning( $"Couldn't find Input Action called \"{action}\"" ); return false; } return (LastActions & (1UL << index)) != 0; } /// /// Action wasn't pressed but now it is /// [ActionGraphNode( "input.pressed" ), Pure, Category( "Input" ), Icon( "gamepad" )] public static bool Pressed( [InputAction] string action ) { if ( Suppressed ) return false; return !WasDownLastCommand( action ) && Down( action ); } /// /// Action was pressed but now it isn't /// [ActionGraphNode( "input.released" ), Pure, Category( "Input" ), Icon( "gamepad" )] public static bool Released( [InputAction] string action ) { if ( Suppressed ) return false; return WasDownLastCommand( action ) && !Down( action ); } /// internal static void SetAction( InputAction action, bool down ) => SetAction( GetActionIndex( action ), down ); /// public static void SetAction( string action, bool down ) => SetAction( GetActionIndex( action ), down ); /// /// Remove this action, so it's no longer being pressed. /// /// public static void Clear( string action ) => SetAction( action, false ); /// /// Clears the current input actions, so that none of them are active. /// public static void ClearActions() { Actions = default; LastActions = default; } /// /// Clears the current input actions, so that none of them are active. Unlike ClearActions /// this will unpress the buttons, so they won't be active again until they're pressed again. /// public static void ReleaseActions() { foreach ( var e in Contexts ) { e.ActionsCurrent = default; e.ActionsPrevious = default; e.AccumActionsPressed = default; } } /// /// Releases the action, and it won't be active again until it's pressed again. /// public static void ReleaseAction( string name ) { var index = GetActionIndex( name ); foreach ( var e in Contexts ) { e.ActionsCurrent &= ~(1UL << index); e.ActionsPrevious &= ~(1UL << index); e.AccumActionsPressed &= ~(1UL << index); } } /// /// Activates / Deactivates an action when building input. /// /// /// internal static void SetAction( int index, bool down ) { if ( down ) Actions |= 1UL << index; else Actions &= ~(1UL << index); } static InputAction FindInputActionByName( string action ) { return InputActions?.FirstOrDefault( x => string.Equals( x.Name, action, StringComparison.OrdinalIgnoreCase ) ); } static HashSet activeButtons = new HashSet(); /// /// Called when a compatible button is pressed. /// internal static void OnButton( ButtonCode code, string button, bool down ) { if ( InputActions == null ) { activeButtons.Clear(); return; } string loadedGame = Application.GameIdent; if ( string.IsNullOrEmpty( loadedGame ) ) loadedGame = "common"; var collection = InputBinds.FindCollection( loadedGame ); if ( down ) { activeButtons.Add( button ); foreach ( var e in Contexts ) { e.AccumKeysPressed.Add( code ); } } else { foreach ( var e in Contexts ) { e.AccumKeysReleased.Add( code ); } // remove it but if it wasn't even active, ignore it if ( !activeButtons.Remove( button ) ) return; } UsingController = false; bool handled = false; // // Find any actions that contain this button // foreach ( var bind in collection.EnumerateWithButton( button ) ) { var action = FindInputActionByName( bind.Name ); if ( action == null ) continue; // For the action to be active we need a bind with this button and all the other buttons in the bind if ( down && !bind.Test( button, activeButtons ) ) continue; // One of the binds for this action passed - so don't do anything if ( !down && bind.Test( button, activeButtons ) ) continue; var i = GetActionIndex( action ); if ( down ) { foreach ( var e in Contexts ) { e.AccumActionsPressed |= 1UL << i; } } else { foreach ( var e in Contexts ) { e.AccumActionsReleased |= 1UL << i; } } handled = true; } if ( !handled ) { OnUnhandledButton( code, button, down ); } } static void OnUnhandledButton( ButtonCode code, string button, bool down ) { if ( !down ) return; var binding = g_pInputService.GetBinding( code ); if ( string.IsNullOrEmpty( binding ) ) return; ConVarSystem.Run( $"{binding}\n" ); } internal static InputSettings InputSettings { get; set; } internal static List InputActions => InputSettings?.Actions; /// /// Copies all input actions to be used publicly /// /// public static IEnumerable GetActions() => InputActions.Select( x => new InputAction( x.Name, x.KeyboardCode, x.GamepadCode, x.GroupName, x.Title ) ); /// /// Names of all actions from the current game's input settings. /// public static IEnumerable ActionNames => InputActions?.Select( x => x.Name ) ?? Array.Empty(); /// /// Finds the of the given action. /// /// Action name to find the group name of. public static string GetGroupName( string action ) => FindInputActionByName( action )?.GroupName; /// /// Read the config from this source /// internal static void ReadConfig( InputSettings inputConfig ) { InputSettings = inputConfig; InputSettings ??= new InputSettings(); // if there's nothing in the list, we want to init to the defaults if ( InputSettings?.Actions?.Count == 0 ) { InputSettings.InitDefault(); } MissingActions.Clear(); if ( string.IsNullOrEmpty( Application.GameIdent ) || InputActions is null ) return; // Tell the binding system about the new binds so it can set defaults properly var collection = InputBinds.FindCollection( Application.GameIdent ); collection.UpdateActions( InputActions ); } }