using Sandbox.Network; using System.Reflection; namespace Sandbox; internal static partial class ConVarSystem { public delegate void ConVarChangedDelegate( Command command, string oldValue ); /// /// Called when the ConVar is changed. /// public static ConVarChangedDelegate ConVarChanged { get; set; } internal static readonly Dictionary Members = new( StringComparer.OrdinalIgnoreCase ); internal static readonly Dictionary CookieContainers = new( StringComparer.OrdinalIgnoreCase ); static CookieContainer GetCookies( string name ) { if ( CookieContainers.TryGetValue( name, out var cookies ) ) return cookies; cookies = new( $"convar/{name}" ); CookieContainers[name] = cookies; return cookies; } /// /// Add this assembly to the console library, which will scan it for console commands and make them available. /// internal static void AddAssembly( Assembly assembly, string cookies, string context = null ) { if ( assembly == null ) return; foreach ( var t in assembly.GetTypes() ) { var methods = t.GetMembers( BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public ); foreach ( var member in methods ) { var attribute = member.GetCustomAttribute(); if ( attribute == null ) continue; if ( attribute.Context != context ) continue; try { if ( member is PropertyInfo prop ) { var get = prop.GetGetMethod( true ); var set = prop.GetSetMethod( true ); if ( get == null || set == null ) continue; if ( !get.IsStatic ) continue; AddConVar( new ManagedCommand( assembly, member, attribute, GetCookies( cookies ) ) ); } if ( member is MethodInfo method ) { if ( !method.IsStatic ) continue; AddCommand( new ManagedCommand( assembly, member, attribute, GetCookies( cookies ) ) ); } } catch ( Exception e ) { Log.Error( e ); } } } } /// /// Remove this assembly and its console commands. /// internal static void RemoveAssembly( Assembly assembly ) { if ( assembly == null ) return; foreach ( var (name, member) in Members.Where( x => x.Value.IsFromAssembly( assembly ) ).ToArray() ) { RememberValue( member ); Members.Remove( name ); } } /// /// Add this command to the library. Any existing commands named the same will be over-written. /// internal static void AddCommand( Command command ) { if ( Members.TryAdd( command.Name, command ) ) return; Log.Warning( $"Command {command.Name} already exists - not overwriting ({Members[command.Name].Help})" ); } /// /// Add this ConVar to the library. Any existing commands named the same will be over-written. /// internal static void AddConVar( Command command ) { if ( !Members.TryAdd( command.Name, command ) ) { Log.Warning( $"Convar {command.Name} already exists - not overwriting" ); return; } if ( command.MinValue.HasValue && command.MaxValue.HasValue && command.MinValue > command.MaxValue ) (command.MinValue, command.MaxValue) = (command.MaxValue, command.MinValue); // Load it from cookies if ( command.TryLoad( out var loadedValue ) ) { command.Value = loadedValue; } // Set it from command line command.SetVariableFromCommandLine(); // This is a convar that was added before, maybe a hotload, set it if ( TryGetRememberedValue( command.Name, out var rememberedValue ) ) { command.Value = rememberedValue; } } internal static Command Find( string name ) { return Members.GetValueOrDefault( name ); } // // We want to remember ConVar values between hotloads // static readonly Dictionary RememberedConVarValues = new(); private static void RememberValue( Command var ) { if ( !var.IsVariable ) return; var value = GetValue( var.Name, null, false ); if ( value == null ) return; RememberedConVarValues[var.Name] = value; } private static bool TryGetRememberedValue( string member, out string val ) { return RememberedConVarValues.TryGetValue( member, out val ); } internal static void ClearRememberedValues() { RememberedConVarValues.Clear(); } /// /// Get a ConVar value as a string. /// public static string GetValue( string name, string defaultValue, bool allowEngineVariable ) { var cmd = Find( name ); if ( cmd is null ) return defaultValue; if ( !cmd.IsVariable ) return defaultValue; if ( !allowEngineVariable && cmd.IsProtected ) return defaultValue; return cmd.Value; } /// /// Get a ConVar value as an integer. If the ConVar value is a boolean, this will return /// the value in its integer form. /// public static int GetInt( string name, int defaultValue, bool allowEngineVariable ) { var value = GetValue( name, defaultValue.ToString(), allowEngineVariable ); if ( bool.TryParse( value, out var boolean ) ) return boolean ? 1 : 0; return int.TryParse( value, out var integer ) ? integer : defaultValue; } /// /// Get a ConVar value as a float. /// public static float GetFloat( string name, float defaultValue, bool allowEngineVariable ) { return float.TryParse( GetValue( name, defaultValue.ToString(), allowEngineVariable ), out var value ) ? value : defaultValue; } /// /// Try to set a ConVar. You will only be able to set variables that you have permission to set. /// public static void SetValue( string name, string value, bool allowProtected ) { var cmd = Find( name ); if ( cmd is null ) return; if ( !cmd.IsVariable ) return; if ( cmd.IsProtected && !allowProtected ) return; cmd.Value = value; // Let the ConVar save itself, if it wants to cmd.Save(); } /// /// Try to set a ConVar. You will only be able to set variables that you have permission to set. /// public static void SetInt( string name, int value, bool allowProtected ) { SetValue( name, value.ToString(), allowProtected ); } /// /// Try to set a ConVar. You will only be able to set variables that you have permission to set. /// public static void SetFloat( string name, float value, bool allowProtected ) { SetValue( name, value.ToString(), allowProtected ); } /// /// Save all the convars. /// [ConCmd( "convars_save", ConVarFlags.Protected )] public static void SaveAll() { foreach ( var convar in Members.Values ) { if ( convar.IsConCommand ) continue; if ( !convar.IsSaved ) continue; convar.Save(); } foreach ( var container in CookieContainers.Values ) { container.Save(); } } /// /// Run a single command. [command] [args] /// internal static void RunSingle( string v ) { var parts = v.Split( ' ', 2, StringSplitOptions.RemoveEmptyEntries ); if ( !Members.TryGetValue( parts[0], out var command ) ) { Log.Warning( $"Unknown Command '{parts[0]}'" ); return; } var hasArguments = parts.Length > 1; if ( !hasArguments && command.IsVariable ) { Log.Info( $"{command.Name} - {command.BuildDescription()}" ); return; } if ( command.IsCheat && !Game.CheatsEnabled ) { Log.Info( "Cheats are not enabled on the server." ); return; } var args = string.Join( " ", parts.Skip( 1 ) ); if ( command.IsVariable ) { command.Value = args.SplitQuotesStrings()[0]; return; } if ( Networking.IsActive && !Networking.IsHost && (command.IsServer || command.IsAdmin) ) { var msg = new ServerCommand { Command = command.Name, Args = args }; Connection.Host?.SendMessage( msg, NetFlags.Reliable ); return; } command.Run( args ); } /// /// Run a potential string of commands, seperated by newlines or ; /// internal static void Run( string v ) { ThreadSafe.AssertIsMainThread(); if ( string.IsNullOrWhiteSpace( v ) ) return; foreach ( var part in v.Split( ';', '\n' ) ) { if ( string.IsNullOrWhiteSpace( part ) ) continue; RunSingle( part ); } } /// /// Should be called any time a ConVar is changed. This should be called from two places, the managed side and the native side. /// Managed side comes from when the property value actually changes. The native side comes from a on change callback. /// CAVEAT: This will only get called for ConVars that use CODEGEN! /// internal static void OnConVarChanged( string name, T value, T previous ) { if ( Members.TryGetValue( name, out var command ) ) { MainThread.Queue( () => InvokeConVarChanged( command, previous?.ToString() ) ); } } internal static void InvokeConVarChanged( Command command, string oldValue ) { if ( command.IsUserInfo && Networking.IsActive ) { if ( !Networking.IsHost ) { var msg = new UserInfoUpdate { Command = command.Name, Value = command.Value }; Connection.Host?.SendMessage( msg, NetFlags.Reliable ); } else { Connection.Local?.SetUserData( command.Name, command.Value ); } } ConVarChanged?.Invoke( command, oldValue ); } }