Partial of the Superuser admin menu that draws the Commands window. Lists every command from Server.Commands grouped by module, builds a typed input per argument (bool, enum, player picker, duration presets, numbers), validates order and fill, then runs the command through RunCommand with a confirm step for mutating commands and shows the reply in a key/value table.
using Sandbox;
using SboxSuperuser.Engine.Vendor.ImGui;
namespace SboxSuperuser.UI;
internal sealed partial class AdminMenuRenderer
{
private string _commandFilter = "";
private string _selectedCommand = "";
// Typed values per command and argument, kept while the menu is open so switching commands loses nothing.
private readonly Dictionary<string, string> _commandValues = new();
/// <summary>Every command the caller may run, with a form built from its arguments.</summary>
private void DrawCommandsWindow()
{
if ( !BeginWindow( CommandsWindow, "Commands", new Vector2( 96, 96 ), new Vector2( 840, 520 ), new Vector2( 520, 300 ), out var expanded ) ) return;
if ( expanded )
{
Caption( "Everything you can run on this server, including the game's own commands. Results are private to you." );
var reply = Server.Commands;
if ( reply is null ) Caption( "Loading commands..." );
else if ( reply.Status != CommandStatus.Success ) ColoredWrapped( reply.Message, AdminMenuTheme.Tone( reply.Status ) );
else DrawCommandPanes( reply.Commands );
}
ImGui.End();
}
private void DrawCommandPanes( AdminCommandRow[] commands )
{
var available = ImGui.GetContentRegionAvail();
ImGui.BeginChild( "command-list", new Vector2( MathF.Max( 220 * Scale, available.x * 0.32f ), 0 ), ImGuiChildFlags.Borders | ImGuiChildFlags.ResizeX );
DrawCommandList( commands );
ImGui.EndChild();
ImGui.SameLine();
ImGui.BeginChild( "command-form", Vector2.Zero, ImGuiChildFlags.Borders );
var selected = commands.FirstOrDefault( command => command.Name == _selectedCommand );
if ( selected is null ) Caption( "Pick a command on the left." );
else DrawCommandForm( selected );
ImGui.EndChild();
}
private void DrawCommandList( AdminCommandRow[] commands )
{
Field( "##command-filter", "Filter", ref _commandFilter, 64, ImGui.GetContentRegionAvail().x );
var filter = _commandFilter.Trim();
// Superuser's own commands first, then each game module under its ID.
foreach ( var module in commands.GroupBy( command => command.ModuleId ).OrderBy( group => IsBuiltIn( group.Key ) ? "" : group.Key ) )
{
var visible = module.Where( command => filter.Length == 0
|| command.Name.Contains( filter, StringComparison.OrdinalIgnoreCase )
|| command.Description.Contains( filter, StringComparison.OrdinalIgnoreCase ) ).ToArray();
if ( visible.Length == 0 ) continue;
ImGui.SeparatorText( IsBuiltIn( module.Key ) ? "Superuser" : module.Key );
foreach ( var command in visible )
{
// Command keys are validated lowercase keys, so they are safe as ImGui labels.
if ( ImGui.Selectable( command.Name, command.Name == _selectedCommand ) ) _selectedCommand = command.Name;
ImGui.SetItemTooltip( "{0}", command.Description );
}
}
}
private static bool IsBuiltIn( string moduleId ) => moduleId.StartsWith( "superuser.", StringComparison.Ordinal );
private void DrawCommandForm( AdminCommandRow command )
{
ImGui.TextUnformatted( command.Name );
if ( command.Mutation )
{
ImGui.SameLine();
ImGui.TextColored( AdminMenuTheme.Warning, "{0}", "changes state" );
}
Caption( command.Description );
ImGui.Separator();
ImGui.PushID( command.Name );
foreach ( var argument in command.Arguments ) DrawArgumentInput( command, argument );
var error = BuildArguments( command, out var values );
ImGui.Spacing();
if ( error is not null ) ImGui.TextColored( AdminMenuTheme.Warning, "{0}", error );
ImGui.BeginDisabled( error is not null || Server.Running );
if ( ImGui.Button( Server.Running && Server.LastCommand == command.Name ? "Running..." : "Run", new Vector2( 120 * Scale, 0 ) ) )
{
var details = command.Arguments.Zip( values ).Select( pair => (pair.First.Name, pair.Second) ).ToArray();
RunCommand( $"Run {command.Name}?", command.Name, values, confirm: command.Mutation, details: details );
}
ImGui.EndDisabled();
ImGui.PopID();
if ( Server.LastCommand == command.Name && Server.LastResult is { } result )
{
ImGui.SeparatorText( "Result" );
DrawCommandResult( result );
}
}
private void DrawArgumentInput( AdminCommandRow command, AdminCommandArgumentRow argument )
{
var key = command.Name + "/" + argument.Name;
var value = _commandValues.GetValueOrDefault( key, argument.Kind == CommandArgumentKind.Boolean ? "false" : "" );
var label = argument.Optional ? $"{argument.Name} (optional)" : argument.Name;
var width = ImGui.GetContentRegionAvail().x * 0.6f;
ImGui.PushID( argument.Name );
switch ( argument.Kind )
{
case CommandArgumentKind.Boolean:
var enabled = value == "true";
if ( ImGui.Checkbox( label, ref enabled ) ) value = enabled ? "true" : "false";
break;
case CommandArgumentKind.Enum:
ImGui.SetNextItemWidth( width );
if ( ImGui.BeginCombo( label, value.Length == 0 ? "Choose..." : value ) )
{
if ( argument.Optional && ImGui.Selectable( "(none)", value.Length == 0 ) ) value = "";
foreach ( var choice in argument.Choices )
{
ImGui.PushID( choice );
if ( ImGui.Selectable( "##choice", choice == value, ImGuiSelectableFlags.AllowOverlap ) ) value = choice;
ImGui.SameLine();
ImGui.TextUnformatted( choice );
ImGui.PopID();
}
ImGui.EndCombo();
}
break;
case CommandArgumentKind.Player:
Field( "##value", "Steam ID or exact online name", ref value, 64, width - ImGui.GetFrameHeight() - ImGui.GetStyle().ItemInnerSpacing.x );
ImGui.SameLine( 0, ImGui.GetStyle().ItemInnerSpacing.x );
DrawOnlinePlayerPicker( ref value );
ImGui.SameLine();
ImGui.TextUnformatted( label );
break;
case CommandArgumentKind.Duration:
Field( "##value", "30m, 2h, 7d or permanent", ref value, 32, width - 120 * Scale - ImGui.GetStyle().ItemInnerSpacing.x );
ImGui.SameLine( 0, ImGui.GetStyle().ItemInnerSpacing.x );
ImGui.SetNextItemWidth( 120 * Scale );
if ( ImGui.BeginCombo( "##presets", "Presets" ) )
{
foreach ( var preset in DurationPresets ) if ( ImGui.Selectable( preset, preset == value ) ) value = preset;
ImGui.EndCombo();
}
ImGui.SameLine();
ImGui.TextUnformatted( label );
break;
default:
Field( "##value", Hint( argument ), ref value, 256, argument.Remainder ? ImGui.GetContentRegionAvail().x * 0.8f : width );
ImGui.SameLine();
ImGui.TextUnformatted( label );
break;
}
if ( argument.Description.Length > 0 ) ImGui.SetItemTooltip( "{0}", argument.Description );
ImGui.PopID();
_commandValues[ key ] = value;
}
private void DrawOnlinePlayerPicker( ref string value )
{
var players = Model.Players?.Players ?? Array.Empty<AdminPlayerRow>();
ImGui.BeginDisabled( players.Length == 0 );
if ( ImGui.BeginCombo( "##online", "", ImGuiComboFlags.NoPreview ) )
{
foreach ( var player in players )
{
ImGui.PushID( player.SteamId );
if ( ImGui.Selectable( "##pick", player.SteamId == value, ImGuiSelectableFlags.AllowOverlap ) ) value = player.SteamId;
ImGui.SameLine();
ImGui.TextUnformatted( player.Name );
ImGui.PopID();
}
ImGui.EndCombo();
}
ImGui.EndDisabled();
ImGui.SetItemTooltip( "{0}", "Pick an online player" );
}
private static string Hint( AdminCommandArgumentRow argument )
{
if ( argument.Kind is not (CommandArgumentKind.Integer or CommandArgumentKind.Number) ) return argument.Remainder ? "Text" : "Value";
var kind = argument.Kind == CommandArgumentKind.Integer ? "Whole number" : "Number";
return argument.Minimum is { } min && argument.Maximum is { } max ? $"{kind}, {min}–{max}" : kind;
}
/// <summary>Collects values in order; returns what is missing or wrong, or null when the command can run.</summary>
private string BuildArguments( AdminCommandRow command, out string[] values )
{
var collected = new List<string>();
var skipped = "";
foreach ( var argument in command.Arguments )
{
var value = _commandValues.GetValueOrDefault( command.Name + "/" + argument.Name, "" ).Trim();
if ( value.Length == 0 )
{
if ( !argument.Optional ) { values = Array.Empty<string>(); return $"Fill in {argument.Name}."; }
if ( skipped.Length == 0 ) skipped = argument.Name;
continue;
}
// Arguments are positional, so a later value cannot follow an empty optional one.
if ( skipped.Length > 0 ) { values = Array.Empty<string>(); return $"Fill in {skipped} before {argument.Name}."; }
if ( argument.Kind is CommandArgumentKind.Integer or CommandArgumentKind.Number && !double.TryParse( value,
System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out _ ) )
{
values = Array.Empty<string>();
return $"{argument.Name} must be a number.";
}
collected.Add( value );
}
values = collected.ToArray();
return null;
}
private void DrawCommandResult( AdminReply result )
{
ImGui.TextColored( AdminMenuTheme.Tone( result.Status ), "{0}", result.Status.ToString() );
ImGui.SameLine();
ImGui.TextWrapped( "{0}", result.Message );
if ( result.Data.Count == 0 ) return;
var flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable;
if ( !ImGui.BeginTable( "command-result", 2, flags ) ) return;
ImGui.TableSetupColumn( "Field", ImGuiTableColumnFlags.WidthFixed, ImGui.GetContentRegionAvail().x * 0.35f );
ImGui.TableSetupColumn( "Value", ImGuiTableColumnFlags.WidthStretch );
foreach ( var (field, value) in result.Data.OrderBy( pair => ResultOrder( pair.Key ) ) )
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextDisabled( "{0}", field );
ImGui.TableNextColumn();
ImGui.TextWrapped( "{0}", value );
}
ImGui.EndTable();
}
// Listing rows (row1..row8) come first in their natural order, then the remaining fields alphabetically.
private static string ResultOrder( string key ) =>
key.StartsWith( "row", StringComparison.Ordinal ) && int.TryParse( key.AsSpan( 3 ), out var row ) ? $"0{row:D3}" : "1" + key;
}