Editor/Drivers/SyntheticInputDriver.cs

Editor helper that simulates player input for the editor's MCP (multi-creator panel). It maps common action aliases to canonical action names, sets Sandbox.Input actions, optionally pulses them for a duration, and adjusts the analog movement vector for movement actions.

Native Interop
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace Editor.Mcp
{
	internal static class SyntheticInputDriver
	{
		private static readonly Dictionary<string, string> ActionAliases = new( StringComparer.OrdinalIgnoreCase )
		{
			{ "forward", "Forward" },
			{ "back", "Backward" },
			{ "backward", "Backward" },
			{ "left", "Left" },
			{ "right", "Right" },
			{ "jump", "Jump" },
			{ "run", "Run" },
			{ "sprint", "Run" },
			{ "walk", "Walk" },
			{ "duck", "Duck" },
			{ "crouch", "Duck" },
			{ "attack1", "Attack1" },
			{ "primary_attack", "Attack1" },
			{ "attack2", "Attack2" },
			{ "secondary_attack", "Attack2" },
			{ "reload", "Reload" },
			{ "use", "Use" },
			{ "menu", "Menu" },
			{ "score", "Score" },
			{ "chat", "Chat" },
			{ "voice", "Voice" }
		};

		public static async Task<ToolOutcome<object>> SimulateInputAsync( string action, string state = "pulse", int durationMs = 100 )
		{
			if ( !Game.IsPlaying )
			{
				return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "input.playmode_inactive", "Play mode must be active to simulate player input." );
			}

			var rawAction = (action ?? "").Trim().TrimStart( '+', '-' );
			if ( string.IsNullOrWhiteSpace( rawAction ) )
			{
				return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "input.action_required", "Action name is required (e.g. 'Forward', 'Jump', 'Attack1')." );
			}

			var boundedDuration = Math.Clamp( durationMs, 10, 10000 );
			var normState = state.ToLowerInvariant();

			// Resolve to canonical action name or match existing registered action names
			var resolvedAction = ActionAliases.TryGetValue( rawAction, out var alias ) ? alias : rawAction;
			var knownAction = Sandbox.Input.ActionNames?.FirstOrDefault( a => string.Equals( a, resolvedAction, StringComparison.OrdinalIgnoreCase ) );
			if ( knownAction != null )
			{
				resolvedAction = knownAction;
			}

			bool isDown = normState != "up";

			// 1. Direct modern s&box Scene System action dispatch
			Sandbox.Input.SetAction( resolvedAction, isDown );

			// 2. Also drive analog movement vector for movement actions
			ApplyAnalogMoveForAction( resolvedAction, isDown );

			if ( normState == "pulse" )
			{
				await Task.Delay( boundedDuration );
				Sandbox.Input.SetAction( resolvedAction, false );
				ApplyAnalogMoveForAction( resolvedAction, false );
			}

			return ToolOutcome<object>.Ready( new
			{
				Action = resolvedAction,
				State = normState,
				DurationMs = boundedDuration,
				Executed = true,
				AnalogMove = Sandbox.Input.AnalogMove
			} );
		}

		private static void ApplyAnalogMoveForAction( string action, bool down )
		{
			var current = Sandbox.Input.AnalogMove;
			switch ( action.ToLowerInvariant() )
			{
				case "forward":
					Sandbox.Input.AnalogMove = new Vector3( down ? 1f : 0f, current.y, current.z );
					break;
				case "backward":
					Sandbox.Input.AnalogMove = new Vector3( down ? -1f : 0f, current.y, current.z );
					break;
				case "left":
					Sandbox.Input.AnalogMove = new Vector3( current.x, down ? 1f : 0f, current.z );
					break;
				case "right":
					Sandbox.Input.AnalogMove = new Vector3( current.x, down ? -1f : 0f, current.z );
					break;
			}
		}
	}
}