Files
sbox-public/engine/Sandbox.Engine/Systems/Console/Command.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

344 lines
8.3 KiB
C#

using Sandbox.Engine;
using Sandbox.Utility;
using System.Reflection;
namespace Sandbox;
partial class Command
{
internal static Connection Caller { get; set; }
public string Name { get; set; }
public string Help { get; set; }
public bool IsConCommand { get; set; }
public bool IsVariable => !IsConCommand;
/// <summary>
/// Saved into config file
/// </summary>
public bool IsSaved { get; set; }
/// <summary>
/// If true then this cannot be interacted with via game code
/// </summary>
public bool IsProtected { get; set; }
/// <summary>
/// If true then this command can only be run on the server
/// </summary>
public bool IsServer { get; set; }
/// <summary>
/// If true then this command can only be run by a server administrator
/// </summary>
public bool IsAdmin { get; set; }
/// <summary>
/// Server value is replicated to clients
/// </summary>
public bool IsReplicated { get; set; }
/// <summary>
/// Not visible in auto complete and find
/// </summary>
public bool IsHidden { get; set; }
/// <summary>
/// Client values are sent to the server, accessible via Connection userinfo
/// </summary>
public bool IsUserInfo { get; set; }
/// <summary>
/// Can't access unless sv_cheats is 1
/// </summary>
public bool IsCheat { get; set; }
public float? MinValue { get; set; }
public float? MaxValue { get; set; }
public virtual string Value { get; set; }
public virtual string DefaultValue => default;
public virtual bool IsFromAssembly( Assembly assembly ) => false;
public virtual void Run( string args ) { }
public virtual void Save() { }
public virtual bool TryLoad( out string loaded )
{
loaded = null;
return false;
}
public virtual string BuildDescription()
{
if ( IsConCommand )
{
return Help; // todo - command line arguments auto
}
return $"\"{Value}\" - Default: \"{DefaultValue}\" - {Help}";
}
/// <summary>
/// If we have a command line version of the command then set it and return true
/// </summary>
public bool SetVariableFromCommandLine()
{
if ( !IsVariable ) return false;
var value = CommandLine.GetSwitch( "+" + Name, null );
if ( value is null ) return false;
Value = value.TrimQuoted();
return true;
}
}
partial class ManagedCommand : Command
{
internal ConVarAttribute attribute;
internal MemberInfo member;
internal Assembly assembly;
CookieContainer cookies;
bool _isMenu;
internal ParameterInfo[] parameters;
private string _defaultValue;
public override string DefaultValue => _defaultValue;
public ManagedCommand( Assembly assembly, MemberInfo member, ConVarAttribute attribute, CookieContainer cookies = null )
{
this.attribute = attribute;
this.assembly = assembly;
this.member = member;
this.cookies = cookies;
if ( member is MethodInfo method )
{
parameters = method.GetParameters();
}
Name = attribute.Name;
Help = attribute.Help ?? "No description";
IsConCommand = attribute is ConCmdAttribute;
IsHidden = attribute.Flags.Contains( ConVarFlags.Hidden );
IsServer = attribute.Flags.Contains( ConVarFlags.Server );
IsAdmin = attribute.Flags.Contains( ConVarFlags.Admin );
IsProtected = attribute.Flags.Contains( ConVarFlags.Protected );
IsCheat = attribute.Flags.Contains( ConVarFlags.Cheat );
_isMenu = attribute.Context == "menu";
//
// Set up default value if we have one
//
var type = member is PropertyInfo pi ? pi.PropertyType : member.DeclaringType;
var defaultValue = member.GetCustomAttribute<DefaultValueAttribute>()?.Value;
_defaultValue = defaultValue?.ToString()
?? (type.IsValueType ? Activator.CreateInstance( type )?.ToString() : null);
if ( attribute is ConVarAttribute cv )
{
IsSaved = cv.Saved;
MinValue = cv.MinValue;
MaxValue = cv.MaxValue;
IsReplicated = cv.Flags.Contains( ConVarFlags.Replicated );
IsUserInfo = cv.Flags.Contains( ConVarFlags.UserInfo );
}
if ( string.IsNullOrWhiteSpace( Name ) )
{
Name = member.Name;
}
// Names should be ascii letter digits only, definitely no spaces, semicolons or quotes
if ( !Name.All( x => char.IsAsciiLetterOrDigit( x ) || x == '_' || x == '.' || x == '-' ) )
{
throw new ArgumentException( $"Console name \"{Name}\" is invalid, it should only contain ascii letters, digits or underscores." );
}
if ( string.IsNullOrWhiteSpace( Help ) )
{
var info = DisplayInfo.ForMember( member );
Help = info.Description;
}
}
public override void Run( string argstring )
{
using var contextLocal = _isMenu ? GlobalContext.MenuScope() : GlobalContext.GameScope();
using var scope = _isMenu ? IMenuDll.Current?.PushScope() : IGameInstanceDll.Current?.PushScope();
var caller = Caller ?? Connection.Local;
if ( IsAdmin && !caller.IsHost )
{
caller.SendLog( LogLevel.Warn, "You are not allowed to run this command." );
return;
}
//
// Console command
//
if ( member is MethodInfo method )
{
if ( method == null )
throw new Exception( "ConsoleCommand is not a Method" );
var callargs = new object[parameters.Length];
var args = argstring.SplitQuotesStrings();
var argsCount = args?.Length ?? 0;
var parameterStartIndex = 0;
var paramCount = parameters.Length;
if ( paramCount > 0 && parameters[0].ParameterType == typeof( Connection ) )
{
parameterStartIndex = 1;
paramCount--;
callargs[0] = caller;
}
int argIndex = 0;
for ( int i = parameterStartIndex; i < parameters.Length; i++ )
{
var param = parameters[i];
if ( param.ParameterType.IsArray && param.GetCustomAttribute<ParamArrayAttribute>() != null )
{
var elemType = param.ParameterType.GetElementType();
var srcValues = (args?.Skip( argIndex ) ?? Enumerable.Empty<string>())
.Select( x => x.ToType( elemType ) )
.ToArray();
var paramsArray = Array.CreateInstance( elemType, srcValues.Length );
Array.Copy( srcValues, paramsArray, srcValues.Length );
callargs[i] = paramsArray;
break;
}
if ( argIndex < argsCount )
{
callargs[i] = args[argIndex].ToType( param.ParameterType );
argIndex++;
continue;
}
if ( parameters[i].HasDefaultValue )
{
callargs[i] = param.DefaultValue;
continue;
}
Log.Warning( $"Not enough arguments for command \"{Name}\"! Expected {paramCount}, got {argsCount}." );
return;
}
try
{
method.Invoke( null, callargs );
return;
}
catch ( Exception e )
{
Log.Error( e.InnerException, $"Exception when calling command \"{Name}\"" );
return;
}
}
//
// Console variable
//
if ( member is PropertyInfo propertyInfo )
{
if ( argstring == null )
return;
Value = argstring;
return;
}
return;
}
public override string Value
{
get
{
if ( member is PropertyInfo propertyInfo )
{
return propertyInfo.GetValue( null )?.ToString();
}
return null;
}
set
{
if ( member is not PropertyInfo propertyInfo )
{
Log.Warning( $"Setting {Name} and it's not a variable" );
return;
}
var oldValue = propertyInfo.GetValue( null );
var newValue = value.ToType( propertyInfo.PropertyType );
if ( newValue is float f )
{
if ( MinValue.HasValue ) f = Math.Max( f, MinValue.Value );
if ( MaxValue.HasValue ) f = Math.Min( f, MaxValue.Value );
newValue = f;
}
else if ( newValue is int i )
{
if ( MinValue.HasValue ) i = Math.Max( i, (int)MinValue.Value );
if ( MaxValue.HasValue ) i = Math.Min( i, (int)MaxValue.Value );
newValue = i;
}
if ( object.Equals( newValue, oldValue ) )
return;
propertyInfo.SetValue( null, newValue );
Save();
}
}
public override bool IsFromAssembly( Assembly assembly ) => assembly == this.assembly;
/// <summary>
/// Todo: Add support for managed commands to return shit here
/// Todo: We could maybe do this in a cool way, using parameters?
/// So that for example, we could list players if it's a player etc
/// </summary>
public virtual string[] GetAutoComplete( string v )
{
return null;
}
public override bool TryLoad( out string loaded )
{
loaded = default;
if ( !IsSaved ) return false;
if ( cookies == null ) return false;
return cookies.TryGetString( $"convar.{Name}", out loaded );
}
public override void Save()
{
if ( !IsSaved ) return;
if ( cookies == null ) return;
cookies.SetString( $"convar.{Name}", Value );
}
}