Files
sbox-public/engine/Sandbox.Engine/Systems/Console/ConVarSystem.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
9.0 KiB
C#

using Sandbox.Network;
using System.Reflection;
namespace Sandbox;
internal static partial class ConVarSystem
{
public delegate void ConVarChangedDelegate( Command command, string oldValue );
/// <summary>
/// Called when the ConVar is changed.
/// </summary>
public static ConVarChangedDelegate ConVarChanged { get; set; }
internal static readonly Dictionary<string, Command> Members = new( StringComparer.OrdinalIgnoreCase );
internal static readonly Dictionary<string, CookieContainer> 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;
}
/// <summary>
/// Add this assembly to the console library, which will scan it for console commands and make them available.
/// </summary>
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<ConVarAttribute>();
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 );
}
}
}
}
/// <summary>
/// Remove this assembly and its console commands.
/// </summary>
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 );
}
}
/// <summary>
/// Add this command to the library. Any existing commands named the same will be over-written.
/// </summary>
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})" );
}
/// <summary>
/// Add this ConVar to the library. Any existing commands named the same will be over-written.
/// </summary>
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<string, string> 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();
}
/// <summary>
/// Get a ConVar value as a string.
/// </summary>
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;
}
/// <summary>
/// Get a ConVar value as an integer. If the ConVar value is a boolean, this will return
/// the value in its integer form.
/// </summary>
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;
}
/// <summary>
/// Get a ConVar value as a float.
/// </summary>
public static float GetFloat( string name, float defaultValue, bool allowEngineVariable )
{
return float.TryParse( GetValue( name, defaultValue.ToString(), allowEngineVariable ), out var value ) ? value : defaultValue;
}
/// <summary>
/// Try to set a ConVar. You will only be able to set variables that you have permission to set.
/// </summary>
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();
}
/// <summary>
/// Try to set a ConVar. You will only be able to set variables that you have permission to set.
/// </summary>
public static void SetInt( string name, int value, bool allowProtected )
{
SetValue( name, value.ToString(), allowProtected );
}
/// <summary>
/// Try to set a ConVar. You will only be able to set variables that you have permission to set.
/// </summary>
public static void SetFloat( string name, float value, bool allowProtected )
{
SetValue( name, value.ToString(), allowProtected );
}
/// <summary>
/// Save all the convars.
/// </summary>
[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();
}
}
/// <summary>
/// Run a single command. [command] [args]
/// </summary>
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 );
}
/// <summary>
/// Run a potential string of commands, seperated by newlines or ;
/// </summary>
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 );
}
}
/// <summary>
/// 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!
/// </summary>
internal static void OnConVarChanged<T>( 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 );
}
}