diff --git a/SrcMod/Shell/Extensions/ConversionExtension.cs b/SrcMod/Shell/Extensions/ConversionExtension.cs new file mode 100644 index 0000000..72a8e95 --- /dev/null +++ b/SrcMod/Shell/Extensions/ConversionExtension.cs @@ -0,0 +1,33 @@ +namespace SrcMod.Shell.Extensions; + +public static class ConversionExtension +{ + public static T Cast(this object obj) => (T)Cast(obj, typeof(T)); + public static object Cast(this object obj, Type newType) => Convert.ChangeType(obj, newType); + + public static object CastArray(this object[] obj, Type newElementType) + { + Array result = Array.CreateInstance(newElementType, obj.Length); + for (int i = 0; i < obj.Length; i++) result.SetValue(obj[i].Cast(newElementType), i); + return result; + } + public static T[] CastArray(this object[] obj) + { + Array result = Array.CreateInstance(typeof(T), obj.Length); + for (int i = 0; i < obj.Length; i++) result.SetValue(obj[i].Cast(), i); + return (T[])result; + } + + public static object CastArray(this Array obj, Type newElementType) + { + Array result = Array.CreateInstance(newElementType, obj.Length); + for (int i = 0; i < obj.Length; i++) result.SetValue(obj.GetValue(i)!.Cast(newElementType), i); + return result; + } + public static T[] CastArray(this Array obj) + { + Array result = Array.CreateInstance(typeof(T), obj.Length); + for (int i = 0; i < obj.Length; i++) result.SetValue(obj.GetValue(i)!.Cast(), i); + return (T[])result; + } +} diff --git a/SrcMod/Shell/GlobalUsings.cs b/SrcMod/Shell/GlobalUsings.cs index 8ae847e..19bbcbb 100644 --- a/SrcMod/Shell/GlobalUsings.cs +++ b/SrcMod/Shell/GlobalUsings.cs @@ -3,10 +3,12 @@ global using Newtonsoft.Json; global using SharpCompress.Archives.Rar; global using SharpCompress.Archives.SevenZip; global using SharpCompress.Readers; +global using SrcMod.Shell.Extensions; global using SrcMod.Shell.Interop; global using SrcMod.Shell.Modules.ObjectModels; global using SrcMod.Shell.ObjectModels; global using System; +global using System.Collections; global using System.Collections.Generic; global using System.ComponentModel; global using System.Diagnostics; diff --git a/SrcMod/Shell/LoadingBar.cs b/SrcMod/Shell/LoadingBar.cs index f658c8e..b4102e8 100644 --- a/SrcMod/Shell/LoadingBar.cs +++ b/SrcMod/Shell/LoadingBar.cs @@ -1,6 +1,6 @@ namespace SrcMod.Shell; -internal static class LoadingBar +public static class LoadingBar { public static int position = -1; public static int bufferSize = 0; diff --git a/SrcMod/Shell/Modules/ConfigModule.cs b/SrcMod/Shell/Modules/ConfigModule.cs index 58e88a0..27bb4bc 100644 --- a/SrcMod/Shell/Modules/ConfigModule.cs +++ b/SrcMod/Shell/Modules/ConfigModule.cs @@ -1,28 +1,26 @@ -namespace SrcMod.Shell.Modules; +using SharpCompress; + +namespace SrcMod.Shell.Modules; [Module("config")] public static class ConfigModule { [Command("display")] [Command("list")] - public static void DisplayConfig(ConfigDisplayMode mode = ConfigDisplayMode.All) + public static void DisplayConfig(string display = "all") { - switch (mode) + switch (display.Trim().ToLower()) { - case ConfigDisplayMode.Raw: - DisplayConfigRaw(); - break; - - case ConfigDisplayMode.All: + case "all": DisplayConfigAll(); break; - case ConfigDisplayMode.GameDirectories: - DisplayConfigGameDirectories(); + case "raw": + DisplayConfigRaw(); break; - case ConfigDisplayMode.RunUnsafeCommands: - DisplayConfigUnsafeCommands(); + default: + DisplayConfigName(display); break; } } @@ -31,164 +29,197 @@ public static class ConfigModule [Command("append")] public static void AppendConfigVariable(string name, string value) { - Config config = Config.LoadedConfig; + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); - switch (name.Trim().ToLower()) - { - case "gamedirectories": - config.GameDirectories = config.GameDirectories.Append(value).ToArray(); - break; + FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower()); + if (chosenField is null) throw new($"No valid config variable named \"{name}\"."); + else if (!chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is not an array" + + " and cannot have data added or removed from it." + + " Instead, set or reset the variable."); - case "rununsafecommands": - throw new($"The config variable \"{name}\" is a single variable and cannot be appended to."); + object parsed = TypeParsers.ParseAll(value); + if (parsed is string parsedStr + && chosenField.FieldType.IsEnum + && Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj; - default: throw new($"Unknown config variable \"{name}\""); - } + Type arrayType = chosenField.FieldType.GetElementType()!; - Config.LoadedConfig = config; + Array arrayValue = (Array)chosenField.GetValue(Config.LoadedConfig)!; + ArrayList collection = new(arrayValue) { parsed }; + + chosenField.SetValue(Config.LoadedConfig, collection.ToArray()!.CastArray(arrayType)); + Config.UpdateChanges(); + DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name); } [Command("delete")] [Command("remove")] public static void RemoveConfigVariable(string name, string value) { - Config config = Config.LoadedConfig; + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); - switch (name.Trim().ToLower()) - { - case "gamedirectories": - config.GameDirectories = config.GameDirectories - .Where(x => x.Trim().ToLower() != value.Trim().ToLower()) - .ToArray(); - break; + FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower()); + if (chosenField is null) throw new($"No valid config variable named \"{name}\"."); + else if (!chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is not an array" + + " and cannot have data added or removed from it." + + " Instead, set or reset the variable."); - case "rununsafecommands": - throw new($"The config variable \"{name}\" is a single variable and cannot be appended to."); + object parsed = TypeParsers.ParseAll(value); + if (parsed is string parsedStr + && chosenField.FieldType.IsEnum + && Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj; - default: throw new($"Unknown config variable \"{name}\""); - } + Type arrayType = chosenField.FieldType.GetElementType()!; - Config.LoadedConfig = config; + Array arrayValue = (Array)chosenField.GetValue(Config.LoadedConfig)!; + ArrayList collection = new(arrayValue); + if (!collection.Contains(parsed)) throw new($"The value \"{value}\" is not contained in this variable."); + collection.Remove(parsed); + + chosenField.SetValue(Config.LoadedConfig, collection.ToArray()!.CastArray(arrayType)); + Config.UpdateChanges(); + DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name); } [Command("reset")] public static void ResetConfig(string name = "all") { - Config config = Config.LoadedConfig; - switch (name.Trim().ToLower()) { - case "gamedirectories": - config.GameDirectories = Config.Defaults.GameDirectories; - break; - - case "rununsafecommands": - config.RunUnsafeCommands = Config.Defaults.RunUnsafeCommands; - break; - case "all": - config = Config.Defaults; + Config.LoadedConfig = Config.Defaults; + DisplayConfig("all"); break; - default: throw new($"Unknown config variable \"{name}\""); + default: + ResetConfigVar(name); + break; } - - Config.LoadedConfig = config; } [Command("set")] public static void SetConfigVariable(string name, string value) { - Config config = Config.LoadedConfig; + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); - switch (name.Trim().ToLower()) - { - case "gamedirectories": - throw new($"The config variable \"{name}\" is a list and must be added or removed to."); + FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower()); + if (chosenField is null) throw new($"No valid config variable named \"{name}\"."); + else if (chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is an array and" + + " cannot be directly set. Instead, add or remove items" + + " from it."); - case "rununsafecommands": - if (int.TryParse(value, out int intRes)) - { - AskMode mode = (AskMode)intRes; - if (!Enum.IsDefined(mode)) throw new($"(AskMode){value} is not a valid AskMode."); - config.RunUnsafeCommands = mode; - } - else if (Enum.TryParse(value, true, out AskMode modeRes)) - { - if (!Enum.IsDefined(modeRes)) throw new($"\"{value}\" is not a valid AskMode."); - config.RunUnsafeCommands = modeRes; - } - else throw new($"\"{value}\" is not a valid AskMode."); - break; + object parsed = TypeParsers.ParseAll(value); + if (parsed is string parsedStr + && chosenField.FieldType.IsEnum + && Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj; - default: throw new($"Unknown config variable \"{name}\""); - } - - Config.LoadedConfig = config; + chosenField.SetValue(Config.LoadedConfig, parsed); + Config.UpdateChanges(); + DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name); } private static void DisplayConfigAll() { - DisplayConfigGameDirectories(); - DisplayConfigUnsafeCommands(); + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); + + foreach (FieldInfo field in validFields) + DisplayConfigItem(field.GetValue(Config.LoadedConfig), name: field.Name); } - private static void DisplayConfigRaw() + private static void DisplayConfigItem(T item, int indents = 0, string name = "", bool newLine = true) { - // This is definitely a bit inefficient, but shouldn't be too much of an issue. + Write(new string(' ', indents * 4), newLine: false); + if (!string.IsNullOrWhiteSpace(name)) Write($"{name}: ", newLine: false); - MemoryStream ms = new(); - StreamWriter writer = new(ms, leaveOpen: true); - JsonTextWriter jsonWriter = new(writer); - - Serializer.Serialize(jsonWriter, Config.LoadedConfig); - - jsonWriter.Close(); - writer.Close(); - ms.Position = 0; - - StreamReader reader = new(ms); - string msg = reader.ReadToEnd(); - - Write(msg); - - reader.Close(); - ms.Close(); - } - private static void DisplayConfigGameDirectories() - { - Write("Steam Game Directories: ", null, false); - if (Config.LoadedConfig.GameDirectories is null || Config.LoadedConfig.GameDirectories.Length <= 0) - Write("None", ConsoleColor.DarkGray); - else + if (item is null) Write("null", ConsoleColor.DarkRed, newLine); + else if (item is Array itemArray) { - Write("[", ConsoleColor.DarkGray); - for (int i = 0; i < Config.LoadedConfig.GameDirectories.Length; i++) + if (itemArray.Length < 1) { - Write(" \"", ConsoleColor.DarkGray, false); - Write(Config.LoadedConfig.GameDirectories[i], ConsoleColor.White, false); - if (i < Config.LoadedConfig.GameDirectories.Length - 1) Write("\",", ConsoleColor.DarkGray); - else Write("\"", ConsoleColor.DarkGray); + Write("[]", ConsoleColor.DarkGray, newLine); + return; } - Write("]", ConsoleColor.DarkGray); + + Write("[", ConsoleColor.DarkGray); + for (int i = 0; i < itemArray.Length; i++) + { + DisplayConfigItem(itemArray.GetValue(i), indents + 1, newLine: false); + if (i < itemArray.Length - 1) Write(',', newLine: false); + Write('\n', newLine: false); + } + Write(new string(' ', indents * 4) + "]", ConsoleColor.DarkGray, newLine); } - } - private static void DisplayConfigUnsafeCommands() - { - Write("Run Unsafe Commands: ", null, false); - ConsoleColor color = Config.LoadedConfig.RunUnsafeCommands switch + else if (item is byte itemByte) Write($"0x{itemByte:X2}", ConsoleColor.Yellow, newLine); + else if (item is sbyte or short or ushort or int or uint or long or ulong or float or double or decimal) + Write(item, ConsoleColor.Yellow, newLine); + else if (item is bool itemBool) Write(item, itemBool ? ConsoleColor.Green : ConsoleColor.Red, newLine); + else if (item is char) + { + Write("\'", ConsoleColor.DarkGray, false); + Write(item, ConsoleColor.Blue, false); + Write("\'", ConsoleColor.DarkGray, newLine); + } + else if (item is string) + { + Write("\"", ConsoleColor.DarkGray, false); + Write(item, ConsoleColor.DarkCyan, false); + Write("\"", ConsoleColor.DarkGray, newLine); + } + else if (item is AskMode) Write(item, item switch { AskMode.Never => ConsoleColor.Red, AskMode.Always => ConsoleColor.Green, AskMode.Ask or _ => ConsoleColor.DarkGray - }; - Write(Config.LoadedConfig.RunUnsafeCommands, color); + }, newLine); + else Write(item, newLine: newLine); + } + private static void DisplayConfigName(string name) + { + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); + + FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower()); + if (chosenField is null) throw new($"No config variable named \"{name}\"."); + + DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name); + } + private static void DisplayConfigRaw() + { + string json = JsonConvert.SerializeObject(Config.LoadedConfig, Formatting.Indented); + Write(json); } - public enum ConfigDisplayMode + private static void ResetConfigVar(string name) { - Raw, - All, - GameDirectories, - RunUnsafeCommands + FieldInfo[] validFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); + + FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower()); + if (chosenField is null) throw new($"No valid config variable named \"{name}\"."); + + chosenField.SetValue(Config.LoadedConfig, chosenField.GetValue(Config.Defaults)); + Config.UpdateChanges(); + DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name); } } diff --git a/SrcMod/Shell/ObjectModels/Config.cs b/SrcMod/Shell/ObjectModels/Config.cs index a14e6df..197b7e6 100644 --- a/SrcMod/Shell/ObjectModels/Config.cs +++ b/SrcMod/Shell/ObjectModels/Config.cs @@ -1,10 +1,13 @@ namespace SrcMod.Shell.ObjectModels; -public struct Config +public class Config { public const string FilePath = "config.json"; - public static readonly Config Defaults; + public static Config Defaults => new(); + + private static readonly FieldInfo[] p_configSharedFields; + private static readonly FieldInfo[] p_changeSharedFields; public static Config LoadedConfig { @@ -12,39 +15,107 @@ public struct Config set { p_applied = value; - p_changes = p_applied.GetChanges(Defaults); + UpdateChanges(); } } private static Config p_applied; - private static ConfigChanges? p_changes; + private static Changes? p_changes; static Config() { - Defaults = new() + p_applied = Defaults; + + FieldInfo[] configFields = (from field in typeof(Config).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(), + changeFields = (from field in typeof(Changes).GetFields() + let isPublic = field.IsPublic + let isStatic = field.IsStatic + where isPublic && !isStatic + select field).ToArray(); + + List sharedConfigFields = new(), + sharedChangeFields = new(); + foreach (FieldInfo field in configFields) { - GameDirectories = Array.Empty(), - RunUnsafeCommands = AskMode.Ask - }; + FieldInfo? changeEquivalent = changeFields.FirstOrDefault( + x => x.Name == field.Name && + (x.FieldType == field.FieldType || Nullable.GetUnderlyingType(x.FieldType) == field.FieldType)); + + if (changeEquivalent is null) continue; + + sharedConfigFields.Add(field); + sharedChangeFields.Add(changeEquivalent); + } + + static int sortByName(FieldInfo a, FieldInfo b) => a.Name.CompareTo(b.Name); + + sharedConfigFields.Sort(sortByName); + sharedChangeFields.Sort(sortByName); + + p_configSharedFields = sharedConfigFields.ToArray(); + p_changeSharedFields = sharedChangeFields.ToArray(); } public string[] GameDirectories; public AskMode RunUnsafeCommands; - public Config ApplyChanges(ConfigChanges changes) => this with + internal Config() { - GameDirectories = GameDirectories.Union(changes.GameDirectories ?? Array.Empty()).ToArray(), - RunUnsafeCommands = changes.RunUnsafeCommands ?? RunUnsafeCommands - }; - public ConfigChanges GetChanges(Config? baseConfig = null) + GameDirectories = Array.Empty(); + RunUnsafeCommands = AskMode.Ask; + } + + public Config ApplyChanges(Changes changes) { - Config reference = baseConfig ?? Defaults; - ConfigChanges changes = new() + for (int i = 0; i < p_configSharedFields.Length; i++) { - GameDirectories = reference.GameDirectories == GameDirectories ? null : - GameDirectories.Where(x => !reference.GameDirectories.Contains(x)).ToArray(), - RunUnsafeCommands = reference.RunUnsafeCommands == RunUnsafeCommands ? null : RunUnsafeCommands - }; + FieldInfo configField = p_configSharedFields[i], + changeField = p_changeSharedFields[i]; + + object? toChange = changeField.GetValue(changes); + + if (toChange is null) continue; + + if (configField.FieldType.IsArray) + { + object[] currentArray = ((Array)configField.GetValue(this)!).CastArray(), + changeArray = ((Array)toChange).CastArray(); + + currentArray = currentArray.Union(changeArray).ToArray(); + configField.SetValue(this, currentArray.CastArray(configField.FieldType.GetElementType()!)); + } + else configField.SetValue(this, toChange); + } + + return this; + } + public Changes GetChanges(Config? reference = null) + { + reference ??= Defaults; + Changes changes = new(); + + for (int i = 0; i < p_configSharedFields.Length; i++) + { + FieldInfo configField = p_configSharedFields[i], + changeField = p_changeSharedFields[i]; + + object? toSet = configField.GetValue(this); + + if (toSet is null) continue; + + if (configField.FieldType.IsArray) + { + object[] configArray = ((Array)toSet).CastArray(), + referenceArray = ((Array)configField.GetValue(Defaults)!).CastArray(), + changesArray = configArray.Where(x => !referenceArray.Contains(x)).ToArray(); + changeField.SetValue(changes, changesArray.CastArray(configField.FieldType.GetElementType()!)); + } + else changeField.SetValue(changes, toSet); + } return changes; } @@ -61,26 +132,42 @@ public struct Config } StreamReader reader = new(fullPath); JsonTextReader jsonReader = new(reader); - p_changes = Serializer.Deserialize(jsonReader); + p_changes = Serializer.Deserialize(jsonReader); jsonReader.Close(); reader.Close(); - p_applied = p_changes is null ? Defaults : Defaults.ApplyChanges(p_changes.Value); + p_applied = p_changes is null ? Defaults : Defaults.ApplyChanges(p_changes); } public static void SaveConfig(string basePath) { string fullPath = Path.Combine(basePath, FilePath); - if (p_changes is null || !p_changes.Value.HasChange) + if (p_changes is null || !p_changes.Any()) { if (File.Exists(fullPath)) File.Delete(fullPath); return; } StreamWriter writer = new(fullPath); - JsonTextWriter jsonWriter = new(writer); + JsonTextWriter jsonWriter = new(writer) + { + Indentation = 4 + }; Serializer.Serialize(jsonWriter, p_changes); jsonWriter.Close(); writer.Close(); } + + public static void UpdateChanges() + { + p_changes = p_applied.GetChanges(Defaults); + } + + public class Changes + { + public string[]? GameDirectories; + public AskMode? RunUnsafeCommands; + + public bool Any() => typeof(Changes).GetFields().Any(x => x.GetValue(this) is not null); + } } diff --git a/SrcMod/Shell/ObjectModels/ConfigChanges.cs b/SrcMod/Shell/ObjectModels/ConfigChanges.cs deleted file mode 100644 index 8328796..0000000 --- a/SrcMod/Shell/ObjectModels/ConfigChanges.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace SrcMod.Shell.ObjectModels; - -public record struct ConfigChanges -{ - [JsonIgnore] - public bool HasChange => GameDirectories is not null || RunUnsafeCommands is not null; - - public string[]? GameDirectories; - public AskMode? RunUnsafeCommands; -} diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs index 0629dd8..7fe1d15 100644 --- a/SrcMod/Shell/Shell.cs +++ b/SrcMod/Shell/Shell.cs @@ -4,7 +4,7 @@ public class Shell { public const string Author = "That_One_Nerd"; public const string Name = "SrcMod"; - public const string Version = "Alpha 0.3.3"; + public const string Version = "Alpha 0.4.0"; public readonly string? ShellDirectory; @@ -239,24 +239,27 @@ public class Shell void runCommand(object? sender, DoWorkEventArgs e) { -#if RELEASE try { -#endif command.Invoke(args); -#if RELEASE } +#if RELEASE catch (TargetInvocationException ex) { Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red); if (LoadingBar.Enabled) LoadingBar.End(); } +#endif catch (Exception ex) { +#if RELEASE Write($"[ERROR] {ex.Message}", ConsoleColor.Red); if (LoadingBar.Enabled) LoadingBar.End(); - } +#else + Write($"[ERROR] {ex}", ConsoleColor.Red); + if (LoadingBar.Enabled) LoadingBar.End(); #endif + } } activeCommand = new(); diff --git a/SrcMod/Shell/Tools.cs b/SrcMod/Shell/Tools.cs index c90265d..bd1c219 100644 --- a/SrcMod/Shell/Tools.cs +++ b/SrcMod/Shell/Tools.cs @@ -1,6 +1,6 @@ namespace SrcMod.Shell; -internal static class Tools +public static class Tools { public static JsonSerializer Serializer { get; private set; }