Ready for alpha 0.4.0 (the final alpha version (hopefully)) #72

Merged
That-One-Nerd merged 16 commits from shell-configuration into main 2023-04-25 17:42:32 -04:00
8 changed files with 306 additions and 160 deletions

View File

@ -0,0 +1,33 @@
namespace SrcMod.Shell.Extensions;
public static class ConversionExtension
{
public static T Cast<T>(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<T>(this object[] obj)
{
Array result = Array.CreateInstance(typeof(T), obj.Length);
for (int i = 0; i < obj.Length; i++) result.SetValue(obj[i].Cast<T>(), 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<T>(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<T>(), i);
return (T[])result;
}
}

View File

@ -3,10 +3,12 @@ global using Newtonsoft.Json;
global using SharpCompress.Archives.Rar; global using SharpCompress.Archives.Rar;
global using SharpCompress.Archives.SevenZip; global using SharpCompress.Archives.SevenZip;
global using SharpCompress.Readers; global using SharpCompress.Readers;
global using SrcMod.Shell.Extensions;
global using SrcMod.Shell.Interop; global using SrcMod.Shell.Interop;
global using SrcMod.Shell.Modules.ObjectModels; global using SrcMod.Shell.Modules.ObjectModels;
global using SrcMod.Shell.ObjectModels; global using SrcMod.Shell.ObjectModels;
global using System; global using System;
global using System.Collections;
global using System.Collections.Generic; global using System.Collections.Generic;
global using System.ComponentModel; global using System.ComponentModel;
global using System.Diagnostics; global using System.Diagnostics;

View File

@ -1,6 +1,6 @@
namespace SrcMod.Shell; namespace SrcMod.Shell;
internal static class LoadingBar public static class LoadingBar
{ {
public static int position = -1; public static int position = -1;
public static int bufferSize = 0; public static int bufferSize = 0;

View File

@ -1,28 +1,26 @@
namespace SrcMod.Shell.Modules; using SharpCompress;
namespace SrcMod.Shell.Modules;
[Module("config")] [Module("config")]
public static class ConfigModule public static class ConfigModule
{ {
[Command("display")] [Command("display")]
[Command("list")] [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: case "all":
DisplayConfigRaw();
break;
case ConfigDisplayMode.All:
DisplayConfigAll(); DisplayConfigAll();
break; break;
case ConfigDisplayMode.GameDirectories: case "raw":
DisplayConfigGameDirectories(); DisplayConfigRaw();
break; break;
case ConfigDisplayMode.RunUnsafeCommands: default:
DisplayConfigUnsafeCommands(); DisplayConfigName(display);
break; break;
} }
} }
@ -31,164 +29,197 @@ public static class ConfigModule
[Command("append")] [Command("append")]
public static void AppendConfigVariable(string name, string value) 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()) FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower());
{ if (chosenField is null) throw new($"No valid config variable named \"{name}\".");
case "gamedirectories": else if (!chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is not an array" +
config.GameDirectories = config.GameDirectories.Append(value).ToArray(); " and cannot have data added or removed from it." +
break; " Instead, set or reset the variable.");
case "rununsafecommands": object parsed = TypeParsers.ParseAll(value);
throw new($"The config variable \"{name}\" is a single variable and cannot be appended to."); 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("delete")]
[Command("remove")] [Command("remove")]
public static void RemoveConfigVariable(string name, string value) 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()) FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower());
{ if (chosenField is null) throw new($"No valid config variable named \"{name}\".");
case "gamedirectories": else if (!chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is not an array" +
config.GameDirectories = config.GameDirectories " and cannot have data added or removed from it." +
.Where(x => x.Trim().ToLower() != value.Trim().ToLower()) " Instead, set or reset the variable.");
.ToArray();
break;
case "rununsafecommands": object parsed = TypeParsers.ParseAll(value);
throw new($"The config variable \"{name}\" is a single variable and cannot be appended to."); 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")] [Command("reset")]
public static void ResetConfig(string name = "all") public static void ResetConfig(string name = "all")
{ {
Config config = Config.LoadedConfig;
switch (name.Trim().ToLower()) switch (name.Trim().ToLower())
{ {
case "gamedirectories":
config.GameDirectories = Config.Defaults.GameDirectories;
break;
case "rununsafecommands":
config.RunUnsafeCommands = Config.Defaults.RunUnsafeCommands;
break;
case "all": case "all":
config = Config.Defaults; Config.LoadedConfig = Config.Defaults;
DisplayConfig("all");
break; break;
default: throw new($"Unknown config variable \"{name}\""); default:
ResetConfigVar(name);
break;
} }
Config.LoadedConfig = config;
} }
[Command("set")] [Command("set")]
public static void SetConfigVariable(string name, string value) 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()) FieldInfo? chosenField = validFields.FirstOrDefault(x => x.Name.Trim().ToLower() == name.Trim().ToLower());
{ if (chosenField is null) throw new($"No valid config variable named \"{name}\".");
case "gamedirectories": else if (chosenField.FieldType.IsArray) throw new($"The variable \"{chosenField.Name}\" is an array and" +
throw new($"The config variable \"{name}\" is a list and must be added or removed to."); " cannot be directly set. Instead, add or remove items" +
" from it.");
case "rununsafecommands": object parsed = TypeParsers.ParseAll(value);
if (int.TryParse(value, out int intRes)) if (parsed is string parsedStr
{ && chosenField.FieldType.IsEnum
AskMode mode = (AskMode)intRes; && Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj;
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;
default: throw new($"Unknown config variable \"{name}\""); chosenField.SetValue(Config.LoadedConfig, parsed);
} Config.UpdateChanges();
DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name);
Config.LoadedConfig = config;
} }
private static void DisplayConfigAll() private static void DisplayConfigAll()
{ {
DisplayConfigGameDirectories(); FieldInfo[] validFields = (from field in typeof(Config).GetFields()
DisplayConfigUnsafeCommands(); 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>(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(); if (item is null) Write("null", ConsoleColor.DarkRed, newLine);
StreamWriter writer = new(ms, leaveOpen: true); else if (item is Array itemArray)
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
{ {
Write("[", ConsoleColor.DarkGray); if (itemArray.Length < 1)
for (int i = 0; i < Config.LoadedConfig.GameDirectories.Length; i++)
{ {
Write(" \"", ConsoleColor.DarkGray, false); Write("[]", ConsoleColor.DarkGray, newLine);
Write(Config.LoadedConfig.GameDirectories[i], ConsoleColor.White, false); return;
if (i < Config.LoadedConfig.GameDirectories.Length - 1) Write("\",", ConsoleColor.DarkGray);
else Write("\"", ConsoleColor.DarkGray);
} }
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);
} }
} else if (item is byte itemByte) Write($"0x{itemByte:X2}", ConsoleColor.Yellow, newLine);
private static void DisplayConfigUnsafeCommands() 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);
Write("Run Unsafe Commands: ", null, false); else if (item is bool itemBool) Write(item, itemBool ? ConsoleColor.Green : ConsoleColor.Red, newLine);
ConsoleColor color = Config.LoadedConfig.RunUnsafeCommands switch 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.Never => ConsoleColor.Red,
AskMode.Always => ConsoleColor.Green, AskMode.Always => ConsoleColor.Green,
AskMode.Ask or _ => ConsoleColor.DarkGray AskMode.Ask or _ => ConsoleColor.DarkGray
}; }, newLine);
Write(Config.LoadedConfig.RunUnsafeCommands, color); 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, FieldInfo[] validFields = (from field in typeof(Config).GetFields()
All, let isPublic = field.IsPublic
GameDirectories, let isStatic = field.IsStatic
RunUnsafeCommands 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);
} }
} }

View File

@ -1,10 +1,13 @@
namespace SrcMod.Shell.ObjectModels; namespace SrcMod.Shell.ObjectModels;
public struct Config public class Config
{ {
public const string FilePath = "config.json"; 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 public static Config LoadedConfig
{ {
@ -12,39 +15,107 @@ public struct Config
set set
{ {
p_applied = value; p_applied = value;
p_changes = p_applied.GetChanges(Defaults); UpdateChanges();
} }
} }
private static Config p_applied; private static Config p_applied;
private static ConfigChanges? p_changes; private static Changes? p_changes;
static Config() 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<FieldInfo> sharedConfigFields = new(),
sharedChangeFields = new();
foreach (FieldInfo field in configFields)
{ {
GameDirectories = Array.Empty<string>(), FieldInfo? changeEquivalent = changeFields.FirstOrDefault(
RunUnsafeCommands = AskMode.Ask 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 string[] GameDirectories;
public AskMode RunUnsafeCommands; public AskMode RunUnsafeCommands;
public Config ApplyChanges(ConfigChanges changes) => this with internal Config()
{ {
GameDirectories = GameDirectories.Union(changes.GameDirectories ?? Array.Empty<string>()).ToArray(), GameDirectories = Array.Empty<string>();
RunUnsafeCommands = changes.RunUnsafeCommands ?? RunUnsafeCommands RunUnsafeCommands = AskMode.Ask;
}; }
public ConfigChanges GetChanges(Config? baseConfig = null)
public Config ApplyChanges(Changes changes)
{ {
Config reference = baseConfig ?? Defaults; for (int i = 0; i < p_configSharedFields.Length; i++)
ConfigChanges changes = new()
{ {
GameDirectories = reference.GameDirectories == GameDirectories ? null : FieldInfo configField = p_configSharedFields[i],
GameDirectories.Where(x => !reference.GameDirectories.Contains(x)).ToArray(), changeField = p_changeSharedFields[i];
RunUnsafeCommands = reference.RunUnsafeCommands == RunUnsafeCommands ? null : RunUnsafeCommands
}; object? toChange = changeField.GetValue(changes);
if (toChange is null) continue;
if (configField.FieldType.IsArray)
{
object[] currentArray = ((Array)configField.GetValue(this)!).CastArray<object>(),
changeArray = ((Array)toChange).CastArray<object>();
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<object>(),
referenceArray = ((Array)configField.GetValue(Defaults)!).CastArray<object>(),
changesArray = configArray.Where(x => !referenceArray.Contains(x)).ToArray();
changeField.SetValue(changes, changesArray.CastArray(configField.FieldType.GetElementType()!));
}
else changeField.SetValue(changes, toSet);
}
return changes; return changes;
} }
@ -61,26 +132,42 @@ public struct Config
} }
StreamReader reader = new(fullPath); StreamReader reader = new(fullPath);
JsonTextReader jsonReader = new(reader); JsonTextReader jsonReader = new(reader);
p_changes = Serializer.Deserialize<ConfigChanges?>(jsonReader); p_changes = Serializer.Deserialize<Changes?>(jsonReader);
jsonReader.Close(); jsonReader.Close();
reader.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) public static void SaveConfig(string basePath)
{ {
string fullPath = Path.Combine(basePath, FilePath); 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); if (File.Exists(fullPath)) File.Delete(fullPath);
return; return;
} }
StreamWriter writer = new(fullPath); StreamWriter writer = new(fullPath);
JsonTextWriter jsonWriter = new(writer); JsonTextWriter jsonWriter = new(writer)
{
Indentation = 4
};
Serializer.Serialize(jsonWriter, p_changes); Serializer.Serialize(jsonWriter, p_changes);
jsonWriter.Close(); jsonWriter.Close();
writer.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);
}
} }

View File

@ -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;
}

View File

@ -4,7 +4,7 @@ public class Shell
{ {
public const string Author = "That_One_Nerd"; public const string Author = "That_One_Nerd";
public const string Name = "SrcMod"; 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; public readonly string? ShellDirectory;
@ -239,24 +239,27 @@ public class Shell
void runCommand(object? sender, DoWorkEventArgs e) void runCommand(object? sender, DoWorkEventArgs e)
{ {
#if RELEASE
try try
{ {
#endif
command.Invoke(args); command.Invoke(args);
#if RELEASE
} }
#if RELEASE
catch (TargetInvocationException ex) catch (TargetInvocationException ex)
{ {
Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red); Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red);
if (LoadingBar.Enabled) LoadingBar.End(); if (LoadingBar.Enabled) LoadingBar.End();
} }
#endif
catch (Exception ex) catch (Exception ex)
{ {
#if RELEASE
Write($"[ERROR] {ex.Message}", ConsoleColor.Red); Write($"[ERROR] {ex.Message}", ConsoleColor.Red);
if (LoadingBar.Enabled) LoadingBar.End(); if (LoadingBar.Enabled) LoadingBar.End();
} #else
Write($"[ERROR] {ex}", ConsoleColor.Red);
if (LoadingBar.Enabled) LoadingBar.End();
#endif #endif
}
} }
activeCommand = new(); activeCommand = new();

View File

@ -1,6 +1,6 @@
namespace SrcMod.Shell; namespace SrcMod.Shell;
internal static class Tools public static class Tools
{ {
public static JsonSerializer Serializer { get; private set; } public static JsonSerializer Serializer { get; private set; }