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; /// /// Saved into config file /// public bool IsSaved { get; set; } /// /// If true then this cannot be interacted with via game code /// public bool IsProtected { get; set; } /// /// If true then this command can only be run on the server /// public bool IsServer { get; set; } /// /// If true then this command can only be run by a server administrator /// public bool IsAdmin { get; set; } /// /// Server value is replicated to clients /// public bool IsReplicated { get; set; } /// /// Not visible in auto complete and find /// public bool IsHidden { get; set; } /// /// Client values are sent to the server, accessible via Connection userinfo /// public bool IsUserInfo { get; set; } /// /// Can't access unless sv_cheats is 1 /// 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}"; } /// /// If we have a command line version of the command then set it and return true /// 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()?.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() != null ) { var elemType = param.ParameterType.GetElementType(); var srcValues = (args?.Skip( argIndex ) ?? Enumerable.Empty()) .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; /// /// 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 /// 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 ); } }