#if UNITY_EDITOR || UNITY_STANDALONE // Unity's Text component doesn't render tag correctly on mobile devices #define USE_BOLD_COMMAND_SIGNATURES #endif using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Text; using UnityEngine; #if UNITY_EDITOR && UNITY_2021_1_OR_NEWER using SystemInfo = UnityEngine.Device.SystemInfo; // To support Device Simulator on Unity 2021.1+ #endif // 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 string command; public readonly object instance; public readonly MethodInfo method; public readonly string[] parameters; public readonly Type[] parameterTypes; public readonly string signature; public ConsoleMethodInfo(MethodInfo method, Type[] parameterTypes, object instance, string command, string signature, string[] parameters) { this.method = method; this.parameterTypes = parameterTypes; this.instance = instance; this.command = command; this.signature = signature; this.parameters = parameters; } 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 readonly List methods = new(); private static readonly List matchingMethods = new(4); // All the parse functions private static readonly Dictionary parseFunctions = new() { { 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(Quaternion), ParseQuaternion }, { typeof(Color), ParseColor }, { typeof(Color32), ParseColor32 }, { typeof(Rect), ParseRect }, { typeof(RectOffset), ParseRectOffset }, { typeof(Bounds), ParseBounds }, { typeof(GameObject), ParseGameObject }, #if UNITY_2017_2_OR_NEWER { typeof(Vector2Int), ParseVector2Int }, { typeof(Vector3Int), ParseVector3Int }, { typeof(RectInt), ParseRectInt }, { typeof(BoundsInt), ParseBoundsInt }, #endif }; // All the readable names of accepted types private static readonly Dictionary typeReadableNames = new() { { 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" } }; // Split arguments of an entered command private static readonly List commandArguments = new(8); // Command parameter delimeter groups private static readonly string[] inputDelimiters = { "\"\"", "''", "{}", "()", "[]" }; // CompareInfo used for case-insensitive command name comparison internal static readonly CompareInfo caseInsensitiveComparer = new CultureInfo("en-US").CompareInfo; static DebugLogConsole() { AddCommand("help", "Prints all commands", LogAllCommands); AddCommand("help", "Prints all matching commands", LogAllCommandsWithName); AddCommand("sysinfo", "Prints system information", LogSystemInfo); #if UNITY_EDITOR || !NETFX_CORE // Find all [ConsoleMethod] functions // Don't search built-in assemblies for console methods since they can't have any string[] ignoredAssemblies = { "Unity", "System", "Mono.", "mscorlib", "netstandard", "TextMeshPro", "Microsoft.GeneratedCode", "I18N", "Boo.", "UnityScript.", "ICSharpCode.", "ExCSS.Unity", #if UNITY_EDITOR "Assembly-CSharp-Editor", "Assembly-UnityScript-Editor", "nunit.", "SyntaxTree.", "AssetStoreTools", #endif }; #endif #if UNITY_EDITOR || !NETFX_CORE foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) #else foreach( Assembly assembly in new Assembly[] { typeof( DebugLogConsole ).Assembly } ) // On UWP, at least search this plugin's Assembly for console methods #endif { #if( NET_4_6 || NET_STANDARD_2_0 ) && ( UNITY_EDITOR || !NETFX_CORE ) if (assembly.IsDynamic) continue; #endif var assemblyName = assembly.GetName().Name; #if UNITY_EDITOR || !NETFX_CORE var ignoreAssembly = false; for (var i = 0; i < ignoredAssemblies.Length; i++) if (caseInsensitiveComparer.IsPrefix(assemblyName, ignoredAssemblies[i], CompareOptions.IgnoreCase)) { ignoreAssembly = true; break; } if (ignoreAssembly) continue; #endif try { 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)) { var consoleMethod = attribute as ConsoleMethodAttribute; if (consoleMethod != null) AddCommand(consoleMethod.Command, consoleMethod.Description, method, null, consoleMethod.ParameterNames); } } catch (NotSupportedException) { } catch (FileNotFoundException) { } catch (ReflectionTypeLoadException) { } catch (Exception e) { Debug.LogError( "Couldn't search assembly for [ConsoleMethod] attributes: " + assemblyName + "\n" + e); } } } // Logs the list of available commands public static void LogAllCommands() { var length = 25; for (var i = 0; i < methods.Count; i++) if (methods[i].IsValid()) length += methods[i].signature.Length + 7; var stringBuilder = new StringBuilder(length); stringBuilder.Append("Available commands:"); for (var i = 0; i < methods.Count; i++) if (methods[i].IsValid()) stringBuilder.Append("\n - ").Append(methods[i].signature); Debug.Log(stringBuilder.ToString()); // After typing help, the log that lists all the commands should automatically be expanded for better UX if (DebugLogManager.Instance) DebugLogManager.Instance.AdjustLatestPendingLog(true, true); } // Logs the list of available commands that are either equal to commandName or contain commandName as substring public static void LogAllCommandsWithName(string commandName) { matchingMethods.Clear(); // First, try to find commands that exactly match the commandName. If there are no such commands, try to find // commands that contain commandName as substring FindCommands(commandName, false, matchingMethods); if (matchingMethods.Count == 0) FindCommands(commandName, true, matchingMethods); if (matchingMethods.Count == 0) { Debug.LogWarning(string.Concat("ERROR: can't find command '", commandName, "'")); } else { var commandsLength = 25; for (var i = 0; i < matchingMethods.Count; i++) commandsLength += matchingMethods[i].signature.Length + 7; var stringBuilder = new StringBuilder(commandsLength); stringBuilder.Append("Matching commands:"); for (var i = 0; i < matchingMethods.Count; i++) stringBuilder.Append("\n - ").Append(matchingMethods[i].signature); Debug.Log(stringBuilder.ToString()); if (DebugLogManager.Instance) DebugLogManager.Instance.AdjustLatestPendingLog(true, true); } } // Logs system information public static void LogSystemInfo() { var 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"); #if UNITY_5_6_OR_NEWER stringBuilder.Append("Max Cubemap Size: ").Append(SystemInfo.maxCubemapSize).Append("\n"); #endif 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"); #if UNITY_5_6_OR_NEWER stringBuilder.Append("3D Render Textures: ") .Append(SystemInfo.supports3DRenderTextures ? "supported\n" : "not supported\n"); #endif 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.ToString()); // After typing sysinfo, the log that lists system information should automatically be expanded for better UX if (DebugLogManager.Instance) DebugLogManager.Instance.AdjustLatestPendingLog(true, true); } 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 custom Type to the list of recognized command parameter Types public static void AddCustomParameterType(Type type, ParseFunction parseFunction, string typeReadableName = null) { if (type == null) { Debug.LogError("Parameter type can't be null!"); return; } if (parseFunction == null) { Debug.LogError("Parameter parseFunction can't be null!"); return; } parseFunctions[type] = parseFunction; if (!string.IsNullOrEmpty(typeReadableName)) typeReadableNames[type] = typeReadableName; } // Remove a custom Type from the list of recognized command parameter Types public static void RemoveCustomParameterType(Type type) { parseFunctions.Remove(type); typeReadableNames.Remove(type); } // 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, params string[] parameterNames) { if (instance == null) { Debug.LogError("Instance can't be null!"); return; } AddCommand(command, description, methodName, instance.GetType(), instance, parameterNames); } // 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, params string[] parameterNames) { AddCommand(command, description, methodName, ownerType, null, parameterNames); } // Add a command that can be related to either a static or an instance method public static void AddCommand(string command, string description, Action method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Action method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Func method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Action method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Func method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Action method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Func method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Action method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Func method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Func method) { AddCommand(command, description, method.Method, method.Target, null); } public static void AddCommand(string command, string description, Delegate method) { AddCommand(command, description, method.Method, method.Target, null); } // Add a command with custom parameter names public static void AddCommand(string command, string description, Action method, string parameterName) { AddCommand(command, description, method.Method, method.Target, new string[1] { parameterName }); } public static void AddCommand(string command, string description, Action method, string parameterName1, string parameterName2) { AddCommand(command, description, method.Method, method.Target, new string[2] { parameterName1, parameterName2 }); } public static void AddCommand(string command, string description, Func method, string parameterName) { AddCommand(command, description, method.Method, method.Target, new string[1] { parameterName }); } public static void AddCommand(string command, string description, Action method, string parameterName1, string parameterName2, string parameterName3) { AddCommand(command, description, method.Method, method.Target, new string[3] { parameterName1, parameterName2, parameterName3 }); } public static void AddCommand(string command, string description, Func method, string parameterName1, string parameterName2) { AddCommand(command, description, method.Method, method.Target, new string[2] { parameterName1, parameterName2 }); } public static void AddCommand(string command, string description, Action method, string parameterName1, string parameterName2, string parameterName3, string parameterName4) { AddCommand(command, description, method.Method, method.Target, new string[4] { parameterName1, parameterName2, parameterName3, parameterName4 }); } public static void AddCommand(string command, string description, Func method, string parameterName1, string parameterName2, string parameterName3) { AddCommand(command, description, method.Method, method.Target, new string[3] { parameterName1, parameterName2, parameterName3 }); } public static void AddCommand(string command, string description, Func method, string parameterName1, string parameterName2, string parameterName3, string parameterName4) { AddCommand(command, description, method.Method, method.Target, new string[4] { parameterName1, parameterName2, parameterName3, parameterName4 }); } public static void AddCommand(string command, string description, Delegate method, params string[] parameterNames) { AddCommand(command, description, method.Method, method.Target, parameterNames); } // Create a new command and set its properties private static void AddCommand(string command, string description, string methodName, Type ownerType, object instance, string[] parameterNames) { // Get the method from the class var method = ownerType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | (instance != null ? BindingFlags.Instance : BindingFlags.Static)); if (method == null) { Debug.LogError(methodName + " does not exist in " + ownerType); return; } AddCommand(command, description, method, instance, parameterNames); } private static void AddCommand(string command, string description, MethodInfo method, object instance, string[] parameterNames) { 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; } // Fetch the parameters of the class var parameters = method.GetParameters(); if (parameters == null) parameters = new ParameterInfo[0]; // Store the parameter types in an array var parameterTypes = new Type[parameters.Length]; for (var i = 0; i < parameters.Length; i++) { if (parameters[i].ParameterType.IsByRef) { Debug.LogError("Command can't have 'out' or 'ref' parameters"); return; } var parameterType = parameters[i].ParameterType; if (parseFunctions.ContainsKey(parameterType) || typeof(Component).IsAssignableFrom(parameterType) || parameterType.IsEnum || IsSupportedArrayType(parameterType)) { parameterTypes[i] = parameterType; } else { Debug.LogError(string.Concat("Parameter ", parameters[i].Name, "'s Type ", parameterType, " isn't supported")); return; } } var commandIndex = FindCommandIndex(command); if (commandIndex < 0) { commandIndex = ~commandIndex; } else { var commandFirstIndex = commandIndex; var commandLastIndex = commandIndex; while (commandFirstIndex > 0 && caseInsensitiveComparer.Compare(methods[commandFirstIndex - 1].command, command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandFirstIndex--; while (commandLastIndex < methods.Count - 1 && caseInsensitiveComparer.Compare( methods[commandLastIndex + 1].command, command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandLastIndex++; commandIndex = commandFirstIndex; for (var i = commandFirstIndex; i <= commandLastIndex; i++) { var parameterCountDiff = methods[i].parameterTypes.Length - parameterTypes.Length; if (parameterCountDiff <= 0) { // We are sorting the commands in 2 steps: // 1: Sorting by their 'command' names which is handled by FindCommandIndex // 2: Sorting by their parameter counts which is handled here (parameterCountDiff <= 0) commandIndex = i + 1; // Check if this command has been registered before and if it is, overwrite that command if (parameterCountDiff == 0) { var j = 0; while (j < parameterTypes.Length && parameterTypes[j] == methods[i].parameterTypes[j]) j++; if (j >= parameterTypes.Length) { commandIndex = i; commandLastIndex--; methods.RemoveAt(i--); } } } } } // Create the command var methodSignature = new StringBuilder(256); var parameterSignatures = new string[parameterTypes.Length]; #if USE_BOLD_COMMAND_SIGNATURES methodSignature.Append(""); #endif methodSignature.Append(command); if (parameterTypes.Length > 0) { methodSignature.Append(" "); for (var i = 0; i < parameterTypes.Length; i++) { var parameterSignatureStartIndex = methodSignature.Length; methodSignature.Append("[").Append(GetTypeReadableName(parameterTypes[i])).Append(" ").Append( parameterNames != null && i < parameterNames.Length && !string.IsNullOrEmpty(parameterNames[i]) ? parameterNames[i] : parameters[i].Name).Append("]"); if (i < parameterTypes.Length - 1) methodSignature.Append(" "); parameterSignatures[i] = methodSignature.ToString(parameterSignatureStartIndex, methodSignature.Length - parameterSignatureStartIndex); } } #if USE_BOLD_COMMAND_SIGNATURES methodSignature.Append(""); #endif if (!string.IsNullOrEmpty(description)) methodSignature.Append(": ").Append(description); methods.Insert(commandIndex, new ConsoleMethodInfo(method, parameterTypes, instance, command, methodSignature.ToString(), parameterSignatures)); } // Remove all commands with the matching command name from the console public static void RemoveCommand(string command) { if (!string.IsNullOrEmpty(command)) for (var i = methods.Count - 1; i >= 0; i--) if (caseInsensitiveComparer.Compare(methods[i].command, command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) methods.RemoveAt(i); } // Remove all commands with the matching method from the console public static void RemoveCommand(Action method) { RemoveCommand(method.Method); } public static void RemoveCommand(Action method) { RemoveCommand(method.Method); } public static void RemoveCommand(Func method) { RemoveCommand(method.Method); } public static void RemoveCommand(Action method) { RemoveCommand(method.Method); } public static void RemoveCommand(Func method) { RemoveCommand(method.Method); } public static void RemoveCommand(Action method) { RemoveCommand(method.Method); } public static void RemoveCommand(Func method) { RemoveCommand(method.Method); } public static void RemoveCommand(Action method) { RemoveCommand(method.Method); } public static void RemoveCommand(Func method) { RemoveCommand(method.Method); } public static void RemoveCommand(Func method) { RemoveCommand(method.Method); } public static void RemoveCommand(Delegate method) { RemoveCommand(method.Method); } public static void RemoveCommand(MethodInfo method) { if (method != null) for (var i = methods.Count - 1; i >= 0; i--) if (methods[i].method == method) methods.RemoveAt(i); } // Returns the first command that starts with the entered argument public static string GetAutoCompleteCommand(string commandStart, string previousSuggestion) { var commandIndex = FindCommandIndex(!string.IsNullOrEmpty(previousSuggestion) ? previousSuggestion : commandStart); if (commandIndex < 0) { commandIndex = ~commandIndex; return commandIndex < methods.Count && caseInsensitiveComparer.IsPrefix(methods[commandIndex].command, commandStart, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) ? methods[commandIndex].command : null; } // Find the next command that starts with commandStart and is different from previousSuggestion for (var i = commandIndex + 1; i < methods.Count; i++) if (caseInsensitiveComparer.Compare(methods[i].command, previousSuggestion, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) continue; else if (caseInsensitiveComparer.IsPrefix(methods[i].command, commandStart, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)) return methods[i].command; else break; // Couldn't find a command that follows previousSuggestion and satisfies commandStart, loop back to the beginning of the autocomplete suggestions string result = null; for (var i = commandIndex - 1; i >= 0 && caseInsensitiveComparer.IsPrefix(methods[i].command, commandStart, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace); i--) result = methods[i].command; return result; } // 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; // Split the command's arguments commandArguments.Clear(); FetchArgumentsFromCommand(command, commandArguments); // Find all matching commands matchingMethods.Clear(); var parameterCountMismatch = false; var commandIndex = FindCommandIndex(commandArguments[0]); if (commandIndex >= 0) { var _command = commandArguments[0]; var commandLastIndex = commandIndex; while (commandIndex > 0 && caseInsensitiveComparer.Compare(methods[commandIndex - 1].command, _command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandIndex--; while (commandLastIndex < methods.Count - 1 && caseInsensitiveComparer.Compare( methods[commandLastIndex + 1].command, _command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandLastIndex++; while (commandIndex <= commandLastIndex) if (!methods[commandIndex].IsValid()) { methods.RemoveAt(commandIndex); commandLastIndex--; } else { // Check if number of parameters match if (methods[commandIndex].parameterTypes.Length == commandArguments.Count - 1) matchingMethods.Add(methods[commandIndex]); else parameterCountMismatch = true; commandIndex++; } } if (matchingMethods.Count == 0) { var _command = commandArguments[0]; FindCommands(_command, !parameterCountMismatch, matchingMethods); if (matchingMethods.Count == 0) { Debug.LogWarning(string.Concat("ERROR: can't find command '", _command, "'")); } else { var commandsLength = _command.Length + 75; for (var i = 0; i < matchingMethods.Count; i++) commandsLength += matchingMethods[i].signature.Length + 7; var stringBuilder = new StringBuilder(commandsLength); if (parameterCountMismatch) stringBuilder.Append("ERROR: '").Append(_command).Append("' doesn't take ") .Append(commandArguments.Count - 1).Append(" parameter(s). Available command(s):"); else stringBuilder.Append("ERROR: can't find command '").Append(_command).Append("'. Did you mean:"); for (var i = 0; i < matchingMethods.Count; i++) stringBuilder.Append("\n - ").Append(matchingMethods[i].signature); Debug.LogWarning(stringBuilder.ToString()); // The log that lists method signature(s) for this command should automatically be expanded for better UX if (DebugLogManager.Instance) DebugLogManager.Instance.AdjustLatestPendingLog(true, true); } return; } ConsoleMethodInfo methodToExecute = null; var parameters = new object[commandArguments.Count - 1]; string errorMessage = null; for (var i = 0; i < matchingMethods.Count && methodToExecute == null; i++) { var methodInfo = matchingMethods[i]; // Parse the parameters into objects var success = true; for (var j = 0; j < methodInfo.parameterTypes.Length && success; j++) try { var argument = commandArguments[j + 1]; var parameterType = methodInfo.parameterTypes[j]; object val; if (ParseArgument(argument, parameterType, out val)) { parameters[j] = val; } else { success = false; errorMessage = string.Concat("ERROR: couldn't parse ", argument, " to ", GetTypeReadableName(parameterType)); } } catch (Exception e) { success = false; errorMessage = "ERROR: " + e; } if (success) methodToExecute = methodInfo; } if (methodToExecute == null) { Debug.LogWarning(!string.IsNullOrEmpty(errorMessage) ? errorMessage : "ERROR: something went wrong"); } else { // Execute the method associated with the command var result = methodToExecute.method.Invoke(methodToExecute.instance, parameters); if (methodToExecute.method.ReturnType != typeof(void)) { // Print the returned value to the console if (result == null || result.Equals(null)) Debug.Log("Returned: null"); else Debug.Log("Returned: " + result); } } } public static void FetchArgumentsFromCommand(string command, List commandArguments) { for (var i = 0; i < command.Length; i++) { if (char.IsWhiteSpace(command[i])) continue; var delimiterIndex = IndexOfDelimiterGroup(command[i]); if (delimiterIndex >= 0) { var endIndex = IndexOfDelimiterGroupEnd(command, delimiterIndex, i + 1); commandArguments.Add(command.Substring(i + 1, endIndex - i - 1)); i = endIndex < command.Length - 1 && command[endIndex + 1] == ',' ? endIndex + 1 : endIndex; } else { var endIndex = IndexOfChar(command, ' ', i + 1); commandArguments.Add(command.Substring(i, command[endIndex - 1] == ',' ? endIndex - 1 - i : endIndex - i)); i = endIndex; } } } public static void FindCommands(string commandName, bool allowSubstringMatching, List matchingCommands) { if (allowSubstringMatching) { for (var i = 0; i < methods.Count; i++) if (methods[i].IsValid() && caseInsensitiveComparer.IndexOf(methods[i].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0) matchingCommands.Add(methods[i]); } else { for (var i = 0; i < methods.Count; i++) if (methods[i].IsValid() && caseInsensitiveComparer.Compare(methods[i].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) matchingCommands.Add(methods[i]); } } // Finds all commands that have a matching signature with command // - caretIndexIncrements: indices inside "string command" that separate two arguments in the command. This is used to // figure out which argument the caret is standing on // - commandName: command's name (first argument) internal static void GetCommandSuggestions(string command, List matchingCommands, List caretIndexIncrements, ref string commandName, out int numberOfParameters) { var commandNameCalculated = false; var commandNameFullyTyped = false; numberOfParameters = -1; for (var i = 0; i < command.Length; i++) { if (char.IsWhiteSpace(command[i])) continue; var delimiterIndex = IndexOfDelimiterGroup(command[i]); if (delimiterIndex >= 0) { var endIndex = IndexOfDelimiterGroupEnd(command, delimiterIndex, i + 1); if (!commandNameCalculated) { commandNameCalculated = true; commandNameFullyTyped = command.Length > endIndex; var commandNameLength = endIndex - i - 1; if (commandName == null || commandNameLength == 0 || commandName.Length != commandNameLength || caseInsensitiveComparer.IndexOf(command, commandName, i + 1, commandNameLength, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) != i + 1) commandName = command.Substring(i + 1, commandNameLength); } i = endIndex < command.Length - 1 && command[endIndex + 1] == ',' ? endIndex + 1 : endIndex; caretIndexIncrements.Add(i + 1); } else { var endIndex = IndexOfChar(command, ' ', i + 1); if (!commandNameCalculated) { commandNameCalculated = true; commandNameFullyTyped = command.Length > endIndex; var commandNameLength = command[endIndex - 1] == ',' ? endIndex - 1 - i : endIndex - i; if (commandName == null || commandNameLength == 0 || commandName.Length != commandNameLength || caseInsensitiveComparer.IndexOf(command, commandName, i, commandNameLength, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) != i) commandName = command.Substring(i, commandNameLength); } i = endIndex; caretIndexIncrements.Add(i); } numberOfParameters++; } if (!commandNameCalculated) commandName = string.Empty; if (!string.IsNullOrEmpty(commandName)) { var commandIndex = FindCommandIndex(commandName); if (commandIndex < 0) commandIndex = ~commandIndex; var commandLastIndex = commandIndex; if (!commandNameFullyTyped) { // Match all commands that start with commandName if (commandIndex < methods.Count && caseInsensitiveComparer.IsPrefix(methods[commandIndex].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)) { while (commandIndex > 0 && caseInsensitiveComparer.IsPrefix(methods[commandIndex - 1].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)) commandIndex--; while (commandLastIndex < methods.Count - 1 && caseInsensitiveComparer.IsPrefix( methods[commandLastIndex + 1].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace)) commandLastIndex++; } else { commandLastIndex = -1; } } else { // Match only the commands that are equal to commandName if (commandIndex < methods.Count && caseInsensitiveComparer.Compare(methods[commandIndex].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) { while (commandIndex > 0 && caseInsensitiveComparer.Compare(methods[commandIndex - 1].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandIndex--; while (commandLastIndex < methods.Count - 1 && caseInsensitiveComparer.Compare(methods[commandLastIndex + 1].command, commandName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0) commandLastIndex++; } else { commandLastIndex = -1; } } for (; commandIndex <= commandLastIndex; commandIndex++) if (methods[commandIndex].parameterTypes.Length >= numberOfParameters) matchingCommands.Add(methods[commandIndex]); } } // Find the index of the delimiter group that 'c' belongs to private static int IndexOfDelimiterGroup(char c) { for (var i = 0; i < inputDelimiters.Length; i++) if (c == inputDelimiters[i][0]) return i; return -1; } private static int IndexOfDelimiterGroupEnd(string command, int delimiterIndex, int startIndex) { var startChar = inputDelimiters[delimiterIndex][0]; var endChar = inputDelimiters[delimiterIndex][1]; // Check delimiter's depth for array support (e.g. [[1 2] [3 4]] for Vector2 array) var depth = 1; for (var i = startIndex; i < command.Length; i++) { var c = command[i]; if (c == endChar && --depth <= 0) return i; if (c == startChar) depth++; } return command.Length; } // 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) { var result = command.IndexOf(c, startIndex); if (result < 0) result = command.Length; return result; } // Find command's index in the list of registered commands using binary search private static int FindCommandIndex(string command) { var min = 0; var max = methods.Count - 1; while (min <= max) { var mid = (min + max) / 2; var comparison = caseInsensitiveComparer.Compare(command, methods[mid].command, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace); if (comparison == 0) return mid; if (comparison < 0) max = mid - 1; else min = mid + 1; } return ~min; } public static bool IsSupportedArrayType(Type type) { if (type.IsArray) { if (type.GetArrayRank() != 1) return false; type = type.GetElementType(); } else if (type.IsGenericType) { if (type.GetGenericTypeDefinition() != typeof(List<>)) return false; type = type.GetGenericArguments()[0]; } else { return false; } return parseFunctions.ContainsKey(type) || typeof(Component).IsAssignableFrom(type) || type.IsEnum; } public static string GetTypeReadableName(Type type) { string result; if (typeReadableNames.TryGetValue(type, out result)) return result; if (IsSupportedArrayType(type)) { var elementType = type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]; if (typeReadableNames.TryGetValue(elementType, out result)) return result + "[]"; return elementType.Name + "[]"; } return type.Name; } public static bool ParseArgument(string input, Type argumentType, out object output) { ParseFunction parseFunction; if (parseFunctions.TryGetValue(argumentType, out parseFunction)) return parseFunction(input, out output); if (typeof(Component).IsAssignableFrom(argumentType)) return ParseComponent(input, argumentType, out output); if (argumentType.IsEnum) return ParseEnum(input, argumentType, out output); if (IsSupportedArrayType(argumentType)) return ParseArray(input, argumentType, out output); output = null; return false; } public static bool ParseString(string input, out object output) { output = input; return true; } public 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; } public static bool ParseInt(string input, out object output) { int value; var result = int.TryParse(input, out value); output = value; return result; } public static bool ParseUInt(string input, out object output) { uint value; var result = uint.TryParse(input, out value); output = value; return result; } public static bool ParseLong(string input, out object output) { long value; var result = long.TryParse( !input.EndsWith("L", StringComparison.OrdinalIgnoreCase) ? input : input.Substring(0, input.Length - 1), out value); output = value; return result; } public static bool ParseULong(string input, out object output) { ulong value; var result = ulong.TryParse( !input.EndsWith("L", StringComparison.OrdinalIgnoreCase) ? input : input.Substring(0, input.Length - 1), out value); output = value; return result; } public static bool ParseByte(string input, out object output) { byte value; var result = byte.TryParse(input, out value); output = value; return result; } public static bool ParseSByte(string input, out object output) { sbyte value; var result = sbyte.TryParse(input, out value); output = value; return result; } public static bool ParseShort(string input, out object output) { short value; var result = short.TryParse(input, out value); output = value; return result; } public static bool ParseUShort(string input, out object output) { ushort value; var result = ushort.TryParse(input, out value); output = value; return result; } public static bool ParseChar(string input, out object output) { char value; var result = char.TryParse(input, out value); output = value; return result; } public static bool ParseFloat(string input, out object output) { float value; var result = float.TryParse( !input.EndsWith("f", StringComparison.OrdinalIgnoreCase) ? input : input.Substring(0, input.Length - 1), out value); output = value; return result; } public static bool ParseDouble(string input, out object output) { double value; var result = double.TryParse( !input.EndsWith("f", StringComparison.OrdinalIgnoreCase) ? input : input.Substring(0, input.Length - 1), out value); output = value; return result; } public static bool ParseDecimal(string input, out object output) { decimal value; var result = decimal.TryParse( !input.EndsWith("f", StringComparison.OrdinalIgnoreCase) ? input : input.Substring(0, input.Length - 1), out value); output = value; return result; } public static bool ParseVector2(string input, out object output) { return ParseVector(input, typeof(Vector2), out output); } public static bool ParseVector3(string input, out object output) { return ParseVector(input, typeof(Vector3), out output); } public static bool ParseVector4(string input, out object output) { return ParseVector(input, typeof(Vector4), out output); } public static bool ParseQuaternion(string input, out object output) { return ParseVector(input, typeof(Quaternion), out output); } public static bool ParseColor(string input, out object output) { return ParseVector(input, typeof(Color), out output); } public static bool ParseColor32(string input, out object output) { return ParseVector(input, typeof(Color32), out output); } public static bool ParseRect(string input, out object output) { return ParseVector(input, typeof(Rect), out output); } public static bool ParseRectOffset(string input, out object output) { return ParseVector(input, typeof(RectOffset), out output); } public static bool ParseBounds(string input, out object output) { return ParseVector(input, typeof(Bounds), out output); } public static bool ParseGameObject(string input, out object output) { output = input == "null" ? null : GameObject.Find(input); return true; } public static bool ParseComponent(string input, Type componentType, out object output) { var gameObject = input == "null" ? null : GameObject.Find(input); output = gameObject ? gameObject.GetComponent(componentType) : null; return true; } public static bool ParseEnum(string input, Type enumType, out object output) { const int NONE = 0, OR = 1, AND = 2; var outputInt = 0; var operation = NONE; // 0: nothing, 1: OR with outputInt, 2: AND with outputInt for (var i = 0; i < input.Length; i++) { string enumStr; var orIndex = input.IndexOf('|', i); var andIndex = input.IndexOf('&', i); if (orIndex < 0) enumStr = input.Substring(i, (andIndex < 0 ? input.Length : andIndex) - i).Trim(); else enumStr = input.Substring(i, (andIndex < 0 ? orIndex : Mathf.Min(andIndex, orIndex)) - i).Trim(); int value; if (!int.TryParse(enumStr, out value)) try { // Case-insensitive enum parsing value = Convert.ToInt32(Enum.Parse(enumType, enumStr, true)); } catch { output = null; return false; } if (operation == NONE) outputInt = value; else if (operation == OR) outputInt |= value; else outputInt &= value; if (orIndex >= 0) { if (andIndex > orIndex) { operation = AND; i = andIndex; } else { operation = OR; i = orIndex; } } else if (andIndex >= 0) { operation = AND; i = andIndex; } else { i = input.Length; } } output = Enum.ToObject(enumType, outputInt); return true; } public static bool ParseArray(string input, Type arrayType, out object output) { var valuesToParse = new List(2); FetchArgumentsFromCommand(input, valuesToParse); var result = (IList)Activator.CreateInstance(arrayType, valuesToParse.Count); output = result; if (arrayType.IsArray) { var elementType = arrayType.GetElementType(); for (var i = 0; i < valuesToParse.Count; i++) { object obj; if (!ParseArgument(valuesToParse[i], elementType, out obj)) return false; result[i] = obj; } } else { var elementType = arrayType.GetGenericArguments()[0]; for (var i = 0; i < valuesToParse.Count; i++) { object obj; if (!ParseArgument(valuesToParse[i], elementType, out obj)) return false; result.Add(obj); } } return true; } // Create a vector of specified type (fill the blank slots with 0 or ignore unnecessary slots) private static bool ParseVector(string input, Type vectorType, out object output) { var tokens = new List(input.Replace(',', ' ').Trim().Split(' ')); for (var i = tokens.Count - 1; i >= 0; i--) { tokens[i] = tokens[i].Trim(); if (tokens[i].Length == 0) tokens.RemoveAt(i); } var tokenValues = new float[tokens.Count]; for (var i = 0; i < tokens.Count; i++) { object val; if (!ParseFloat(tokens[i], out val)) { if (vectorType == typeof(Vector3)) output = Vector3.zero; else if (vectorType == typeof(Vector2)) output = Vector2.zero; else output = Vector4.zero; return false; } tokenValues[i] = (float)val; } if (vectorType == typeof(Vector3)) { var result = Vector3.zero; for (var i = 0; i < tokenValues.Length && i < 3; i++) result[i] = tokenValues[i]; output = result; } else if (vectorType == typeof(Vector2)) { var result = Vector2.zero; for (var i = 0; i < tokenValues.Length && i < 2; i++) result[i] = tokenValues[i]; output = result; } else if (vectorType == typeof(Vector4)) { var result = Vector4.zero; for (var i = 0; i < tokenValues.Length && i < 4; i++) result[i] = tokenValues[i]; output = result; } else if (vectorType == typeof(Quaternion)) { var result = Quaternion.identity; for (var i = 0; i < tokenValues.Length && i < 4; i++) result[i] = tokenValues[i]; output = result; } else if (vectorType == typeof(Color)) { var result = Color.black; for (var i = 0; i < tokenValues.Length && i < 4; i++) result[i] = tokenValues[i]; output = result; } else if (vectorType == typeof(Color32)) { var result = new Color32(0, 0, 0, 255); if (tokenValues.Length > 0) result.r = (byte)Mathf.RoundToInt(tokenValues[0]); if (tokenValues.Length > 1) result.g = (byte)Mathf.RoundToInt(tokenValues[1]); if (tokenValues.Length > 2) result.b = (byte)Mathf.RoundToInt(tokenValues[2]); if (tokenValues.Length > 3) result.a = (byte)Mathf.RoundToInt(tokenValues[3]); output = result; } else if (vectorType == typeof(Rect)) { var result = Rect.zero; if (tokenValues.Length > 0) result.x = tokenValues[0]; if (tokenValues.Length > 1) result.y = tokenValues[1]; if (tokenValues.Length > 2) result.width = tokenValues[2]; if (tokenValues.Length > 3) result.height = tokenValues[3]; output = result; } else if (vectorType == typeof(RectOffset)) { var result = new RectOffset(); if (tokenValues.Length > 0) result.left = Mathf.RoundToInt(tokenValues[0]); if (tokenValues.Length > 1) result.right = Mathf.RoundToInt(tokenValues[1]); if (tokenValues.Length > 2) result.top = Mathf.RoundToInt(tokenValues[2]); if (tokenValues.Length > 3) result.bottom = Mathf.RoundToInt(tokenValues[3]); output = result; } else if (vectorType == typeof(Bounds)) { var center = Vector3.zero; for (var i = 0; i < tokenValues.Length && i < 3; i++) center[i] = tokenValues[i]; var size = Vector3.zero; for (var i = 3; i < tokenValues.Length && i < 6; i++) size[i - 3] = tokenValues[i]; output = new Bounds(center, size); } #if UNITY_2017_2_OR_NEWER else if (vectorType == typeof(Vector3Int)) { var result = Vector3Int.zero; for (var i = 0; i < tokenValues.Length && i < 3; i++) result[i] = Mathf.RoundToInt(tokenValues[i]); output = result; } else if (vectorType == typeof(Vector2Int)) { var result = Vector2Int.zero; for (var i = 0; i < tokenValues.Length && i < 2; i++) result[i] = Mathf.RoundToInt(tokenValues[i]); output = result; } else if (vectorType == typeof(RectInt)) { var result = new RectInt(); if (tokenValues.Length > 0) result.x = Mathf.RoundToInt(tokenValues[0]); if (tokenValues.Length > 1) result.y = Mathf.RoundToInt(tokenValues[1]); if (tokenValues.Length > 2) result.width = Mathf.RoundToInt(tokenValues[2]); if (tokenValues.Length > 3) result.height = Mathf.RoundToInt(tokenValues[3]); output = result; } else if (vectorType == typeof(BoundsInt)) { var center = Vector3Int.zero; for (var i = 0; i < tokenValues.Length && i < 3; i++) center[i] = Mathf.RoundToInt(tokenValues[i]); var size = Vector3Int.zero; for (var i = 3; i < tokenValues.Length && i < 6; i++) size[i - 3] = Mathf.RoundToInt(tokenValues[i]); output = new BoundsInt(center, size); } #endif else { output = null; return false; } return true; } #if UNITY_2017_2_OR_NEWER public static bool ParseVector2Int(string input, out object output) { return ParseVector(input, typeof(Vector2Int), out output); } public static bool ParseVector3Int(string input, out object output) { return ParseVector(input, typeof(Vector3Int), out output); } public static bool ParseRectInt(string input, out object output) { return ParseVector(input, typeof(RectInt), out output); } public static bool ParseBoundsInt(string input, out object output) { return ParseVector(input, typeof(BoundsInt), out output); } #endif } }