using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Services.Console { [AttributeUsage(AttributeTargets.Method)] public class RCCMD : Attribute { public string name; public string description; public RCCMD(string name, string description) { this.name = name; this.description = description; } } public class ConsoleService : IService { static readonly Logger s_logger = new Logger(nameof(ConsoleService)); public struct ConsoleCommand { public string name; public string description; public Action action; } readonly List m_ConsoleCommands = new List(); FileStream m_LogFileStream; TextWriter m_LogFileWriter; bool m_IsLoading; public ConsoleService() { ConfigVar.StateChanged += OnCVarStateChanged; m_LogFileStream = new FileStream(Application.persistentDataPath + "/rr_logs.txt", FileMode.Append, FileAccess.Write); m_LogFileWriter = new StreamWriter(m_LogFileStream); m_LogFileWriter.WriteLine("============================"); m_LogFileWriter.WriteLine("Starting new log"); m_LogFileWriter.WriteLine($" > Game: {Application.productName}"); m_LogFileWriter.WriteLine($" > Version: {Application.version}"); m_LogFileWriter.WriteLine($" > Date: {DateTime.Now}"); m_LogFileWriter.WriteLine("============================"); m_LogFileWriter.Flush(); s_logger.Info("Waking up"); Load(); RegisterCommands(); } public void Dispose() { s_logger.Info("Shutting down"); ConfigVar.StateChanged -= OnCVarStateChanged; m_LogFileWriter.Dispose(); m_LogFileStream.Dispose(); m_LogFileStream = null; m_LogFileWriter = null; } void OnCVarStateChanged(ConfigVar cvar) { if (m_IsLoading) { return; } if (!cvar.flags.HasFlag(CVarFlags.ReadOnly)) { Save(); } } public event Action OnOutputMessage = _ => { }; public void WriteToOutput(string message) { if (m_LogFileWriter != null) { m_LogFileWriter.WriteLine(message); m_LogFileWriter.Flush(); } OnOutputMessage?.Invoke(message); } string[] ParseCommandInput(string text) { return text.Split(' '); } // @NOTE: Input must be in format: "command arg1 arg2 arg3", one command = one call public void Execute(string input) { if (input.Length == 0) { return; } string[] arguments = ParseCommandInput(input); if (arguments.Length == 0) { return; } string commandName = arguments[0]; foreach (ConsoleCommand command in m_ConsoleCommands) { if (command.name.Equals(commandName)) { command.action(arguments); return; } } foreach (ConfigVar cvar in ConfigVarsContainer.All()) { if (cvar.name.Equals(commandName)) { if (arguments.Length == 2) { cvar.ParseFromString(arguments[1]); } WriteToOutput($"{cvar.name} - {cvar}\n"); return; } } WriteToOutput($"ERROR: Command/CVar `{commandName}` not found."); } public void RegisterCommand(string name, string description, Action action) { if (IsCommandRegistered(name)) { s_logger.Error($"`{name}` command is already registered"); return; } m_ConsoleCommands.Add(new ConsoleCommand { name = name, description = description, action = action }); s_logger.Info($"Registered command: {name}"); } public static ConsoleCommand[] GenerateCommandsToRegister() { IEnumerable methods = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .SelectMany(type => type.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)) .Where(method => method.GetCustomAttributes(typeof(RCCMD), false).Length > 0); List commands = new List(); foreach (MethodInfo method in methods) { RCCMD attribute = (RCCMD)method.GetCustomAttributes(typeof(RCCMD), false)[0]; if (!method.IsStatic) { s_logger.Error($"Command `{attribute.name}` is not static, skipping"); continue; } if (method.GetParameters().Length != 1) { s_logger.Error($"Command `{attribute.name}` has invalid number of parameters, skipping"); continue; } if (method.GetParameters()[0].ParameterType != typeof(string[])) { s_logger.Error($"Command `{attribute.name}` has invalid parameter type, skipping"); continue; } Action action = (Action)Delegate.CreateDelegate(typeof(Action), method); commands.Add(new ConsoleCommand { name = attribute.name, description = attribute.description, action = action }); } return commands.ToArray(); } public void RegisterCommands() { ConsoleCommand[] commands = GenerateCommandsToRegister(); foreach (ConsoleCommand command in commands) { RegisterCommand(command.name, command.description, command.action); } } bool IsCommandRegistered(string name) { foreach (ConsoleCommand command in m_ConsoleCommands) { if (command.name.Equals(name)) { return true; } } return false; } [RCCMD("help", "Prints help message with all commands and cvars")] public static void PrintHelpCommand(string[] args) { StringBuilder message = new StringBuilder(); message.AppendLine("Available commands:"); foreach (ConsoleCommand command in RR.Console.m_ConsoleCommands) { message.Append(" "); message.Append(command.name); message.Append(" - "); message.Append(command.description); message.AppendLine(); } message.AppendLine("Available cvars:"); foreach (ConfigVar cvar in ConfigVarsContainer.All()) { message.Append(" "); message.Append(cvar.name); message.Append(" - "); message.Append(cvar.description); message.AppendLine(); } RR.Console.WriteToOutput(message.ToString()); } [RCCMD("cvars", "Prints all cvars")] public static void PrintCVars(string[] args) { StringBuilder message = new StringBuilder(); foreach (ConfigVar cvar in ConfigVarsContainer.All()) { message.AppendLine($"{cvar.name} - {cvar}"); } RR.Console.WriteToOutput(message.ToString()); } void Save() { string path = Application.persistentDataPath + "/" + RConsts.k_CVarsFilename; s_logger.Info("Saving cvars to file: " + path); StringBuilder sb = new StringBuilder(); foreach (ConfigVar cvar in ConfigVarsContainer.All()) { if (!cvar.flags.HasFlag(CVarFlags.ReadOnly)) { sb.AppendFormat("{0} {1}\n", cvar.name, cvar); } } File.WriteAllText(path, sb.ToString()); } void Load() { string path = Application.persistentDataPath + "/" + RConsts.k_CVarsFilename; if (!File.Exists(path)) { s_logger.Info("CVar file not found, skipping load"); return; } m_IsLoading = true; ExecuteFile(path); m_IsLoading = false; } bool ExecuteFile(string path) { if (!File.Exists(path)) { s_logger.Error($"Cannot load file '{path}', file not found"); return false; } s_logger.Info($"Executing file '{path}'"); string[] lines = File.ReadAllLines(path); foreach (string line in lines) { WriteToOutput(path + " > " + line); Execute(line); } return true; } } }