From 9659fb60864491644ac217278f2a23b961dbe70c Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Wed, 1 Jan 2025 14:43:35 -0600 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cogwheel.csproj | 10 + DeafultCogwheelConsole.cs | 55 +++++ Program.cs | 9 + src/COGWHEEL.cs | 131 +++++++++++ src/Command.cs | 17 ++ src/CommandAttribute.cs | 8 + src/CommandsManager.cs | 337 +++++++++++++++++++++++++++++ src/TestClass.cs | 20 ++ src/interfaces/ICogwheelConsole.cs | 16 ++ src/interfaces/ICommand.cs | 10 + src/interfaces/ICommandsManager.cs | 33 +++ 12 files changed, 648 insertions(+) create mode 100644 .gitignore create mode 100644 Cogwheel.csproj create mode 100644 DeafultCogwheelConsole.cs create mode 100644 Program.cs create mode 100644 src/COGWHEEL.cs create mode 100644 src/Command.cs create mode 100644 src/CommandAttribute.cs create mode 100644 src/CommandsManager.cs create mode 100644 src/TestClass.cs create mode 100644 src/interfaces/ICogwheelConsole.cs create mode 100644 src/interfaces/ICommand.cs create mode 100644 src/interfaces/ICommandsManager.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/Cogwheel.csproj b/Cogwheel.csproj new file mode 100644 index 0000000..2f4fc77 --- /dev/null +++ b/Cogwheel.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/DeafultCogwheelConsole.cs b/DeafultCogwheelConsole.cs new file mode 100644 index 0000000..04db22b --- /dev/null +++ b/DeafultCogwheelConsole.cs @@ -0,0 +1,55 @@ +namespace Cogwheel; + +using System; +using System.Collections.Generic; +using System.Linq; + +public class DeafultCogwheelConsole : ICogwheelConsole +{ + public string OpeningMessage { get; set; } = "** COGWHEEL CONSOLE **"; + public bool IsRunning { get; set; } + public ICommandsManager CommandsManager { get; set; } + + + public void Initialize(ICommandsManager commandsManager) + { + CommandsManager = commandsManager; + CommandsManager.RegisterObject(this); + + Log(OpeningMessage); + + IsRunning = true; + while (IsRunning) + { + Console.Write("> "); + string input = Console.ReadLine(); + CommandsManager.RunCommand(input); + } + } + + public void Log(string message) + { + Console.WriteLine($"[COGWHEEL] {message}"); + } + + public void LogError(string message) + { + Console.WriteLine($"[COGWHEEL ERROR] {message}"); + } + + public void LogWarning(string message) + { + Console.WriteLine($"[COGWHEEL WARNING] {message}"); + } + + public void ClearConsole() + { + Console.Clear(); + } + + public void Exit() + { + IsRunning = false; + } + +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..cbc250b --- /dev/null +++ b/Program.cs @@ -0,0 +1,9 @@ +// Program.cs + +using Cogwheel; + +ICogwheelConsole cogwheelConsole = new DeafultCogwheelConsole(); +ICommandsManager commandsManager = new CommandsManager(cogwheelConsole); + +COGWHEEL.Initialize(commandsManager, cogwheelConsole); + diff --git a/src/COGWHEEL.cs b/src/COGWHEEL.cs new file mode 100644 index 0000000..fbccbc1 --- /dev/null +++ b/src/COGWHEEL.cs @@ -0,0 +1,131 @@ +namespace Cogwheel; + +public static class COGWHEEL +{ + /* TODO: + * -[X] Initialize the CommandsManager + * -[ ] Public static methods for the CommandsManager + * -[X] Create built-in commands + * -[ ] + * -[ ] + * -[ ] + */ + + private static ICommandsManager _commandsManager; + private static ICogwheelConsole _console; + + public static void Initialize(ICommandsManager commandsManager, ICogwheelConsole console) + { + _commandsManager = commandsManager; + _console = console; + + _console.Initialize(_commandsManager); + } + + + // == Public static methods for the CommandsManager == // + public static void RunCommand(string input) + { + _commandsManager.RunCommand(input); + } + + public static void RegisterObject(object obj) + { + _commandsManager.RegisterObject(obj); + } + + + // == Public static methods for the Console == // + public static void Log(string message) + { + _console.Log(message); + } + + public static void LogError(string message) + { + _console.LogError(message); + } + + public static void LogWarning(string message) + { + _console.LogWarning(message); + } + + // == Built-in commands == // + [Command(Name = "quit", Description = "Quits the Cogwheel console.")] + public static void QuitCogwheelConsole() + { + _console.Exit(); + } + + [Command(Name = "clear", Description = "Clears the console.")] + public static void ClearConsole() + { + _console.ClearConsole(); + } + + [Command(Name = "help", Description = "Gets usage for given command.")] + public static void ShowHelp(string commandName) + { + ICommand command = _commandsManager.GetCommandByName(commandName); + if (command is null) + { + _console.LogError($"Command error: '{commandName}' is not a command."); + return; + } + + _console.Log($"-- Command Usage For {commandName} --\n" + + $"Description: {command.Description}\n" + + $"Usage: {_commandsManager.GetCommandUsage(command)}"); + } + + [Command(Name = "list", Description = "Lists all available commands.")] + public static void ListCommands() + { + foreach (var command in _commandsManager.Commands) + { + _console.Log($"{command.Key} - {command.Value.Description}"); + } + } + + [Command(Name = "what", Description = "Prints the current context.")] + public static void ShowCurrentContext() + { + _console.Log($"Current context: {_commandsManager.CurrentContext.GetType()} : {_commandsManager.CurrentContextGuid}"); + } + + [Command(Name = "test")] + public static void CreateTestObject(string name) + { + _console.Log($"Creating new TestClass object with name: {name}"); + var test = new TestClass(name); + } + + [Command(Name = "find", Description = "Lists available registered objects to select from.")] + private static void SelectContextFromRegisteredObjects(string typeName = "") + { + var registeredObjects = _commandsManager.RegisteredObjectInstances; + var filteredObjects = string.IsNullOrEmpty(typeName) + ? registeredObjects + : registeredObjects.Where(kvp => kvp.Value.GetType().FullName == typeName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + + if (!filteredObjects.Any()) + { + LogError($"No registered objects found for type '{typeName}'"); + return; + } + + Log("Available registered objects:"); + foreach (var (guid, obj) in filteredObjects) + { + Log($"- {obj.GetType().FullName} : {guid}"); + } + } + + [Command(Name = "set", Description = "Sets the context to the specified registered object.")] + private static void SetContextFromGuid(string guidString) + { + _commandsManager.SetContext(guidString); + Log($"Set context to {_commandsManager.CurrentContext?.GetType().FullName} : {_commandsManager.CurrentContextGuid}"); + } +} \ No newline at end of file diff --git a/src/Command.cs b/src/Command.cs new file mode 100644 index 0000000..e0719d4 --- /dev/null +++ b/src/Command.cs @@ -0,0 +1,17 @@ +using System.Reflection; + +namespace Cogwheel; + +public class Command : ICommand +{ + public string Name { get; } + public string Description { get; } + public MethodBase Method { get; } + + public Command(string name, string description, MethodBase method) + { + Name = name; + Description = description; + Method = method; + } +} \ No newline at end of file diff --git a/src/CommandAttribute.cs b/src/CommandAttribute.cs new file mode 100644 index 0000000..b2ea52f --- /dev/null +++ b/src/CommandAttribute.cs @@ -0,0 +1,8 @@ +namespace Cogwheel; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +public class CommandAttribute : Attribute +{ + public string Name = ""; + public string Description = ""; +} \ No newline at end of file diff --git a/src/CommandsManager.cs b/src/CommandsManager.cs new file mode 100644 index 0000000..7f16591 --- /dev/null +++ b/src/CommandsManager.cs @@ -0,0 +1,337 @@ +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Exception = System.Exception; + +namespace Cogwheel; + +public class CommandsManager : ICommandsManager +{ + public ICogwheelConsole CogwheelConsole { get; set; } + + public string CommandPattern { get; set; } = "(?(\\([^\\)]+\\)))|\"(?[^\"]+)\"|'(?[^']+)'|(?[^\\s]+)"; + public List Assemblies { get; set; } = []; + public Dictionary Commands { get; set; } = new(); + public Dictionary> CustomParsers { get; set; } = new(); + + // Context related stuff + public object? CurrentContext { get; set; } + public Guid? CurrentContextGuid { get; set; } + public Dictionary RegisteredObjectInstances { get; set; } = new(); + public Dictionary RegisteredObjectGuids { get; set; } = new(); + + + public CommandsManager(ICogwheelConsole console) + { + CogwheelConsole = console; + + RefreshCommandsList(); + CogwheelConsole.Log($"CommandsManager initialized, Commands found: {Commands.Count}"); + } + + + public (ICommand, List)? GetCommandAndArgsFromString(string input) + { + var splitString = Regex.Matches(input, CommandPattern).Select(m => m.Groups["val"].Value).ToArray(); + var commandName = splitString[0]; + List args = new(); + for (var splitIndex = 1; splitIndex < splitString.Length; splitIndex++) + { + args.Add(splitString[splitIndex]); + } + + if (Commands.TryGetValue(commandName, out var command)) + { + return (command, args); + } + + return null; + } + + public bool RunCommand(string input, object? context = null) + { + if (string.IsNullOrWhiteSpace(input)) return false; + var command = GetCommandAndArgsFromString(input); + if (command is null) + { + CogwheelConsole.LogError($"[COGWHEEL] Command not found: {input}"); + return false; + } + + if (command.Value.Item1.Method.IsStatic) + { + return ExecuteCommand(null, command.Value); + } + + if (context is not null && IsCommandContextValid(command.Value.Item1, context)) + { + return ExecuteCommand(context, command.Value); + } + + if (CurrentContext is not null && IsCommandContextValid(command.Value.Item1, CurrentContext)) + { + return ExecuteCommand(CurrentContext, command.Value); + } + + CogwheelConsole.LogWarning($"[COGWHEEL] Command '{command.Value.Item1.Name}' is not static and no valid context was provided, searching for first registered context of type '{command.Value.Item1.Method.DeclaringType}'"); + context = GetFirstValidRegisteredContext(command.Value.Item1.Method.DeclaringType); + if (context is null) + { + CogwheelConsole.LogError($"[COGWHEEL] No context of type '{command.Value.Item1.Method.DeclaringType}' found"); + return false; + } + + if (IsCommandContextValid(command.Value.Item1, context)) + { + return ExecuteCommand(context, command.Value); + } + + return false; + } + + public string GetCommandUsage(ICommand command) + { + string paramUsage = string.Join(" ", + command.Method.GetParameters().Select(param => + $"<{(param.IsDefined(typeof(ParamArrayAttribute)) ? "params " : "")}{param}>")); + + return $"{command.Name}: {paramUsage}"; + } + + public bool ExecuteCommand(object obj, (ICommand command, List args) command) + { + var parameters = command.command.Method.GetParameters(); + for (var parameterIndex = 0; parameterIndex < parameters.Length; parameterIndex++) + { + if (parameters[parameterIndex].IsDefined(typeof(ParamArrayAttribute), false)) + { + var paramList = Activator.CreateInstance(typeof(List<>).MakeGenericType(parameters[parameterIndex].ParameterType.GetElementType()!)); + for (var argIndex = parameterIndex; argIndex < parameters.Length; argIndex++) + { + if (TryParseParameter(parameters[parameterIndex].ParameterType.GetElementType(), + (string)command.args[argIndex], out var val)) + { + paramList.GetType().GetMethod("Add")?.Invoke(paramList, new[] { val }); + } + else + { + CogwheelConsole.LogError($"Format exception: could not parse '{command.args[parameterIndex]}' as '{parameters[parameterIndex].ParameterType}'"); + return false; + } + } + + command.args = command.args.Take(parameterIndex).ToList(); + command.args.Add(paramList.GetType().GetMethod("ToArray")?.Invoke(paramList, null)); + } + else if (parameterIndex >= command.args.Count) + { + if (parameters[parameterIndex].IsOptional) + { + command.args.Add(Type.Missing); + } + else + { + CogwheelConsole.LogError("Not enough args passed"); + CogwheelConsole.Log(GetCommandUsage(command.command)); + return false; + } + } + else + { + if (TryParseParameter(parameters[parameterIndex].ParameterType, (string)command.args[parameterIndex], out var val)) + { + command.args[parameterIndex] = val; + } + else + { + CogwheelConsole.LogError($"Format exception: could not parse '{command.args[parameterIndex]}' as '{parameters[parameterIndex].ParameterType}'"); + return false; + } + } + } + + try + { + command.command.Method.Invoke(obj, command.args.ToArray()); + return true; + } + catch (Exception e) + { + CogwheelConsole.LogError(e.Message); + throw; + } + } + + public bool TryParseParameter(Type parameterType, string parameterString, out object parsedValue) + { + if (parameterType == typeof(string)) + { + parsedValue = parameterString; + return true; + } + + if (CustomParsers.ContainsKey(parameterType)) + { + try + { + parsedValue = CustomParsers[parameterType].Invoke(parameterString); + return true; + } + catch (Exception e) + { + CogwheelConsole.LogError(e.Message); + parsedValue = null; + return false; + } + } + else + { + var parseMethod = parameterType.GetMethod("Parse", new[] { typeof(string) }); + + if (parseMethod is not null) + { + try + { + parsedValue = parseMethod.Invoke(null, new object[] { parameterString }); + return true; + } + catch (Exception e) + { + CogwheelConsole.LogError(e.Message); + parsedValue = null; + return false; + } + } + } + + parsedValue = null; + return false; + } + + public ICommand? GetCommandByName(string commandName) + { + return Commands.GetValueOrDefault(commandName); + } + + // Context related stuff + + public void RegisterObject(object obj) + { + // Use a combination of the object's type and a hash of its properties to generate a deterministic GUID + string seed = obj.GetType().FullName + GetObjectPropertiesHash(obj); + using (MD5 md5 = MD5.Create()) + { + byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(seed)); + Guid uniqueId = new Guid(hash.Take(16).ToArray()); + + if (!RegisteredObjectInstances.ContainsKey(uniqueId)) + { + RegisteredObjectInstances[uniqueId] = obj; + RegisteredObjectGuids[obj] = uniqueId; + CogwheelConsole.Log($"[COGWHEEL] Registered object with ID {uniqueId}"); + } + else + { + CogwheelConsole.LogWarning($"[COGWHEEL] Object with ID {uniqueId} is already registered"); + } + } + } + + public bool IsCommandContextValid(ICommand command, object? context = null) + { + if (context is not null) + { + return command.Method.DeclaringType == context.GetType(); + } + return command.Method.DeclaringType == CurrentContext?.GetType(); + } + + public bool SetContext(object context) + { + CurrentContext = context; + if (!RegisteredObjectGuids.ContainsKey(context)) + { + RegisterObject(context); + return true; + } + CurrentContextGuid = RegisteredObjectGuids[context]; + return true; + } + + public bool SetContext(Guid guid) + { + if (RegisteredObjectInstances.ContainsKey(guid)) + { + CurrentContext = RegisteredObjectInstances[guid]; + CurrentContextGuid = guid; + } + else + { + CogwheelConsole.LogError($"No object registered with ID {guid}"); + return false; + } + + return true; + } + + public bool SetContext(string guidString) + { + if (Guid.TryParse(guidString, out var guid)) + { + return SetContext(guid); + } + + CogwheelConsole.LogError($"Could not parse '{guidString}' as a GUID"); + return false; + } + + public object? GetFirstValidRegisteredContext(Type type) + { + return RegisteredObjectInstances.Values.FirstOrDefault(obj => obj.GetType() == type); + } + + public Guid GetGuidFromContext(object? context = null) + { + if (context is not null) + { + return RegisteredObjectGuids[context]; + } + + return CurrentContextGuid ?? Guid.Empty; + } + + private string GetObjectPropertiesHash(object obj) + { + var properties = obj.GetType().GetProperties(); + var sb = new StringBuilder(); + + foreach (var prop in properties) + { + var value = prop.GetValue(obj)?.ToString() ?? string.Empty; + sb.Append(value); + } + + return sb.ToString(); + } + + private void RefreshCommandsList() + { + foreach (var type in Assembly.GetCallingAssembly().GetTypes()) + { + var methods = type.GetMethods(BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance); + + foreach (var method in methods) + { + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + var commandName = attribute.Name ?? method.Name; + var newCommand = new Command(commandName, attribute.Description, method); + Commands.TryAdd(newCommand.Name, newCommand); + } + } + } + } +} \ No newline at end of file diff --git a/src/TestClass.cs b/src/TestClass.cs new file mode 100644 index 0000000..1add217 --- /dev/null +++ b/src/TestClass.cs @@ -0,0 +1,20 @@ +namespace Cogwheel; + +public class TestClass +{ + + public string Name { get; set; } = "Test"; + + public TestClass(string name) + { + Name = name; + + COGWHEEL.RegisterObject(this); + } + + [Command(Name = "getname", Description = "Prints the name of the TestClass instance.")] + private void GetName() + { + COGWHEEL.Log($"My name is {Name}"); + } +} \ No newline at end of file diff --git a/src/interfaces/ICogwheelConsole.cs b/src/interfaces/ICogwheelConsole.cs new file mode 100644 index 0000000..db36628 --- /dev/null +++ b/src/interfaces/ICogwheelConsole.cs @@ -0,0 +1,16 @@ +namespace Cogwheel; + +public interface ICogwheelConsole +{ + public string OpeningMessage { get; set; } + public bool IsRunning { get; set; } + public ICommandsManager CommandsManager { get; set; } + + public void Initialize(ICommandsManager commandsManager); // Make sure to pass the CommandsManager instance to the Console + public void Log(string message); + public void LogError(string message); + public void LogWarning(string message); + public void ClearConsole(); + public void Exit(); + +} \ No newline at end of file diff --git a/src/interfaces/ICommand.cs b/src/interfaces/ICommand.cs new file mode 100644 index 0000000..9dc10fa --- /dev/null +++ b/src/interfaces/ICommand.cs @@ -0,0 +1,10 @@ +using System.Reflection; + +namespace Cogwheel; + +public interface ICommand +{ + public string Name { get; } + public string Description { get; } + public MethodBase Method { get; } +} \ No newline at end of file diff --git a/src/interfaces/ICommandsManager.cs b/src/interfaces/ICommandsManager.cs new file mode 100644 index 0000000..6241639 --- /dev/null +++ b/src/interfaces/ICommandsManager.cs @@ -0,0 +1,33 @@ +using System.Reflection; + +namespace Cogwheel; + +public interface ICommandsManager +{ + public ICogwheelConsole CogwheelConsole { get; set; } + public string CommandPattern { get; set; } + public List Assemblies { get; set; } + public Dictionary Commands { get; set; } + public Dictionary> CustomParsers { get; set; } + public object? CurrentContext { get; set; } + public Guid? CurrentContextGuid { get; set; } + public Dictionary RegisteredObjectInstances { get; set; } + public Dictionary RegisteredObjectGuids { get; set; } + + + public void RegisterObject(object obj); + public (ICommand, List)? GetCommandAndArgsFromString(string input); + public bool RunCommand(string input, object? context = null); + public string GetCommandUsage(ICommand command); + public bool ExecuteCommand(object obj, (ICommand command, List args) command); + public bool TryParseParameter(Type parameterType, string parameterString, out object parsedValue); + public ICommand? GetCommandByName(string commandName); + + // Context related stuff + public bool IsCommandContextValid(ICommand command, object? context = null); + public bool SetContext(object context); + public bool SetContext(Guid guid); + public bool SetContext(string guidString); + public object? GetFirstValidRegisteredContext(Type type); + public Guid GetGuidFromContext(object? context = null); +} \ No newline at end of file