diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4dc7a3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Visual Studio stuff +.vs/ +SrcMod/.vs/ +*.sln + +# Compiled Files +SrcMod/Compiled +SrcMod/Shell/obj/ diff --git a/README.md b/README.md index a7264ad..90fe2b2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ # SrcMod - SrcMod *will be* a command-line Source Engine modding tool. + +## About +SrcMod is a command-line [Source Engine](https://developer.valvesoftware.com/wiki/Source) modding tool. It's currently in alpha development and will be developed further in the future. + +--- + +SrcMod is a tool I originally decided to make so I wouldn't have to do all of the setup and finicky stuff required to develop a Source Engine mod. It runs its own shell and has a set of commands that can be entered. This tool is expected to near a working product by the beginning of summer. No promises though. + +## Roadmap +I don't have any specific deadlines for anything but the first official release, which will *hopefully* be around the beginning of summer. However, currently the shell is in a state where no actual modding functionality is implemented and it's just a super simple shell. During the alpha releases, it will remain that way. When modding functionality is finally added, the development will shift into beta development. + +In the official releases, an installer will accompany the shell, but in the alpha and beta releases the shell will have to be installed manually. The exact directory of the shell doesn't particularly matter. + +--- + +More to come! diff --git a/SrcMod/Assets/Logo 128p.png b/SrcMod/Assets/Logo 128p.png new file mode 100644 index 0000000..27f023e Binary files /dev/null and b/SrcMod/Assets/Logo 128p.png differ diff --git a/SrcMod/Assets/Logo 16p.png b/SrcMod/Assets/Logo 16p.png new file mode 100644 index 0000000..ec11779 Binary files /dev/null and b/SrcMod/Assets/Logo 16p.png differ diff --git a/SrcMod/Assets/Logo 256p.png b/SrcMod/Assets/Logo 256p.png new file mode 100644 index 0000000..a8b8740 Binary files /dev/null and b/SrcMod/Assets/Logo 256p.png differ diff --git a/SrcMod/Assets/Logo 32p.png b/SrcMod/Assets/Logo 32p.png new file mode 100644 index 0000000..3ef1c5a Binary files /dev/null and b/SrcMod/Assets/Logo 32p.png differ diff --git a/SrcMod/Assets/Logo 64p.png b/SrcMod/Assets/Logo 64p.png new file mode 100644 index 0000000..37cb835 Binary files /dev/null and b/SrcMod/Assets/Logo 64p.png differ diff --git a/SrcMod/Assets/Logo 8p.png b/SrcMod/Assets/Logo 8p.png new file mode 100644 index 0000000..e318f36 Binary files /dev/null and b/SrcMod/Assets/Logo 8p.png differ diff --git a/SrcMod/Assets/Logo Max.png b/SrcMod/Assets/Logo Max.png new file mode 100644 index 0000000..0dec4e3 Binary files /dev/null and b/SrcMod/Assets/Logo Max.png differ diff --git a/SrcMod/Assets/Logo.ico b/SrcMod/Assets/Logo.ico new file mode 100644 index 0000000..5d7b974 Binary files /dev/null and b/SrcMod/Assets/Logo.ico differ diff --git a/SrcMod/Shell/Game.cs b/SrcMod/Shell/Game.cs new file mode 100644 index 0000000..1a21734 --- /dev/null +++ b/SrcMod/Shell/Game.cs @@ -0,0 +1,19 @@ +namespace SrcMod.Shell; + +public class Game +{ + public static readonly Game Portal2 = new() + { + Name = "Portal 2", + NameId = "portal2", + SteamId = 620 + }; + + public required string Name { get; init; } + public required string NameId { get; init; } + public required int SteamId { get; init; } + + private Game() { } + + public override string ToString() => Name; +} diff --git a/SrcMod/Shell/GlobalUsings.cs b/SrcMod/Shell/GlobalUsings.cs new file mode 100644 index 0000000..a019627 --- /dev/null +++ b/SrcMod/Shell/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using Nerd_STF.Mathematics; +global using SrcMod.Shell; +global using SrcMod.Shell.Modules.ObjectModels; +global using System; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Reflection; +global using static SrcMod.Shell.Tools; diff --git a/SrcMod/Shell/HistoryItem.cs b/SrcMod/Shell/HistoryItem.cs new file mode 100644 index 0000000..7f9ce1b --- /dev/null +++ b/SrcMod/Shell/HistoryItem.cs @@ -0,0 +1,17 @@ +namespace SrcMod.Shell; + +public struct HistoryItem +{ + public required Action action; + public required string name; + public DateTime timestamp; + + public HistoryItem() + { + timestamp = DateTime.Now; + } + + public void Invoke() => action.Invoke(); + + public override string ToString() => $"{timestamp:MM/dd/yyyy HH:mm:ss} | {name}"; +} diff --git a/SrcMod/Shell/Logo.ico b/SrcMod/Shell/Logo.ico new file mode 100644 index 0000000..5d7b974 Binary files /dev/null and b/SrcMod/Shell/Logo.ico differ diff --git a/SrcMod/Shell/Mod.cs b/SrcMod/Shell/Mod.cs new file mode 100644 index 0000000..b4472d5 --- /dev/null +++ b/SrcMod/Shell/Mod.cs @@ -0,0 +1,25 @@ +namespace SrcMod.Shell; + +public class Mod +{ + public string Name { get; set; } + + private Mod() + { + Name = string.Empty; + } + + public static Mod? ReadDirectory(string dir) + { + if (!File.Exists(dir + "\\GameInfo.txt")) return null; + + Mod mod = new() + { + Name = dir.Split("\\").Last() + }; + + return mod; + } + + public override string ToString() => Name; +} diff --git a/SrcMod/Shell/Modules/BaseModule.cs b/SrcMod/Shell/Modules/BaseModule.cs new file mode 100644 index 0000000..5b830ce --- /dev/null +++ b/SrcMod/Shell/Modules/BaseModule.cs @@ -0,0 +1,258 @@ +using System.IO; +using System.IO.Compression; + +namespace SrcMod.Shell.Modules; + +[Module("base", false)] +public static class BaseModule +{ + [Command("cd")] + public static void ChangeDirectory(string newLocalPath) + { + string curDir = Program.Shell!.WorkingDirectory, + newDir = Path.GetFullPath(Path.Combine(curDir, newLocalPath)); + Program.Shell!.UpdateWorkingDirectory(newDir); + Environment.CurrentDirectory = newDir; + } + + [Command("clear")] + public static void ClearConsole() + { + Console.Clear(); + Console.Write("\x1b[3J"); + } + + [Command("compress")] + public static void CompressFile(CompressedFileType type, string source, string? destination = null, + CompressionLevel level = CompressionLevel.Optimal) + { + destination ??= Path.Combine(Path.GetDirectoryName(source)!, + $"{Path.GetFileNameWithoutExtension(source)}.{type.ToString().ToLower()}"); + + string absSource = Path.GetFullPath(source), + absDest = Path.GetFullPath(destination); + + switch (type) + { + case CompressedFileType.Zip: + if (File.Exists(source)) + { + if (File.Exists(destination)) throw new($"File already exists at \"{destination}\""); + string message = $"Compressing file at \"{source}\" into \"{destination}\"..."; + Write(message); + + Stream writer = new FileStream(absDest, FileMode.CreateNew); + ZipArchive archive = new(writer, ZipArchiveMode.Create); + + archive.CreateEntryFromFile(absSource, Path.GetFileName(absSource)); + + archive.Dispose(); + writer.Dispose(); + + Console.CursorLeft = 0; + Console.CursorTop -= (message.Length / Console.BufferWidth) + 1; + Write(new string(' ', message.Length), newLine: false); + } + else if (Directory.Exists(source)) + { + if (File.Exists(destination)) throw new($"File already exists at \"{destination}\""); + + int consolePos = Console.CursorTop; + Write($"Compressing folder at \"{source}\" into \"{destination}\"..."); + + Stream writer = new FileStream(absDest, FileMode.CreateNew); + ZipArchive archive = new(writer, ZipArchiveMode.Create); + + List files = new(GetAllFiles(absSource)), + relative = new(); + foreach (string f in files) relative.Add(Path.GetRelativePath(absSource, f)); + + LoadingBarStart(); + for (int i = 0; i < files.Count; i++) + { + archive.CreateEntryFromFile(files[i], relative[i], level); + LoadingBarSet((i + 1) / (float)files.Count, ConsoleColor.DarkMagenta); + Console.CursorLeft = 0; + string message = $"{relative[i]}"; + int remainder = Console.BufferWidth - message.Length; + if (remainder >= 0) message += new string(' ', remainder); + else message = $"...{message[(3 - remainder)..]}"; + + Write(message, newLine: false); + } + + archive.Dispose(); + writer.Dispose(); + + LoadingBarEnd(); + + Console.CursorLeft = 0; + Write(new string(' ', Console.BufferWidth), newLine: false); + Console.SetCursorPosition(0, Console.CursorTop - 2); + Write(new string(' ', Console.BufferWidth), newLine: false); + } + else throw new("No file or directory located at \"source\""); + break; + + default: throw new($"Unknown type: \"{type}\""); + } + + DateTime stamp = DateTime.Now; + + Program.Shell!.AddHistory(new() + { + action = delegate + { + if (!File.Exists(absDest)) + { + Write("Looks like the job is already completed Boss.", ConsoleColor.DarkYellow); + return; + } + + FileInfo info = new(absDest); + if ((info.LastWriteTime - stamp).TotalMilliseconds >= 10) + throw new("The archive has been modified and probably shouldn't be undone."); + + File.Delete(absDest); + }, + name = $"Compressed a file or folder into a {type} archive located at \"{destination}\"" + }); + } + + [Command("del")] + public static void Delete(string path) + { + if (File.Exists(path)) + { + string tempFile = Path.GetTempFileName(); + File.Delete(tempFile); + File.Copy(path, tempFile); + File.Delete(path); + + Program.Shell!.AddHistory(new() + { + action = delegate + { + if (File.Exists(path)) throw new("Can't overwrite already existing file."); + File.Copy(tempFile, path); + File.Delete(tempFile); + }, + name = $"Deleted file \"{Path.GetFileName(path)}\"" + }); + } + else if (Directory.Exists(path)) + { + string[] parts = path.Replace("/", "\\").Split('\\', + StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + DirectoryInfo tempDir = Directory.CreateTempSubdirectory(); + Directory.Delete(tempDir.FullName); + Directory.Move(path, tempDir.FullName); + + Program.Shell!.AddHistory(new() + { + action = delegate + { + if (Directory.Exists(path)) throw new("Can't overwrite already existing file."); + Directory.Move(tempDir.FullName, path); + }, + name = $"Deleted directory \"{parts.Last()}\"" + }); + } + else throw new($"No file or directory exists at \"{path}\""); + } + + [Command("dir")] + public static void ListFilesAndDirs(string path = ".") + { + string[] dirs = Directory.GetDirectories(path), + files = Directory.GetFiles(path); + + List lines = new(); + + int longestName = 0, + longestSize = 0; + foreach (string d in dirs) if (d.Length > longestName) longestName = d.Length; + foreach (string f in files) + { + FileInfo info = new(f); + if (f.Length > longestName) longestName = f.Trim().Length; + + int size = Mathf.Ceiling(MathF.Log10(info.Length)); + if (longestSize > size) longestSize = size; + } + + string header = $" Type Name{new string(' ', longestName - 4)}Date Modified File Size" + + $"{new string(' ', Mathf.Max(0, longestSize - 10) + 1)}"; + lines.Add($"{header}\n{new string('-', header.Length)}"); + + foreach (string d in dirs) + { + DirectoryInfo info = new(d); + lines.Add($" Directory {info.Name.Trim()}{new string(' ', longestName - info.Name.Trim().Length)}" + + $"{info.LastWriteTime:MM/dd/yyyy HH:mm:ss}"); + } + foreach (string f in files) + { + FileInfo info = new(f); + lines.Add($" File {info.Name.Trim()}{new string(' ', longestName - info.Name.Trim().Length)}" + + $"{info.LastWriteTime:MM/dd/yyyy HH:mm:ss} {info.Length}"); + } + + DisplayWithPages(lines); + } + + [Command("echo")] + public static void Echo(string msg) => Write(msg); + + [Command("exit")] + public static void ExitShell(int code = 0) => QuitShell(code); + + [Command("explorer")] + public static void OpenExplorer(string path = ".") => Process.Start("explorer.exe", Path.GetFullPath(path)); + + [Command("history")] + public static void ShowHistory() + { + List lines = new(); + for (int i = lines.Count - 1; i >= 0; i--) lines.Add(Program.Shell!.History[i].ToString()); + DisplayWithPages(lines); + } + + [Command("permdel")] + public static void ReallyDelete(string path) + { + if (File.Exists(path)) File.Delete(path); + else if (Directory.Exists(path)) Directory.Delete(path); + else throw new($"No file or directory exists at \"{path}\""); + } + + [Command("quit")] + public static void QuitShell(int code = 0) + { + Environment.Exit(code); + } + + [Command("undo")] + public static void UndoCommand(int amount = 1) + { + for (int i = 0; i < amount; i++) + { + if (Program.Shell!.History.Count < 1) + { + if (i == 0) throw new("No operations to undo."); + else + { + Write("No more operations to undo.", ConsoleColor.DarkYellow); + break; + } + } + Program.Shell!.UndoItem(); + } + } + + public enum CompressedFileType + { + Zip + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/CommandAttribute.cs b/SrcMod/Shell/Modules/ObjectModels/CommandAttribute.cs new file mode 100644 index 0000000..9d39d52 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/CommandAttribute.cs @@ -0,0 +1,12 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class CommandAttribute : Attribute +{ + public readonly string NameId; + + public CommandAttribute(string nameId) + { + NameId = nameId; + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs b/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs new file mode 100644 index 0000000..7dee2d0 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/CommandInfo.cs @@ -0,0 +1,69 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +public class CommandInfo +{ + public required ModuleInfo Module { get; init; } + public required MethodInfo Method { get; init; } + public string Name { get; private set; } + public string NameId { get; private set; } + public ParameterInfo[] Parameters { get; private set; } + public int RequiredParameters { get; private set; } + + private CommandInfo() + { + Name = string.Empty; + NameId = string.Empty; + Parameters = Array.Empty(); + RequiredParameters = 0; + } + + public static CommandInfo? FromMethod(ModuleInfo parentModule, MethodInfo info) + { + CommandAttribute? attribute = info.GetCustomAttribute(); + if (attribute is null) return null; + + if (info.ReturnType != typeof(void)) return null; + ParameterInfo[] param = info.GetParameters(); + + int required = 0; + while (required < param.Length && !param[required].IsOptional) required++; + + return new() + { + Method = info, + Module = parentModule, + Name = info.Name, + NameId = attribute.NameId, + Parameters = param, + RequiredParameters = required + }; + } + + public void Invoke(params string[] args) + { + if (args.Length < RequiredParameters) throw new("Too few arguments. You must supply at least " + + $"{RequiredParameters}."); + if (args.Length > Parameters.Length) throw new("Too many parameters. You must supply no more than " + + $"{Parameters.Length}."); + + object?[] invokes = new object?[Parameters.Length]; + for (int i = 0; i < invokes.Length; i++) + { + if (i < args.Length) + { + string msg = args[i]; + Type paramType = Parameters[i].ParameterType; + object? val = TypeParsers.ParseAll(msg); + if (val is string && paramType.IsEnum) + { + if (Enum.TryParse(paramType, msg, true, out object? possible)) val = possible; + } + val = Convert.ChangeType(val, paramType); + invokes[i] = val; + } + else invokes[i] = Parameters[i].DefaultValue; + } + + Method.Invoke(Module.Instance, invokes); + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/ModuleAttribute.cs b/SrcMod/Shell/Modules/ObjectModels/ModuleAttribute.cs new file mode 100644 index 0000000..fae68b3 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/ModuleAttribute.cs @@ -0,0 +1,14 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ModuleAttribute : Attribute +{ + public readonly bool NameIsPrefix; + public readonly string NameId; + + public ModuleAttribute(string nameId, bool nameIsPrefix = true) + { + NameId = nameId; + NameIsPrefix = nameIsPrefix; + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/ModuleInfo.cs b/SrcMod/Shell/Modules/ObjectModels/ModuleInfo.cs new file mode 100644 index 0000000..1200c83 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/ModuleInfo.cs @@ -0,0 +1,49 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +public class ModuleInfo +{ + public List Commands { get; init; } + public object? Instance { get; init; } + public string Name { get; init; } + public string NameId { get; init; } + public bool NameIsPrefix { get; init; } + public required Type Type { get; init; } + + private ModuleInfo() + { + Commands = new(); + Instance = null; + Name = string.Empty; + NameId = string.Empty; + NameIsPrefix = true; + } + + public static ModuleInfo? FromModule(Type info) + { + ModuleAttribute? attribute = info.GetCustomAttribute(); + if (attribute is null) return null; + + object? instance = info.IsAbstract ? null : Activator.CreateInstance(info); + + ModuleInfo module = new() + { + Instance = instance, + Name = info.Name, + NameId = attribute.NameId, + NameIsPrefix = attribute.NameIsPrefix, + Type = info + }; + + List commands = new(); + foreach (MethodInfo method in info.GetMethods()) + { + CommandInfo? cmd = CommandInfo.FromMethod(module, method); + if (cmd is null) continue; + commands.Add(cmd); + } + + module.Commands.AddRange(commands); + + return module; + } +} diff --git a/SrcMod/Shell/Modules/ObjectModels/TypeParsers.cs b/SrcMod/Shell/Modules/ObjectModels/TypeParsers.cs new file mode 100644 index 0000000..cce44b9 --- /dev/null +++ b/SrcMod/Shell/Modules/ObjectModels/TypeParsers.cs @@ -0,0 +1,36 @@ +namespace SrcMod.Shell.Modules.ObjectModels; + +public static class TypeParsers +{ + public static object ParseAll(string msg) + { + if (TryParse(msg, out sbyte int8)) return int8; + if (TryParse(msg, out byte uInt8)) return uInt8; + if (TryParse(msg, out short int16)) return int16; + if (TryParse(msg, out ushort uInt16)) return uInt16; + if (TryParse(msg, out int int32)) return int32; + if (TryParse(msg, out uint uInt32)) return uInt32; + if (TryParse(msg, out long int64)) return int64; + if (TryParse(msg, out ulong uInt64)) return uInt64; + if (TryParse(msg, out Int128 int128)) return int128; + if (TryParse(msg, out UInt128 uInt128)) return uInt128; + if (TryParse(msg, out nint intPtr)) return intPtr; + if (TryParse(msg, out nuint uIntPtr)) return uIntPtr; + if (TryParse(msg, out Half float16)) return float16; + if (TryParse(msg, out float float32)) return float32; + if (TryParse(msg, out double float64)) return float64; + if (TryParse(msg, out decimal float128)) return float128; + if (TryParse(msg, out char resChar)) return resChar; + if (TryParse(msg, out DateOnly dateOnly)) return dateOnly; + if (TryParse(msg, out DateTime dateTime)) return dateTime; + if (TryParse(msg, out DateTime dateTimeOffset)) return dateTimeOffset; + if (TryParse(msg, out Guid guid)) return guid; + if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly; + if (TryParse(msg, out TimeOnly timeSpan)) return timeSpan; + + return msg; + } + + public static bool TryParse(string msg, out T? result) where T : IParsable + => T.TryParse(msg, null, out result); +} diff --git a/SrcMod/Shell/Program.cs b/SrcMod/Shell/Program.cs new file mode 100644 index 0000000..7aa8f7d --- /dev/null +++ b/SrcMod/Shell/Program.cs @@ -0,0 +1,25 @@ +namespace SrcMod.Shell; + +public static class Program +{ + public static Shell? Shell { get; private set; } + + public static void Main(string[] args) + { + Console.Clear(); + + // Check for arguments and send a warning if they are found. + // In the future, I may use these arguments. + if (args.Length != 0) Write("[WARNING] You have supplied this shell " + + "with arguments. They will be ignored.", ConsoleColor.DarkYellow); + + Shell = new(); + + while (true) + { + string cmd = Shell.ReadLine(); + Shell.InvokeCommand(cmd); + Shell.ReloadDirectoryInfo(); + } + } +} diff --git a/SrcMod/Shell/Shell.cs b/SrcMod/Shell/Shell.cs new file mode 100644 index 0000000..661f7c7 --- /dev/null +++ b/SrcMod/Shell/Shell.cs @@ -0,0 +1,190 @@ +namespace SrcMod.Shell; + +public class Shell +{ + public const string Author = "That_One_Nerd"; + public const string Name = "SrcMod"; + public const string Version = "Alpha 0.1.0"; + + public readonly string? ShellDirectory; + + public List LoadedCommands; + public List LoadedModules; + + public Game? ActiveGame; + public Mod? ActiveMod; + public List History; + public string WorkingDirectory; + + public Shell() + { + Console.CursorVisible = false; + + // Get shell directory and compare it to the path variable. + Assembly assembly = Assembly.GetExecutingAssembly(); + string assemblyPath = assembly.Location; + + if (string.IsNullOrWhiteSpace(assemblyPath) || !File.Exists(assemblyPath)) ShellDirectory = null; + + ShellDirectory = Path.GetDirectoryName(assemblyPath)!.Replace("/", "\\"); + if (ShellDirectory is null) Write("[ERROR] There was a problem detecting the shell's location. " + + "Many featues will be disabled.", ConsoleColor.Red); + + // Check if the path in the PATH variable is correct. + string envVal = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User)!; + string[] pathParts = envVal.Split(";"); + if (ShellDirectory is not null && !pathParts.Contains(ShellDirectory)) + { + envVal += $"{ShellDirectory};"; + Environment.SetEnvironmentVariable("PATH", envVal, EnvironmentVariableTarget.User); + Write($"[WARNING] The environment PATH does not contain the {Name} directory. It has now been added " + + "automatically. Is this your first time running the shell?", ConsoleColor.DarkYellow); + } + + WorkingDirectory = Directory.GetCurrentDirectory(); + + // Load modules and commands. + LoadedModules = new(); + LoadedCommands = new(); + + Type[] possibleModules = assembly.GetTypes(); + foreach (Type t in possibleModules) + { + ModuleInfo? module = ModuleInfo.FromModule(t); + if (module is not null) + { + LoadedModules.Add(module); + LoadedCommands.AddRange(module.Commands); + } + } + + // Other stuff + History = new(); + + // Send welcome message. + Write("\nWelcome to ", ConsoleColor.White, false); + Write($"{Name} {Version}", ConsoleColor.DarkCyan, false); + Write(" by ", ConsoleColor.White, false); + Write($"{Author}", ConsoleColor.DarkYellow); + + ActiveGame = null; + + ReloadDirectoryInfo(); + } + + public void AddHistory(HistoryItem item) => History.Add(item); + public void UndoItem() + { + HistoryItem item = History.Last(); + item.Invoke(); + History.RemoveAt(History.Count - 1); + Write($"Undid \"", newLine: false); + Write(item.name, ConsoleColor.White, false); + Write("\""); + } + + public void UpdateWorkingDirectory(string dir) + { + string global = Path.GetFullPath(dir.Replace("/", "\\"), WorkingDirectory); + + Directory.SetCurrentDirectory(global); + WorkingDirectory = global; + ReloadDirectoryInfo(); + } + + public string ReadLine() + { + Console.CursorVisible = true; + + Write($"\n{WorkingDirectory}", ConsoleColor.DarkGreen, false); + if (ActiveGame is not null) Write($" {ActiveGame}", ConsoleColor.DarkYellow, false); + if (ActiveMod is not null) Write($" {ActiveMod}", ConsoleColor.Magenta, false); + Write(null); + + Write($" {Name}", ConsoleColor.DarkCyan, false); + Write(" > ", ConsoleColor.White, false); + + Console.ForegroundColor = ConsoleColor.White; + string message = Console.ReadLine()!; + Console.ResetColor(); + + Console.CursorVisible = false; + + return message; + } + + public void InvokeCommand(string cmd) + { + List parts = new(); + string active = string.Empty; + + bool inQuotes = false; + for (int i = 0; i < cmd.Length; i++) + { + char c = cmd[i]; + + if (c == '\"' && i > 0 && cmd[i - 1] != '\\') inQuotes = !inQuotes; + else if (c == ' ' && !inQuotes) + { + if (string.IsNullOrWhiteSpace(active)) continue; + if (active.StartsWith('\"') && active.EndsWith('\"')) active = active[1..^1]; + parts.Add(active); + active = string.Empty; + } + else active += c; + } + if (!string.IsNullOrWhiteSpace(active)) + { + if (active.StartsWith('\"') && active.EndsWith('\"')) active = active[1..^1]; + parts.Add(active); + } + + if (parts.Count < 1) return; + + string moduleName = parts[0].Trim().ToLower(); + foreach (ModuleInfo module in LoadedModules) + { + if (module.NameIsPrefix && module.NameId.Trim().ToLower() != moduleName) continue; + string commandName; + if (module.NameIsPrefix) + { + if (parts.Count < 2) continue; + commandName = parts[1].Trim().ToLower(); + } + else commandName = moduleName; + + foreach (CommandInfo command in module.Commands) + { + if (command.NameId.Trim().ToLower() != commandName) continue; + int start = module.NameIsPrefix ? 2 : 1; + string[] args = parts.GetRange(start, parts.Count - start).ToArray(); + + try + { + command.Invoke(args); + } + catch (TargetInvocationException ex) + { + Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red); + } + catch (Exception ex) + { + Write($"[ERROR] {ex.Message}", ConsoleColor.Red); + } + return; + } + } + + Write($"[ERROR] Could not find command \"{cmd}\".", ConsoleColor.Red); + } + + public void ReloadDirectoryInfo() + { + ActiveMod = Mod.ReadDirectory(WorkingDirectory); + + // Update title. + string title = "SrcMod"; + if (ActiveMod is not null) title += $" - {ActiveMod.Name}"; + Console.Title = title; + } +} diff --git a/SrcMod/Shell/Shell.csproj b/SrcMod/Shell/Shell.csproj new file mode 100644 index 0000000..0fdb552 --- /dev/null +++ b/SrcMod/Shell/Shell.csproj @@ -0,0 +1,35 @@ + + + + Exe + net7.0 + disable + enable + srcmod + SrcMod.Shell + ../Compiled/Shell + SrcMod Shell + That_One_Nerd + false + Logo.ico + + + + embedded + 9999 + + + + embedded + 9999 + + + + + + + + + + + diff --git a/SrcMod/Shell/Tools.cs b/SrcMod/Shell/Tools.cs new file mode 100644 index 0000000..efdd61d --- /dev/null +++ b/SrcMod/Shell/Tools.cs @@ -0,0 +1,129 @@ +using System.Net.Http.Headers; + +namespace SrcMod.Shell; + +public static class Tools +{ + private static int loadingPosition = -1; + private static int lastBufferSize = 0; + private static int lastValue = -1; + + public static void DisplayWithPages(IEnumerable lines, ConsoleColor? color = null) + { + int written = 0; + bool multiPage = false, hasQuit = false; + foreach (string line in lines) + { + if (written == Console.BufferHeight - 2) + { + multiPage = true; + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Black; + + Console.Write(" -- More -- "); + ConsoleKey key = Console.ReadKey(true).Key; + Console.ResetColor(); + + Console.CursorLeft = 0; + + if (key == ConsoleKey.Q) + { + hasQuit = true; + break; + } + if (key == ConsoleKey.Spacebar) written = 0; + else written--; + } + Write(line, color); + written++; + } + + if (multiPage) + { + Console.BackgroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.DarkGreen; + + Console.Write(" -- End -- "); + while (!hasQuit && Console.ReadKey(true).Key != ConsoleKey.Q) ; + + Console.ResetColor(); + + Console.CursorLeft = 0; + Console.Write(" "); + } + } + + public static IEnumerable GetAllFiles(string directory) + { + List allFiles = new(); + foreach (string f in Directory.GetFiles(directory)) allFiles.Add(Path.GetFullPath(f)); + foreach (string dir in Directory.GetDirectories(directory)) allFiles.AddRange(GetAllFiles(dir)); + return allFiles; + } + + public static void LoadingBarEnd(bool clear = true) + { + if (loadingPosition == -1) throw new("No loading bar is active."); + + if (clear) + { + Int2 oldPos = (Console.CursorLeft, Console.CursorTop); + + Console.CursorLeft = 0; + Console.CursorTop = loadingPosition; + Console.Write(new string(' ', Console.BufferWidth)); + Console.CursorLeft = 0; + + Console.SetCursorPosition(oldPos.x, oldPos.y); + } + loadingPosition = -1; + } + public static void LoadingBarSet(float value, ConsoleColor? color = null) + { + const string left = " --- [", + right = "] --- "; + int barSize = Console.BufferWidth - left.Length - right.Length, + filled = (int)(barSize * value); + + if (filled == lastValue) return; + + Int2 oldPos = (Console.CursorLeft, Console.CursorTop); + + // Erase last bar. + Console.SetCursorPosition(0, loadingPosition); + Console.Write(new string(' ', lastBufferSize)); + Console.CursorLeft = 0; + + // Add new bar. + lastBufferSize = Console.BufferWidth; + + Write(left, newLine: false); + ConsoleColor oldFore = Console.ForegroundColor; + + if (color is not null) Console.ForegroundColor = color.Value; + Write(new string('=', filled), newLine: false); + if (color is not null) Console.ForegroundColor = oldFore; + Write(new string(' ', barSize - filled), newLine: false); + Write(right, newLine: false); + + if (oldPos.y == Console.CursorTop) oldPos.y++; + Console.SetCursorPosition(oldPos.x, oldPos.y); + } + public static void LoadingBarStart(float value = 0, int? position = null, ConsoleColor? color = null) + { + if (loadingPosition != -1) throw new("The loading bar has already been enabled."); + loadingPosition = position ?? Console.CursorTop; + LoadingBarSet(value, color); + } + + public static void Write(object? message, ConsoleColor? col = null, bool newLine = true) + { + ConsoleColor prevCol = Console.ForegroundColor; + if (col is not null) Console.ForegroundColor = col.Value; + + if (newLine) Console.WriteLine(message); + else Console.Write(message); + + Console.ForegroundColor = prevCol; + } +}