using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using FlaxEditor; using FlaxEngine; namespace Game; public struct ConsoleLine { public string content; internal ConsoleLine(string line) { content = line; } public static implicit operator string(ConsoleLine line) => line.content; public static explicit operator ConsoleLine(string line) => new ConsoleLine(line); public override string ToString() => content; } public static class Console { private static ConsoleInstance instance; // Returns if Console window open right now. public static bool IsOpen => instance.IsOpen; // For debugging only: Returns true when Console was not closed during the same frame. // Needed when Escape-key both closes the console and exits the game. public static bool IsSafeToQuit => instance.IsSafeToQuit; // Called when Console is opened. public static Action OnOpen { get => instance.OnOpen; set => instance.OnOpen = value; } // Called when Console is closed. public static Action OnClose { get => instance.OnClose; set => instance.OnClose = value; } // Called when a line of text was printed in Console. public static Action OnPrint { get => instance.OnPrint; set => instance.OnPrint = value; } public static bool ShowExecutedLines => instance.ShowExecutedLines; public static int DebugVerbosity { get => instance.DebugVerbosity; set => instance.DebugVerbosity = value; } public static string LinePrefix => instance.LinePrefix; public static ReadOnlySpan Lines => instance.Lines; public static void Init() { if (instance != null) return; Destroy(); instance = new ConsoleInstance(); instance.InitConsoleSubsystems(); #if FLAX_EDITOR ScriptsBuilder.ScriptsReload += Destroy; Editor.Instance.PlayModeEnd += OnEditorPlayModeChanged; #endif } public static void Destroy() { if (instance != null) { instance.Dispose(); instance = null; #if FLAX_EDITOR ScriptsBuilder.ScriptsReload -= Destroy; Editor.Instance.PlayModeEnd -= OnEditorPlayModeChanged; #endif } } #if FLAX_EDITOR private static void OnEditorPlayModeChanged() { //AssetManager.Globals.ResetValues(); // Clear console buffer when leaving play mode if (instance != null) Clear(); } #endif public static string GetBufferHistory(int index) => instance.GetBufferHistory(index); // Echoes text to Console public static void Print(string text) => instance.Print(text); // Echoes warning text to Console public static void PrintWarning(string text) => instance.PrintWarning(text); // Echoes error text to Console [DebuggerHidden] public static void PrintError(string text) => instance.PrintError(text); // Echoes developer/debug text to Console public static void PrintDebug(string text) => instance.PrintDebug(1, false, text); // Echoes developer/debug text to Console public static void PrintDebug(int verbosity, string text) => instance.PrintDebug(verbosity, false, text); // Echoes developer/debug text to Console public static void PrintDebug(int verbosity, bool noRepeat, string text) => instance.PrintDebug(verbosity, noRepeat, text); // Opens the Console public static void Open() => instance.Open(); // Closes the Console public static void Close() => instance.Close(); // Clears the content of the Console public static void Clear() => instance.Clear(); public static void Execute(string str, bool bufferInput = false, bool noOutput = false) => instance.Execute(str, bufferInput, noOutput); public static string GetVariable(string variableName) => instance.GetVariable(variableName); } public class ConsoleInstance : IDisposable { private readonly List consoleBufferHistory = new(); private readonly Dictionary consoleCommands = new(); private readonly List consoleLines = new(); private readonly Dictionary consoleVariables = new(); private readonly Stopwatch closeThrottleStopwatch = Stopwatch.StartNew(); private StreamWriter logStream; // Echoes developer/debug text to Console private string debugLastLine = ""; public Action OnClose; public Action OnOpen; public Action OnPrint; public bool ShowExecutedLines = true; internal ConsoleInstance() { // Try different filename when running multiple instances string logFilename = "console.log"; const int attempts = 16; for (int i = 0; i < attempts;) { try { #if FLAX_EDITOR logStream = new StreamWriter(Path.Combine(Globals.ProjectFolder, logFilename), false); #else logStream = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), logFilename), false); #endif } catch (Exception) { logFilename = $"console-{++i}.log"; continue; } break; } } public bool IsOpen { get; internal set; } = true; public bool IsSafeToQuit => closeThrottleStopwatch.Elapsed.TotalSeconds > 0.1; public int DebugVerbosity { get; set; } = 1; public string LinePrefix { get; internal set; } = "]"; public ReadOnlySpan Lines => CollectionsMarshal.AsSpan(consoleLines); public void Dispose() { if (logStream != null) { logStream.Flush(); logStream.Dispose(); logStream = null; } } // Initializes the Console system. internal void InitConsoleSubsystems() { #if USE_NETCORE var assemblies = Utils.GetAssemblies(); #else var assemblies = AppDomain.CurrentDomain.GetAssemblies(); #endif foreach (Assembly assembly in assemblies) { // Skip common assemblies string assemblyName = assembly.GetName().Name; if (assemblyName == "System" || assemblyName.StartsWith("System.") || assemblyName.StartsWith("Mono.") || assemblyName == "mscorlib" || assemblyName == "Newtonsoft.Json" || assemblyName.StartsWith("FlaxEngine.") || assemblyName.StartsWith("JetBrains.") || assemblyName.StartsWith("Microsoft.") || assemblyName.StartsWith("nunit.") || assemblyName == "Snippets" || assemblyName == "Anonymously Hosted DynamicMethods Assembly" || assemblyName == "netstandard") { continue; } foreach (Type type in assembly.GetTypes()) { var cmdParsed = new Dictionary(); var cmdMethods = new Dictionary>(); MethodInfo cmdInitializer = null; foreach (MethodInfo method in type.GetMethods()) { if (!method.IsStatic) continue; var attributes = Attribute.GetCustomAttributes(method); foreach (Attribute attr in attributes) { if (attr is ConsoleCommandAttribute cmdAttribute) { //Console.Print("found cmd '" + cmdAttribute.name + "' bound to field '" + method.Name + "'"); // Defer constructing the command until we have parsed all the methods for it in this assembly. List methods; if (!cmdMethods.TryGetValue(cmdAttribute.name, out methods)) { methods = new List(); cmdMethods.Add(cmdAttribute.name, methods); } methods.Add(method); ConsoleCommand cmd = new ConsoleCommand(cmdAttribute.name, null); if (!cmdParsed.ContainsKey(cmdAttribute.name)) cmdParsed.Add(cmdAttribute.name, cmd); foreach (string alias in cmdAttribute.aliases) { if (!cmdParsed.ContainsKey(alias)) cmdParsed.Add(alias, cmd); List aliasMethods; if (!cmdMethods.TryGetValue(alias, out aliasMethods)) { aliasMethods = new List(); cmdMethods.Add(alias, aliasMethods); } aliasMethods.Add(method); } } else if (attr is ConsoleSubsystemInitializer) { cmdInitializer = method; } } } foreach (var kv in cmdParsed) { var methods = cmdMethods[kv.Key]; ConsoleCommand definition = kv.Value; ConsoleCommand cmd = new ConsoleCommand(definition.name, methods.ToArray()); consoleCommands.Add(kv.Key, cmd); } foreach (FieldInfo field in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { if (!field.IsStatic) continue; var attributes = Attribute.GetCustomAttributes(field); foreach (Attribute attr in attributes) { if (attr is ConsoleVariableAttribute cvarAttribute) { //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); if (!consoleVariables.TryGetValue(cvarAttribute.name, out var entry)) { entry.cvar = new ConsoleVariable(cvarAttribute.name); consoleVariables.Add(cvarAttribute.name, entry); } entry.cvar.AddField(field); foreach (string alias in cvarAttribute.aliases) consoleVariables.Add(alias, (entry.cvar, true)); } } } foreach (PropertyInfo property in type.GetProperties(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { var attributes = Attribute.GetCustomAttributes(property); foreach (Attribute attr in attributes) { if (attr is ConsoleVariableAttribute cvarAttribute) { MethodInfo getter = property.GetGetMethod() ?? property.GetMethod; MethodInfo setter = property.GetSetMethod() ?? property.SetMethod; if (getter == null || setter == null || !getter.IsStatic || !setter.IsStatic) continue; //Console.Print("found cvar '" + cvarAttribute.name + "' bound to field '" + field.Name + "'"); if (!consoleVariables.TryGetValue(cvarAttribute.name, out var entry)) { entry.cvar = new ConsoleVariable(cvarAttribute.name); consoleVariables.Add(cvarAttribute.name, entry); } entry.cvar.AddProperty(property); foreach (string alias in cvarAttribute.aliases) consoleVariables.Add(alias, (entry.cvar, true)); } } } if (cmdInitializer != null) { Console.PrintDebug(2, "Initializing " + type.Name); cmdInitializer.Invoke(null, null); } } } } public string GetBufferHistory(int index) { if (consoleBufferHistory.Count == 0) return null; if (index > consoleBufferHistory.Count - 1 || index < 0) return null; return consoleBufferHistory[index]; } // Echoes text to Console public void Print(string text) { debugLastLine = text; PrintLine(text); } // Echoes warning text to Console public void PrintWarning(string text) => PrintLine(text); // Echoes error text to Console //[DebuggerNonUserCode] [DebuggerHidden] public void PrintError(string text) { PrintLine(text); /*if (Debugger.IsAttached) Debugger.Break(); else*/ throw new Exception(text); } private void PrintLine(string text) { if (text.IndexOf('\n') != -1) { // Avoid generating extra garbage in single-line cases foreach (string line in text.Split('\n')) { ConsoleLine lineEntry = new ConsoleLine(line); consoleLines.Add(lineEntry); logStream.WriteLine(line); OnPrint?.Invoke(text); } } else { ConsoleLine lineEntry = new ConsoleLine(text); consoleLines.Add(lineEntry); logStream.WriteLine(text); OnPrint?.Invoke(text); } logStream.Flush(); if (Debugger.IsAttached) System.Diagnostics.Debug.WriteLine(text); } public void PrintDebug(int verbosity, bool noRepeat, string text) { if (DebugVerbosity < verbosity) return; if (noRepeat) if (debugLastLine.Length == text.Length && debugLastLine == text) return; debugLastLine = text; PrintLine(text); } // Opens the Console public void Open() { if (IsOpen) return; IsOpen = true; OnOpen?.Invoke(); } // Closes the Console public void Close() { if (!IsOpen) return; IsOpen = false; OnClose?.Invoke(); closeThrottleStopwatch.Restart(); } // Clears the content of the Console public void Clear() { consoleLines.Clear(); } public void Execute(string str, bool bufferInput = false, bool noOutput = false) { if (bufferInput) consoleBufferHistory.Insert(0, str); str = str.Trim(); if (ShowExecutedLines && !noOutput) Console.Print(LinePrefix + str); string[] strs = str.Split(' '); string execute = strs[0]; string executeLower = execute.ToLowerInvariant(); string value = strs.Length > 1 ? str.Substring(execute.Length + 1) : null; //Console.PrintDebug("Executed '" + execute + "' with params: '" + value + "'"); try { if (consoleCommands.TryGetValue(executeLower, out ConsoleCommand cmd)) { string[] values = strs.Skip(1).ToArray(); if (values.Length > 0) cmd.Invoke(values); else cmd.Invoke(); //Console.Print("Command bound to '" + execute + "' is '" + cmd.method.Name + "'"); } else if (consoleVariables.TryGetValue(executeLower, out var entry)) { if (value != null) entry.cvar.SetValue(value); if (!noOutput) Console.Print("'" + execute + "' is '" + entry.cvar.GetValueString() + "'"); } else { Console.Print("Unknown command '" + execute + "'"); } } catch (Exception e) { var message = e.InnerException != null ? e.InnerException.Message : e.Message; var exp = e.InnerException != null ? e.InnerException : e; Console.Print("Command failed: " + exp.ToString()); throw; } } public string GetVariable(string variableName) { if (consoleVariables.TryGetValue(variableName, out var entry)) { string value = entry.cvar.GetValueString(); return value; } return null; } }