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.
#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;
}
}
}
}