using UnityEngine; using System.Reflection; using System.Collections.Generic; using System.Text; using System; // Manages the console commands, parses console input and handles execution of commands // Supported method parameter types: int, float, bool, string, Vector2, Vector3, Vector4 // Helper class to store important information about a command namespace IngameDebugConsole { public class ConsoleMethodInfo { public readonly MethodInfo method; public readonly Type[] parameterTypes; public readonly object instance; public readonly string signature; public ConsoleMethodInfo( MethodInfo method, Type[] parameterTypes, object instance, string signature ) { this.method = method; this.parameterTypes = parameterTypes; this.instance = instance; this.signature = signature; } public bool IsValid() { if( !method.IsStatic && ( instance == null || instance.Equals( null ) ) ) return false; return true; } } public static class DebugLogConsole { public delegate bool ParseFunction( string input, out object output ); // All the commands private static Dictionary methods = new Dictionary(); // All the parse functions private static Dictionary parseFunctions; // All the readable names of accepted types private static Dictionary typeReadableNames; // Split arguments of an entered command private static List commandArguments = new List( 8 ); // Command parameter delimeter groups private static readonly string[] inputDelimiters = new string[] { "\"\"", "{}", "()", "[]" }; static DebugLogConsole() { parseFunctions = new Dictionary() { { typeof( string ), ParseString }, { typeof( bool ), ParseBool }, { typeof( int ), ParseInt }, { typeof( uint ), ParseUInt }, { typeof( long ), ParseLong }, { typeof( ulong ), ParseULong }, { typeof( byte ), ParseByte }, { typeof( sbyte ), ParseSByte }, { typeof( short ), ParseShort }, { typeof( ushort ), ParseUShort }, { typeof( char ), ParseChar }, { typeof( float ), ParseFloat }, { typeof( double ), ParseDouble }, { typeof( decimal ), ParseDecimal }, { typeof( Vector2 ), ParseVector2 }, { typeof( Vector3 ), ParseVector3 }, { typeof( Vector4 ), ParseVector4 }, { typeof( GameObject ), ParseGameObject } }; typeReadableNames = new Dictionary() { { typeof( string ), "String" }, { typeof( bool ), "Boolean" }, { typeof( int ), "Integer" }, { typeof( uint ), "Unsigned Integer" }, { typeof( long ), "Long" }, { typeof( ulong ), "Unsigned Long" }, { typeof( byte ), "Byte" }, { typeof( sbyte ), "Short Byte" }, { typeof( short ), "Short" }, { typeof( ushort ), "Unsigned Short" }, { typeof( char ), "Char" }, { typeof( float ), "Float" }, { typeof( double ), "Double" }, { typeof( decimal ), "Decimal" }, { typeof( Vector2 ), "Vector2" }, { typeof( Vector3 ), "Vector3" }, { typeof( Vector4 ), "Vector4" }, { typeof( GameObject ), "GameObject" } }; #if UNITY_EDITOR || !NETFX_CORE // Load commands in most common Unity assemblies HashSet assemblies = new HashSet { Assembly.GetAssembly( typeof( DebugLogConsole ) ) }; try { assemblies.Add( Assembly.Load( "Assembly-CSharp" ) ); } catch { } foreach( var assembly in assemblies ) { foreach( var type in assembly.GetExportedTypes() ) { foreach( var method in type.GetMethods( BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly ) ) { foreach( var attribute in method.GetCustomAttributes( typeof( ConsoleMethodAttribute ), false ) ) { ConsoleMethodAttribute consoleMethod = attribute as ConsoleMethodAttribute; if( consoleMethod != null ) AddCommand( consoleMethod.Command, consoleMethod.Description, method ); } } } } #endif AddCommandStatic( "help", "Prints all commands", "LogAllCommands", typeof( DebugLogConsole ) ); AddCommandStatic( "sysinfo", "Prints system information", "LogSystemInfo", typeof( DebugLogConsole ) ); } // Logs the list of available commands public static void LogAllCommands() { int length = 25; foreach( var entry in methods ) { if( entry.Value.IsValid() ) length += 3 + entry.Value.signature.Length; } StringBuilder stringBuilder = new StringBuilder( length ); stringBuilder.Append( "Available commands:" ); foreach( var entry in methods ) { if( entry.Value.IsValid() ) stringBuilder.Append( "\n- " ).Append( entry.Value.signature ); } Debug.Log( stringBuilder.Append( "\n" ).ToString() ); } // Logs system information public static void LogSystemInfo() { StringBuilder stringBuilder = new StringBuilder( 1024 ); stringBuilder.Append( "Rig: " ).AppendSysInfoIfPresent( SystemInfo.deviceModel ).AppendSysInfoIfPresent( SystemInfo.processorType ) .AppendSysInfoIfPresent( SystemInfo.systemMemorySize, "MB RAM" ).Append( SystemInfo.processorCount ).Append( " cores\n" ); stringBuilder.Append( "OS: " ).Append( SystemInfo.operatingSystem ).Append( "\n" ); stringBuilder.Append( "GPU: " ).Append( SystemInfo.graphicsDeviceName ).Append( " " ).Append( SystemInfo.graphicsMemorySize ) .Append( "MB " ).Append( SystemInfo.graphicsDeviceVersion ) .Append( SystemInfo.graphicsMultiThreaded ? " multi-threaded\n" : "\n" ); stringBuilder.Append( "Data Path: " ).Append( Application.dataPath ).Append( "\n" ); stringBuilder.Append( "Persistent Data Path: " ).Append( Application.persistentDataPath ).Append( "\n" ); stringBuilder.Append( "StreamingAssets Path: " ).Append( Application.streamingAssetsPath ).Append( "\n" ); stringBuilder.Append( "Temporary Cache Path: " ).Append( Application.temporaryCachePath ).Append( "\n" ); stringBuilder.Append( "Device ID: " ).Append( SystemInfo.deviceUniqueIdentifier ).Append( "\n" ); stringBuilder.Append( "Max Texture Size: " ).Append( SystemInfo.maxTextureSize ).Append( "\n" ); stringBuilder.Append( "Max Cubemap Size: " ).Append( SystemInfo.maxCubemapSize ).Append( "\n" ); stringBuilder.Append( "Accelerometer: " ).Append( SystemInfo.supportsAccelerometer ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Gyro: " ).Append( SystemInfo.supportsGyroscope ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Location Service: " ).Append( SystemInfo.supportsLocationService ? "supported\n" : "not supported\n" ); #if !UNITY_2019_1_OR_NEWER stringBuilder.Append( "Image Effects: " ).Append( SystemInfo.supportsImageEffects ? "supported\n" : "not supported\n" ); stringBuilder.Append( "RenderToCubemap: " ).Append( SystemInfo.supportsRenderToCubemap ? "supported\n" : "not supported\n" ); #endif stringBuilder.Append( "Compute Shaders: " ).Append( SystemInfo.supportsComputeShaders ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Shadows: " ).Append( SystemInfo.supportsShadows ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Instancing: " ).Append( SystemInfo.supportsInstancing ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Motion Vectors: " ).Append( SystemInfo.supportsMotionVectors ? "supported\n" : "not supported\n" ); stringBuilder.Append( "3D Textures: " ).Append( SystemInfo.supports3DTextures ? "supported\n" : "not supported\n" ); stringBuilder.Append( "3D Render Textures: " ).Append( SystemInfo.supports3DRenderTextures ? "supported\n" : "not supported\n" ); stringBuilder.Append( "2D Array Textures: " ).Append( SystemInfo.supports2DArrayTextures ? "supported\n" : "not supported\n" ); stringBuilder.Append( "Cubemap Array Textures: " ).Append( SystemInfo.supportsCubemapArrayTextures ? "supported" : "not supported" ); Debug.Log( stringBuilder.Append( "\n" ).ToString() ); } private static StringBuilder AppendSysInfoIfPresent( this StringBuilder sb, string info, string postfix = null ) { if( info != SystemInfo.unsupportedIdentifier ) { sb.Append( info ); if( postfix != null ) sb.Append( postfix ); sb.Append( " " ); } return sb; } private static StringBuilder AppendSysInfoIfPresent( this StringBuilder sb, int info, string postfix = null ) { if( info > 0 ) { sb.Append( info ); if( postfix != null ) sb.Append( postfix ); sb.Append( " " ); } return sb; } // Add a command related with an instance method (i.e. non static method) public static void AddCommandInstance( string command, string description, string methodName, object instance ) { if( instance == null ) { Debug.LogError( "Instance can't be null!" ); return; } AddCommand( command, description, methodName, instance.GetType(), instance ); } // Add a command related with a static method (i.e. no instance is required to call the method) public static void AddCommandStatic( string command, string description, string methodName, Type ownerType ) { AddCommand( command, description, methodName, ownerType ); } // Remove a command from the console public static void RemoveCommand( string command ) { if( !string.IsNullOrEmpty( command ) ) methods.Remove( command ); } // Returns the first command that starts with the entered argument public static string GetAutoCompleteCommand( string commandStart ) { foreach( var entry in methods ) { if( entry.Key.StartsWith( commandStart ) ) return entry.Key; } return null; } // Create a new command and set its properties private static void AddCommand( string command, string description, string methodName, Type ownerType, object instance = null ) { if( string.IsNullOrEmpty( command ) ) { Debug.LogError( "Command name can't be empty!" ); return; } command = command.Trim(); if( command.IndexOf( ' ' ) >= 0 ) { Debug.LogError( "Command name can't contain whitespace: " + command ); return; } // Get the method from the class MethodInfo method = ownerType.GetMethod( methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static ); if( method == null ) { Debug.LogError( methodName + " does not exist in " + ownerType ); return; } AddCommand( command, description, method, instance ); } private static void AddCommand( string command, string description, MethodInfo method, object instance = null ) { // Fetch the parameters of the class ParameterInfo[] parameters = method.GetParameters(); if( parameters == null ) parameters = new ParameterInfo[0]; bool isMethodValid = true; // Store the parameter types in an array Type[] parameterTypes = new Type[parameters.Length]; for( int k = 0; k < parameters.Length; k++ ) { Type parameterType = parameters[k].ParameterType; if( parseFunctions.ContainsKey( parameterType ) ) parameterTypes[k] = parameterType; else { isMethodValid = false; break; } } // If method is valid, associate it with the entered command if( isMethodValid ) { StringBuilder methodSignature = new StringBuilder( 256 ); methodSignature.Append( command ).Append( ": " ); if( !string.IsNullOrEmpty( description ) ) methodSignature.Append( description ).Append( " -> " ); methodSignature.Append( method.DeclaringType.ToString() ).Append( "." ).Append( method.Name ).Append( "(" ); for( int i = 0; i < parameterTypes.Length; i++ ) { Type type = parameterTypes[i]; string typeName; if( !typeReadableNames.TryGetValue( type, out typeName ) ) typeName = type.Name; methodSignature.Append( typeName ); if( i < parameterTypes.Length - 1 ) methodSignature.Append( ", " ); } methodSignature.Append( ")" ); Type returnType = method.ReturnType; if( returnType != typeof( void ) ) { string returnTypeName; if( !typeReadableNames.TryGetValue( returnType, out returnTypeName ) ) returnTypeName = returnType.Name; methodSignature.Append( " : " ).Append( returnTypeName ); } methods[command] = new ConsoleMethodInfo( method, parameterTypes, instance, methodSignature.ToString() ); } } // Parse the command and try to execute it public static void ExecuteCommand( string command ) { if( command == null ) return; command = command.Trim(); if( command.Length == 0 ) return; // Parse the arguments commandArguments.Clear(); int endIndex = IndexOfChar( command, ' ', 0 ); commandArguments.Add( command.Substring( 0, endIndex ) ); for( int i = endIndex + 1; i < command.Length; i++ ) { if( command[i] == ' ' ) continue; int delimiterIndex = IndexOfDelimiter( command[i] ); if( delimiterIndex >= 0 ) { endIndex = IndexOfChar( command, inputDelimiters[delimiterIndex][1], i + 1 ); commandArguments.Add( command.Substring( i + 1, endIndex - i - 1 ) ); } else { endIndex = IndexOfChar( command, ' ', i + 1 ); commandArguments.Add( command.Substring( i, endIndex - i ) ); } i = endIndex; } // Check if command exists ConsoleMethodInfo methodInfo; if( !methods.TryGetValue( commandArguments[0], out methodInfo ) ) Debug.LogWarning( "Can't find command: " + commandArguments[0] ); else if( !methodInfo.IsValid() ) Debug.LogWarning( "Method no longer valid (instance dead): " + commandArguments[0] ); else { // Check if number of parameter match if( methodInfo.parameterTypes.Length != commandArguments.Count - 1 ) { Debug.LogWarning( "Parameter count mismatch: " + methodInfo.parameterTypes.Length + " parameters are needed" ); return; } Debug.Log( "Executing command: " + commandArguments[0] ); // Parse the parameters into objects object[] parameters = new object[methodInfo.parameterTypes.Length]; for( int i = 0; i < methodInfo.parameterTypes.Length; i++ ) { string argument = commandArguments[i + 1]; Type parameterType = methodInfo.parameterTypes[i]; ParseFunction parseFunction; if( !parseFunctions.TryGetValue( parameterType, out parseFunction ) ) { Debug.LogError( "Unsupported parameter type: " + parameterType.Name ); return; } object val; if( !parseFunction( argument, out val ) ) { Debug.LogError( "Couldn't parse " + argument + " to " + parameterType.Name ); return; } parameters[i] = val; } // Execute the method associated with the command object result = methodInfo.method.Invoke( methodInfo.instance, parameters ); if( methodInfo.method.ReturnType != typeof( void ) ) { // Print the returned value to the console if( result == null || result.Equals( null ) ) Debug.Log( "Value returned: null" ); else Debug.Log( "Value returned: " + result.ToString() ); } } } // Find the index of the delimiter group that 'c' belongs to private static int IndexOfDelimiter( char c ) { for( int i = 0; i < inputDelimiters.Length; i++ ) { if( c == inputDelimiters[i][0] ) return i; } return -1; } // Find the index of char in the string, or return the length of string instead of -1 private static int IndexOfChar( string command, char c, int startIndex ) { int result = command.IndexOf( c, startIndex ); if( result < 0 ) result = command.Length; return result; } private static bool ParseString( string input, out object output ) { output = input; return input.Length > 0; } private static bool ParseBool( string input, out object output ) { if( input == "1" || input.ToLowerInvariant() == "true" ) { output = true; return true; } if( input == "0" || input.ToLowerInvariant() == "false" ) { output = false; return true; } output = false; return false; } private static bool ParseInt( string input, out object output ) { bool result; int value; result = int.TryParse( input, out value ); output = value; return result; } private static bool ParseUInt( string input, out object output ) { bool result; uint value; result = uint.TryParse( input, out value ); output = value; return result; } private static bool ParseLong( string input, out object output ) { bool result; long value; result = long.TryParse( input, out value ); output = value; return result; } private static bool ParseULong( string input, out object output ) { bool result; ulong value; result = ulong.TryParse( input, out value ); output = value; return result; } private static bool ParseByte( string input, out object output ) { bool result; byte value; result = byte.TryParse( input, out value ); output = value; return result; } private static bool ParseSByte( string input, out object output ) { bool result; sbyte value; result = sbyte.TryParse( input, out value ); output = value; return result; } private static bool ParseShort( string input, out object output ) { bool result; short value; result = short.TryParse( input, out value ); output = value; return result; } private static bool ParseUShort( string input, out object output ) { bool result; ushort value; result = ushort.TryParse( input, out value ); output = value; return result; } private static bool ParseChar( string input, out object output ) { bool result; char value; result = char.TryParse( input, out value ); output = value; return result; } private static bool ParseFloat( string input, out object output ) { bool result; float value; result = float.TryParse( input, out value ); output = value; return result; } private static bool ParseDouble( string input, out object output ) { bool result; double value; result = double.TryParse( input, out value ); output = value; return result; } private static bool ParseDecimal( string input, out object output ) { bool result; decimal value; result = decimal.TryParse( input, out value ); output = value; return result; } private static bool ParseVector2( string input, out object output ) { return CreateVectorFromInput( input, typeof( Vector2 ), out output ); } private static bool ParseVector3( string input, out object output ) { return CreateVectorFromInput( input, typeof( Vector3 ), out output ); } private static bool ParseVector4( string input, out object output ) { return CreateVectorFromInput( input, typeof( Vector4 ), out output ); } private static bool ParseGameObject( string input, out object output ) { output = GameObject.Find( input ); return true; } // Create a vector of specified type (fill the blank slots with 0 or ignore unnecessary slots) private static bool CreateVectorFromInput( string input, Type vectorType, out object output ) { List tokens = new List( input.Replace( ',', ' ' ).Trim().Split( ' ' ) ); int i; for( i = tokens.Count - 1; i >= 0; i-- ) { tokens[i] = tokens[i].Trim(); if( tokens[i].Length == 0 ) tokens.RemoveAt( i ); } float[] tokenValues = new float[tokens.Count]; for( i = 0; i < tokens.Count; i++ ) { float val; if( !float.TryParse( tokens[i], out val ) ) { if( vectorType == typeof( Vector3 ) ) output = new Vector3(); else if( vectorType == typeof( Vector2 ) ) output = new Vector2(); else output = new Vector4(); return false; } tokenValues[i] = val; } if( vectorType == typeof( Vector3 ) ) { Vector3 result = new Vector3(); for( i = 0; i < tokenValues.Length && i < 3; i++ ) result[i] = tokenValues[i]; for( ; i < 3; i++ ) result[i] = 0; output = result; } else if( vectorType == typeof( Vector2 ) ) { Vector2 result = new Vector2(); for( i = 0; i < tokenValues.Length && i < 2; i++ ) result[i] = tokenValues[i]; for( ; i < 2; i++ ) result[i] = 0; output = result; } else { Vector4 result = new Vector4(); for( i = 0; i < tokenValues.Length && i < 4; i++ ) result[i] = tokenValues[i]; for( ; i < 4; i++ ) result[i] = 0; output = result; } return true; } } }