Compare commits
98 Commits
v0.2.0-alp
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f479f63662 | |||
| 8804ac08d9 | |||
| 287cd26bcb | |||
| 3a961fffbb | |||
| 7cad137f39 | |||
| 82afefd3e7 | |||
| 858bd580b6 | |||
| dfda870eac | |||
| f25b366a4d | |||
| 5b2b0abfa3 | |||
| 2abadc4071 | |||
| be6e4a496b | |||
| 62d3ba6492 | |||
| 7854a576a7 | |||
| 92b22d6749 | |||
| 45e23b9924 | |||
| c0afa7dbb4 | |||
| 321d6acb7f | |||
| eee6306428 | |||
| bcf02f4ecd | |||
| d83c16827f | |||
| 13c9adf781 | |||
| 7c96f30d1b | |||
| c1a90cfc99 | |||
| ab0453e0ab | |||
| 7e1492c664 | |||
| ab372b3eec | |||
| 154d21ee13 | |||
| 3c0c3068a4 | |||
| 856811687e | |||
| 71edd4fffa | |||
| e6198cd035 | |||
| 7428ac9110 | |||
| 1ee60cdf65 | |||
| bb520424ac | |||
| 54ea2b5203 | |||
| 20413603e3 | |||
| 03128d3a57 | |||
| 46f4c779d2 | |||
| 4e2a8ec05c | |||
| 4d64b0bae6 | |||
| 864c39be7f | |||
| 97a799e10d | |||
| a0c50c4cab | |||
| 2f8565529f | |||
| 3f5319adc0 | |||
| 2e61bcfbec | |||
| 6f65417fe7 | |||
| 2dc37cd6ac | |||
| d8df50a928 | |||
| 35885b8724 | |||
| b446feefe7 | |||
| 409817f208 | |||
| 2420e16fa4 | |||
| b5517e724a | |||
| dd62e66871 | |||
| 647993ef1f | |||
| 318c8119a0 | |||
| 8677ff464d | |||
| aac52a43ac | |||
| 25c6d152a2 | |||
| fd4162aa32 | |||
| 3ecb967b91 | |||
| 4d2e98ce42 | |||
| b869de9be8 | |||
| b7a4333e2d | |||
| ec15e78564 | |||
| 8bfd8c142c | |||
| 4d342ae958 | |||
| 3742b4a230 | |||
| 0c07c9cfe5 | |||
| b2bc9fa7ee | |||
| 68336df868 | |||
| 24fde24746 | |||
| a2b9daa107 | |||
| daae786bec | |||
| e8a5cbc846 | |||
| 4c919a152c | |||
| 21b7436f0e | |||
| a7ff0a7ab4 | |||
| 1e5f0dba4e | |||
| 5412b023a0 | |||
| 51f58e6e82 | |||
| 559190158a | |||
| ce72491109 | |||
| 471bb54fe0 | |||
| 33130bc5f8 | |||
| 8f7c2775d4 | |||
| dda3ccad4d | |||
| 87166acda3 | |||
| 9738599088 | |||
| 9928f7d62f | |||
| 506892b6e0 | |||
| f95e3a89ff | |||
| b5cfa6f0cc | |||
| 0ebecf4234 | |||
| 8170a86247 | |||
| a1d95170bd |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,11 +1,13 @@
|
||||
# Visual Studio stuff
|
||||
.vs/
|
||||
SrcMod/.vs/
|
||||
*.sln
|
||||
|
||||
# Compiled Files
|
||||
SrcMod/Compiled
|
||||
SrcMod/Shell/obj/
|
||||
|
||||
SrcMod/Valve.NET/obj
|
||||
|
||||
# Personal Stuff
|
||||
SrcMod/Shell/Modules/TestingModule.cs
|
||||
TODO.md
|
||||
|
||||
33
SrcMod/Shell/Extensions/ConversionExtension.cs
Normal file
33
SrcMod/Shell/Extensions/ConversionExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
namespace SrcMod.Shell;
|
||||
|
||||
public class Game
|
||||
public class Game : IEquatable<Game>
|
||||
{
|
||||
public static readonly Game Portal2 = new()
|
||||
{
|
||||
@ -8,12 +8,48 @@ public class Game
|
||||
NameId = "portal2",
|
||||
SteamId = 620
|
||||
};
|
||||
public static readonly Game Unknown = new()
|
||||
{
|
||||
Name = "Unknown Game",
|
||||
NameId = "unknown",
|
||||
SteamId = -1,
|
||||
IsUnknown = true
|
||||
};
|
||||
|
||||
public required string Name { get; init; }
|
||||
public required string NameId { get; init; }
|
||||
public required int SteamId { get; init; }
|
||||
public string Name { get; private set; }
|
||||
public string NameId { get; private set; }
|
||||
public int SteamId { get; private set; }
|
||||
|
||||
private Game() { }
|
||||
public bool IsUnknown { get; private set; }
|
||||
|
||||
private Game()
|
||||
{
|
||||
IsUnknown = false;
|
||||
Name = string.Empty;
|
||||
NameId = string.Empty;
|
||||
}
|
||||
|
||||
public static Game FromSteamId(int id)
|
||||
{
|
||||
if (id == Portal2.SteamId) return Portal2;
|
||||
else
|
||||
{
|
||||
Game game = (Game)Unknown.MemberwiseClone();
|
||||
game.SteamId = id;
|
||||
return game;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is Game game) return Equals(game);
|
||||
return false;
|
||||
}
|
||||
public bool Equals(Game? other) => other is not null && SteamId == other.SteamId;
|
||||
public override int GetHashCode() => base.GetHashCode();
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public static bool operator ==(Game a, Game b) => a.Equals(b);
|
||||
public static bool operator !=(Game a, Game b) => !a.Equals(b);
|
||||
}
|
||||
|
||||
25
SrcMod/Shell/Interop/Winmm.cs
Normal file
25
SrcMod/Shell/Interop/Winmm.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace SrcMod.Shell.Interop;
|
||||
|
||||
internal static partial class Winmm
|
||||
{
|
||||
[LibraryImport("winmm.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static partial bool PlaySound([MarshalAs(UnmanagedType.LPStr)] string pszSound, nint hMod, uint fdwSound);
|
||||
|
||||
public enum PlaySoundFlags : uint
|
||||
{
|
||||
Sync = 0x00000000,
|
||||
Async = 0x00000001,
|
||||
NoDefault = 0x00000002,
|
||||
Memory = 0x00000004,
|
||||
Loop = 0x00000008,
|
||||
NoStop = 0x00000010,
|
||||
Purge = 0x00000040,
|
||||
Application = 0x00000080,
|
||||
NoWait = 0x00002000,
|
||||
Alias = 0x00010000,
|
||||
FileName = 0x00020000,
|
||||
Resource = 0x00040000,
|
||||
AliasId = 0x00100000,
|
||||
}
|
||||
}
|
||||
79
SrcMod/Shell/LoadingBar.cs
Normal file
79
SrcMod/Shell/LoadingBar.cs
Normal file
@ -0,0 +1,79 @@
|
||||
namespace SrcMod.Shell;
|
||||
|
||||
public static class LoadingBar
|
||||
{
|
||||
public static int position = -1;
|
||||
public static int bufferSize = 0;
|
||||
public static int lastValue = -1;
|
||||
public static float value = 0;
|
||||
public static ConsoleColor color = Console.ForegroundColor;
|
||||
|
||||
public static bool Enabled { get; private set; }
|
||||
|
||||
public static void End(bool clear = true)
|
||||
{
|
||||
if (position == -1) throw new("No loading bar is active.");
|
||||
|
||||
if (clear)
|
||||
{
|
||||
Int2 oldPos = (Console.CursorLeft, Console.CursorTop);
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Console.CursorTop = position;
|
||||
Console.Write(new string(' ', Console.BufferWidth));
|
||||
Console.CursorLeft = 0;
|
||||
|
||||
Console.SetCursorPosition(oldPos.x, oldPos.y);
|
||||
}
|
||||
position = -1;
|
||||
Enabled = false;
|
||||
}
|
||||
public static void Set(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;
|
||||
lastValue = filled;
|
||||
|
||||
Int2 oldPos = (Console.CursorLeft, Console.CursorTop);
|
||||
|
||||
LoadingBar.value = value;
|
||||
LoadingBar.color = color ?? Console.ForegroundColor;
|
||||
|
||||
// Erase last bar.
|
||||
Console.SetCursorPosition(0, position);
|
||||
Console.Write(new string(' ', bufferSize));
|
||||
Console.CursorLeft = 0;
|
||||
|
||||
// Add new bar.
|
||||
bufferSize = 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++;
|
||||
while (oldPos.y >= Console.BufferHeight)
|
||||
{
|
||||
Console.WriteLine();
|
||||
oldPos.y--;
|
||||
position--;
|
||||
}
|
||||
Console.SetCursorPosition(oldPos.x, oldPos.y);
|
||||
}
|
||||
public static void Start(float value = 0, int? position = null, ConsoleColor? color = null)
|
||||
{
|
||||
if (LoadingBar.position != -1) throw new("The loading bar has already been enabled.");
|
||||
LoadingBar.position = position ?? Console.CursorTop;
|
||||
Enabled = true;
|
||||
Set(value, color);
|
||||
}
|
||||
}
|
||||
28
SrcMod/Shell/Miscellaneous/GlobalUsings.cs
Normal file
28
SrcMod/Shell/Miscellaneous/GlobalUsings.cs
Normal file
@ -0,0 +1,28 @@
|
||||
global using Microsoft.Win32;
|
||||
global using Nerd_STF.Mathematics;
|
||||
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;
|
||||
global using SrcMod.Shell.Modules.ObjectModels;
|
||||
global using SrcMod.Shell.ObjectModels;
|
||||
global using SrcMod.Shell.ObjectModels.Source;
|
||||
global using SrcMod.Shell.ObjectModels.Steam;
|
||||
global using System;
|
||||
global using System.Collections;
|
||||
global using System.Collections.Generic;
|
||||
global using System.ComponentModel;
|
||||
global using System.Diagnostics;
|
||||
global using System.Formats.Tar;
|
||||
global using System.IO;
|
||||
global using System.IO.Compression;
|
||||
global using System.Linq;
|
||||
global using System.Reflection;
|
||||
global using System.Runtime.InteropServices;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using Valve.Vkv;
|
||||
global using static SrcMod.Shell.Tools;
|
||||
@ -2,24 +2,183 @@
|
||||
|
||||
public class Mod
|
||||
{
|
||||
public Game BaseGame { get; set; }
|
||||
|
||||
public string? Developer { get; set; }
|
||||
public string? DeveloperUrl { get; set; }
|
||||
public Dictionary<SearchPathType, string> SearchPaths { get; set; }
|
||||
public string? ManualUrl { get; set; }
|
||||
|
||||
public string? FgdDataPath { get; set; }
|
||||
public string? IconPath { get; set; }
|
||||
public string? InstancePath { get; set; }
|
||||
|
||||
public PlayerType PlayerMode { get; set; }
|
||||
|
||||
public CrosshairFlags CrosshairMenuFlags { get; set; }
|
||||
public bool ShowDifficultyMenu { get; set; }
|
||||
public bool ShowModelMenu { get; set; }
|
||||
public bool ShowPortalMenu { get; set; }
|
||||
public SupportFlags SupportingFlags { get; set; }
|
||||
|
||||
public bool HiResModels { get; set; }
|
||||
|
||||
public string[] HiddenMaps { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public string? Motto { get; set; }
|
||||
public TitleDisplay TitleDisplayMode { get; set; }
|
||||
|
||||
public bool BuildMapNodegraphs { get; set; }
|
||||
|
||||
public Dictionary<string, string>? MapbaseLaunchOptions { get; set; }
|
||||
public string RootDirectory { get; set; }
|
||||
|
||||
private Mod()
|
||||
{
|
||||
BaseGame = Game.Unknown;
|
||||
SearchPaths = new();
|
||||
HiddenMaps = Array.Empty<string>();
|
||||
Name = string.Empty;
|
||||
RootDirectory = string.Empty;
|
||||
}
|
||||
|
||||
public static Mod FromInfo(string root, GameInfo info)
|
||||
{
|
||||
Mod curMod = new()
|
||||
{
|
||||
BaseGame = Game.FromSteamId(info.FileSystem.SteamAppID),
|
||||
BuildMapNodegraphs = info.Nodegraph is not null && info.Nodegraph.Value,
|
||||
CrosshairMenuFlags = CrosshairFlags.None,
|
||||
Developer = info.Developer,
|
||||
DeveloperUrl = info.Developer_URL,
|
||||
FgdDataPath = info.GameData,
|
||||
HiddenMaps = info.Hidden_Maps is null ? Array.Empty<string>() : info.Hidden_Maps.Keys.ToArray(),
|
||||
HiResModels = info.NoHIModel is null || !info.NoHIModel.Value,
|
||||
IconPath = info.Icon is null ? null : info.Icon.Trim().Replace('/', '\\') + ".tga",
|
||||
InstancePath = info.InstancePath,
|
||||
MapbaseLaunchOptions = info.CommandLine,
|
||||
ManualUrl = info.Manual,
|
||||
Motto = info.Title2,
|
||||
Name = string.IsNullOrEmpty(info.Title) ? "Default Mod" : info.Title,
|
||||
PlayerMode = info.Type is null ? PlayerType.Both : info.Type.Trim().ToLower() switch
|
||||
{
|
||||
"singleplayer_only" => PlayerType.Singleplayer,
|
||||
"multiplayer_only" => PlayerType.Multiplayer,
|
||||
_ => throw new ArgumentException($"Unknown type \"{info.Type}\"")
|
||||
},
|
||||
RootDirectory = root,
|
||||
SearchPaths = new(),
|
||||
ShowDifficultyMenu = info.NoDifficulty is null || !info.NoDifficulty.Value,
|
||||
ShowModelMenu = info.NoModels is null || !info.NoModels.Value,
|
||||
ShowPortalMenu = info.HasPortals is not null && info.HasPortals.Value,
|
||||
SupportingFlags = SupportFlags.None,
|
||||
TitleDisplayMode = info.GameLogo is null ? TitleDisplay.Title :
|
||||
(info.GameLogo.Value ? TitleDisplay.Logo : TitleDisplay.Title)
|
||||
};
|
||||
if (curMod.PlayerMode == PlayerType.Multiplayer && info.NoDifficulty is null)
|
||||
curMod.ShowDifficultyMenu = false;
|
||||
|
||||
if (info.NoCrosshair is null || !info.NoCrosshair.Value)
|
||||
curMod.CrosshairMenuFlags |= CrosshairFlags.ShowMultiplayer;
|
||||
if (info.AdvCrosshair is not null && info.AdvCrosshair.Value)
|
||||
curMod.CrosshairMenuFlags |= CrosshairFlags.AdvancedMenu;
|
||||
|
||||
if (info.SupportsDX8 is not null && info.SupportsDX8.Value)
|
||||
curMod.SupportingFlags |= SupportFlags.DirectX8;
|
||||
if (info.SupportsVR is not null && info.SupportsVR.Value)
|
||||
curMod.SupportingFlags |= SupportFlags.VirtualReality;
|
||||
if (info.SupportsXBox360 is not null && info.SupportsXBox360.Value)
|
||||
curMod.SupportingFlags |= SupportFlags.XBox360;
|
||||
|
||||
foreach (KeyValuePair<string, string> pair in info.FileSystem.SearchPaths)
|
||||
{
|
||||
SearchPathType type = SearchPathType.Unknown;
|
||||
string[] parts = pair.Key.Trim().ToLower().Split('+');
|
||||
foreach (string part in parts) type |= part switch
|
||||
{
|
||||
"game" => SearchPathType.Game,
|
||||
"game_write" => SearchPathType.GameWrite,
|
||||
"gamebin" => SearchPathType.GameBinaries,
|
||||
"platform" => SearchPathType.Platform,
|
||||
"mod" => SearchPathType.Mod,
|
||||
"mod_write" => SearchPathType.ModWrite,
|
||||
"default_write_path" => SearchPathType.DefaultWritePath,
|
||||
"vpk" => SearchPathType.Vpk,
|
||||
_ => SearchPathType.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
return curMod;
|
||||
}
|
||||
|
||||
public static Mod? ReadDirectory(string dir)
|
||||
{
|
||||
if (!File.Exists(dir + "\\GameInfo.txt")) return null;
|
||||
dir = dir.Trim().Replace('/', '\\');
|
||||
string check = dir;
|
||||
|
||||
Mod mod = new()
|
||||
while (!string.IsNullOrEmpty(check))
|
||||
{
|
||||
Name = dir.Split("\\").Last()
|
||||
};
|
||||
string gameInfoPath = Path.Combine(check, "GameInfo.txt");
|
||||
if (File.Exists(gameInfoPath))
|
||||
{
|
||||
// Root mod directory found, go from here.
|
||||
|
||||
return mod;
|
||||
FileStream fs = new(gameInfoPath, FileMode.Open);
|
||||
GameInfo? modInfo = SerializeVkv.Deserialize<GameInfo>(fs);
|
||||
if (modInfo is null) continue;
|
||||
|
||||
return FromInfo(check, modInfo);
|
||||
}
|
||||
|
||||
check = Path.GetDirectoryName(check) ?? string.Empty; // Go to parent folder.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
[Flags]
|
||||
public enum CrosshairFlags
|
||||
{
|
||||
None = 0,
|
||||
ShowMultiplayer = 1,
|
||||
AdvancedMenu = 2
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum SupportFlags
|
||||
{
|
||||
None,
|
||||
DirectX8 = 1,
|
||||
VirtualReality = 2,
|
||||
XBox360 = 4
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum SearchPathType
|
||||
{
|
||||
Unknown = 0,
|
||||
Game = 1,
|
||||
GameWrite = 2,
|
||||
GameBinaries = 4,
|
||||
Platform = 8,
|
||||
Mod = 16,
|
||||
ModWrite = 32,
|
||||
DefaultWritePath = 64,
|
||||
Vpk = 128
|
||||
}
|
||||
|
||||
public enum PlayerType
|
||||
{
|
||||
Singleplayer = 1,
|
||||
Multiplayer = 2,
|
||||
Both = Singleplayer | Multiplayer
|
||||
}
|
||||
public enum TitleDisplay
|
||||
{
|
||||
Title,
|
||||
Logo
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ public static class BaseModule
|
||||
|
||||
Write($"Copying directory \"{source}\" to \"{destination}\"...");
|
||||
|
||||
LoadingBarStart();
|
||||
LoadingBar.Start();
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
string file = files[i],
|
||||
@ -53,7 +53,7 @@ public static class BaseModule
|
||||
destFile = Path.Combine(destination, file);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destFile)!);
|
||||
File.Copy(sourceFile, destFile);
|
||||
LoadingBarSet((i + 1) / (float)files.Length, ConsoleColor.DarkGreen);
|
||||
LoadingBar.Set((i + 1) / (float)files.Length, ConsoleColor.DarkGreen);
|
||||
Console.CursorLeft = 0;
|
||||
string message = $"{sourceFile}";
|
||||
int remainder = Console.BufferWidth - message.Length;
|
||||
@ -63,7 +63,7 @@ public static class BaseModule
|
||||
Write(message, newLine: false);
|
||||
}
|
||||
|
||||
LoadingBarEnd();
|
||||
LoadingBar.End();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
@ -101,115 +101,6 @@ public static class BaseModule
|
||||
}
|
||||
|
||||
[Command("cut")]
|
||||
public static void CutFile(string source, string destination) => MoveFile(source, 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<string> 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<string> lines = new() { " Timestamp Description"};
|
||||
int longestName = 0;
|
||||
for (int i = lines.Count - 1; i >= 0; i--)
|
||||
{
|
||||
HistoryItem hist = Program.Shell!.History[i];
|
||||
if (hist.name.Length > longestName) longestName = hist.name.Length;
|
||||
lines.Add(hist.ToString());
|
||||
}
|
||||
lines.Insert(1, new string('-', 22 + longestName));
|
||||
DisplayWithPages(lines);
|
||||
}
|
||||
|
||||
[Command("move")]
|
||||
public static void MoveFile(string source, string destination)
|
||||
{
|
||||
@ -278,6 +169,145 @@ public static class BaseModule
|
||||
});
|
||||
}
|
||||
|
||||
[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<string> 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(info.Length.ToString().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")]
|
||||
[Command("quit")]
|
||||
public static void QuitShell(int code = 0)
|
||||
{
|
||||
Environment.Exit(code);
|
||||
}
|
||||
|
||||
[Command("explorer")]
|
||||
public static void OpenExplorer(string path = ".") => Process.Start("explorer.exe", Path.GetFullPath(path));
|
||||
|
||||
[Command("history")]
|
||||
public static void ShowHistory()
|
||||
{
|
||||
List<string> lines = new() { " Timestamp Description"};
|
||||
int longestName = 0;
|
||||
for (int i = Program.Shell!.History.Count - 1; i >= 0; i--)
|
||||
{
|
||||
HistoryItem hist = Program.Shell!.History[i];
|
||||
if (hist.name.Length > longestName) longestName = hist.name.Length;
|
||||
lines.Add(hist.ToString());
|
||||
}
|
||||
lines.Insert(1, new string('-', 22 + longestName));
|
||||
DisplayWithPages(lines);
|
||||
}
|
||||
|
||||
[Command("makedir")]
|
||||
[Command("mkdir")]
|
||||
public static void CreateDirectory(string name)
|
||||
{
|
||||
if (Directory.Exists(name)) throw new($"Directory already exists at \"{name}\"");
|
||||
Directory.CreateDirectory(name);
|
||||
}
|
||||
|
||||
[Command("makefile")]
|
||||
[Command("mkfile")]
|
||||
public static void CreateFile(string name, string? text = null)
|
||||
{
|
||||
string? dir = Path.GetDirectoryName(name);
|
||||
if (dir is null) throw new($"Cannot parse file path \"{name}\"");
|
||||
|
||||
if (File.Exists(name)) throw new($"File already exists at \"{name}\"");
|
||||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||
|
||||
FileStream stream = File.Create(name);
|
||||
if (text is not null)
|
||||
{
|
||||
StreamWriter writer = new(stream);
|
||||
writer.Write(text);
|
||||
writer.Close();
|
||||
}
|
||||
stream.Close();
|
||||
}
|
||||
|
||||
[Command("permdel")]
|
||||
public static void ReallyDelete(string path)
|
||||
{
|
||||
@ -287,6 +317,7 @@ public static class BaseModule
|
||||
}
|
||||
|
||||
[Command("print")]
|
||||
[Command("type")]
|
||||
public static void Print(string file)
|
||||
{
|
||||
if (!File.Exists(file)) throw new($"No file exists at \"{file}\"");
|
||||
@ -294,6 +325,23 @@ public static class BaseModule
|
||||
Write(reader.ReadToEnd());
|
||||
}
|
||||
|
||||
[Command("run")]
|
||||
[CanCancel(false)]
|
||||
public static void RunProcess(string name, string args = "")
|
||||
{
|
||||
Process? run = Process.Start(new ProcessStartInfo()
|
||||
{
|
||||
Arguments = args,
|
||||
CreateNoWindow = false,
|
||||
ErrorDialog = true,
|
||||
FileName = name
|
||||
});
|
||||
|
||||
if (run is null) throw new($"Error starting the process \"{name}\"");
|
||||
|
||||
run.WaitForExit();
|
||||
}
|
||||
|
||||
[Command("sleep")]
|
||||
public static void WaitTime(int timeMs) => Thread.Sleep(timeMs);
|
||||
|
||||
@ -313,13 +361,13 @@ public static class BaseModule
|
||||
|
||||
Thread.Sleep(rand.Next(500, 1000));
|
||||
|
||||
LoadingBarStart();
|
||||
LoadingBar.Start();
|
||||
|
||||
for (int i = 0; i < files.Length; i++)
|
||||
{
|
||||
FileInfo file = new(files[i]);
|
||||
Thread.Sleep((int)(rand.Next(50, 100) * (file.Length >> 20)));
|
||||
LoadingBarSet((i + 1) / (float)files.Length, ConsoleColor.Red);
|
||||
LoadingBar.Set((i + 1) / (float)files.Length, ConsoleColor.Red);
|
||||
Console.CursorLeft = 0;
|
||||
string message = $"{files[i]}";
|
||||
int remainder = Console.BufferWidth - message.Length;
|
||||
@ -329,7 +377,7 @@ public static class BaseModule
|
||||
Write(message, newLine: false);
|
||||
}
|
||||
|
||||
LoadingBarEnd();
|
||||
LoadingBar.End();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
@ -346,15 +394,6 @@ public static class BaseModule
|
||||
});
|
||||
}
|
||||
|
||||
[Command("quit")]
|
||||
public static void QuitShell(int code = 0)
|
||||
{
|
||||
Environment.Exit(code);
|
||||
}
|
||||
|
||||
[Command("type")]
|
||||
public static void Type(string file) => Print(file);
|
||||
|
||||
[Command("undo")]
|
||||
public static void UndoCommand(int amount = 1)
|
||||
{
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
using SrcMod.Shell.Interop;
|
||||
|
||||
namespace SrcMod.Shell.Modules;
|
||||
namespace SrcMod.Shell.Modules;
|
||||
|
||||
[Module("clipboard")]
|
||||
public static class ClipboardModule
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
namespace SrcMod.Shell.Modules;
|
||||
|
||||
// Some things that can be extracted can't be compressed by SharpCompress.
|
||||
// In the future I might replace it with my own, but that'll take a *really*
|
||||
// long time and I'm already planning to do that for the valve compression formats,
|
||||
// so I'll seethe for now.
|
||||
[Module("compress")]
|
||||
public static class CompressionModule
|
||||
{
|
||||
[Command("zip")]
|
||||
public static void CompressZip(string source, string? destination = null,
|
||||
[Command("gz")]
|
||||
[Command("gzip")]
|
||||
public static void CompressGZip(string source, string? destination = null,
|
||||
CompressionLevel level = CompressionLevel.Optimal)
|
||||
{
|
||||
destination ??= Path.Combine(Path.GetDirectoryName(Path.GetFullPath(source))!,
|
||||
$"{Path.GetFileNameWithoutExtension(source)}.zip");
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source),
|
||||
name = Path.GetFileName(full),
|
||||
folder = Program.Shell!.WorkingDirectory;
|
||||
destination ??= $"{folder}\\{name}.gz";
|
||||
}
|
||||
|
||||
string absSource = Path.GetFullPath(source),
|
||||
localDest = Path.GetRelativePath(Program.Shell!.WorkingDirectory, destination);
|
||||
@ -19,8 +29,109 @@ public static class CompressionModule
|
||||
string message = $"Compressing file at \"{source}\" into \"{localDest}\"...";
|
||||
Write(message);
|
||||
|
||||
Stream writer = new FileStream(destination, FileMode.CreateNew);
|
||||
FileStream writer = new(localDest, FileMode.CreateNew),
|
||||
reader = new(absSource, FileMode.Open);
|
||||
GZipStream gzip = new(writer, level);
|
||||
|
||||
reader.CopyTo(gzip);
|
||||
|
||||
gzip.Close();
|
||||
reader.Close();
|
||||
writer.Close();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Console.CursorTop -= (message.Length / Console.BufferWidth) + 1;
|
||||
Write(new string(' ', message.Length), newLine: false);
|
||||
}
|
||||
else if (Directory.Exists(source)) throw new("The GZip format can only compress 1 file.");
|
||||
else throw new("No file located at \"source\"");
|
||||
}
|
||||
|
||||
[Command("tar")]
|
||||
[Command("tarball")]
|
||||
public static void CompressTar(string source, string? destination = null)
|
||||
{
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source),
|
||||
name = Path.GetFileNameWithoutExtension(full),
|
||||
folder = Program.Shell!.WorkingDirectory;
|
||||
destination ??= $"{folder}\\{name}.tar";
|
||||
}
|
||||
|
||||
string absSource = Path.GetFullPath(source),
|
||||
localDest = Path.GetRelativePath(Program.Shell!.WorkingDirectory, destination);
|
||||
|
||||
if (File.Exists(source)) throw new("The Tar format cannot compress a single file.");
|
||||
else if (Directory.Exists(source))
|
||||
{
|
||||
if (File.Exists(destination)) throw new($"File already exists at \"{localDest}\"");
|
||||
|
||||
Write($"Compressing folder at \"{source}\" into \"{localDest}\"...");
|
||||
|
||||
FileStream writer = new(destination, FileMode.CreateNew);
|
||||
TarFile.CreateFromDirectory(absSource, writer, false);
|
||||
|
||||
writer.Close();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
Console.SetCursorPosition(0, Console.CursorTop - 1);
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
}
|
||||
else throw new("No file or directory located at \"source\"");
|
||||
}
|
||||
|
||||
// Rar can't be compressed.
|
||||
|
||||
[Command("targz")]
|
||||
[Command("tar.gz")]
|
||||
[Command("tar-gz")]
|
||||
public static void CompressTarGzip(string source, string? destination = null,
|
||||
CompressionLevel level = CompressionLevel.Optimal)
|
||||
{
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source),
|
||||
name = Path.GetFileNameWithoutExtension(full),
|
||||
folder = Program.Shell!.WorkingDirectory;
|
||||
destination ??= $"{folder}\\{name}.tar.gz";
|
||||
}
|
||||
|
||||
string firstDest = Path.GetFileNameWithoutExtension(destination);
|
||||
|
||||
CompressTar(source, firstDest);
|
||||
CompressGZip(firstDest, destination, level);
|
||||
File.Delete(firstDest);
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Console.CursorTop--;
|
||||
}
|
||||
|
||||
[Command("zip")]
|
||||
public static void CompressZip(string source, string? destination = null,
|
||||
CompressionLevel level = CompressionLevel.Optimal, string comment = "")
|
||||
{
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source),
|
||||
name = Path.GetFileNameWithoutExtension(full),
|
||||
folder = Program.Shell!.WorkingDirectory;
|
||||
destination ??= $"{folder}\\{name}.zip";
|
||||
}
|
||||
|
||||
string absSource = Path.GetFullPath(source),
|
||||
localDest = Path.GetRelativePath(Program.Shell!.WorkingDirectory, destination);
|
||||
|
||||
if (File.Exists(source))
|
||||
{
|
||||
if (File.Exists(destination)) throw new($"File already exists at \"{localDest}\"");
|
||||
string message = $"Compressing file at \"{source}\" into \"{localDest}\"...";
|
||||
Write(message);
|
||||
|
||||
FileStream writer = new(destination, FileMode.CreateNew);
|
||||
ZipArchive archive = new(writer, ZipArchiveMode.Create);
|
||||
if (!string.IsNullOrWhiteSpace(comment)) archive.Comment = comment;
|
||||
|
||||
archive.CreateEntryFromFile(absSource, Path.GetFileName(absSource), level);
|
||||
|
||||
@ -37,18 +148,40 @@ public static class CompressionModule
|
||||
|
||||
Write($"Compressing folder at \"{source}\" into \"{localDest}\"...");
|
||||
|
||||
Stream writer = new FileStream(destination, FileMode.CreateNew);
|
||||
FileStream writer = new(destination, FileMode.CreateNew);
|
||||
ZipArchive archive = new(writer, ZipArchiveMode.Create);
|
||||
if (!string.IsNullOrWhiteSpace(comment)) archive.Comment = comment;
|
||||
|
||||
List<string> 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++)
|
||||
{
|
||||
string f = files[i];
|
||||
if (f.Trim().ToLower() == destination.Trim().ToLower())
|
||||
{
|
||||
files.RemoveAt(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
relative.Add(Path.GetRelativePath(absSource, f));
|
||||
}
|
||||
|
||||
int failed = 0;
|
||||
|
||||
LoadingBar.Start();
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
bool failedThisTime = false;
|
||||
try
|
||||
{
|
||||
archive.CreateEntryFromFile(files[i], relative[i], level);
|
||||
LoadingBarSet((i + 1) / (float)files.Count, ConsoleColor.DarkGreen);
|
||||
}
|
||||
catch
|
||||
{
|
||||
failedThisTime = true;
|
||||
failed++;
|
||||
}
|
||||
LoadingBar.Set((i + 1) / (float)files.Count, failedThisTime ? ConsoleColor.Red : ConsoleColor.DarkGreen); ;
|
||||
Console.CursorLeft = 0;
|
||||
string message = $"{relative[i]}";
|
||||
int remainder = Console.BufferWidth - message.Length;
|
||||
@ -61,12 +194,19 @@ public static class CompressionModule
|
||||
archive.Dispose();
|
||||
writer.Dispose();
|
||||
|
||||
LoadingBarEnd();
|
||||
LoadingBar.End();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
Console.SetCursorPosition(0, Console.CursorTop - 2);
|
||||
Write(new string(' ', Console.BufferWidth), newLine: false);
|
||||
|
||||
if (failed > 0)
|
||||
{
|
||||
Console.CursorLeft = 0;
|
||||
Write($"{failed} file{(failed == 1 ? " has" : "s have")} been ignored due to an error.",
|
||||
ConsoleColor.DarkYellow);
|
||||
}
|
||||
}
|
||||
else throw new("No file or directory located at \"source\"");
|
||||
|
||||
@ -91,4 +231,6 @@ public static class CompressionModule
|
||||
name = $"Compressed a file or folder into a zip archive located at \"{destination}\""
|
||||
});
|
||||
}
|
||||
|
||||
// 7z can't be compressed.
|
||||
}
|
||||
|
||||
225
SrcMod/Shell/Modules/ConfigModule.cs
Normal file
225
SrcMod/Shell/Modules/ConfigModule.cs
Normal file
@ -0,0 +1,225 @@
|
||||
using SharpCompress;
|
||||
|
||||
namespace SrcMod.Shell.Modules;
|
||||
|
||||
[Module("config")]
|
||||
public static class ConfigModule
|
||||
{
|
||||
[Command("display")]
|
||||
[Command("list")]
|
||||
public static void DisplayConfig(string display = "all")
|
||||
{
|
||||
switch (display.Trim().ToLower())
|
||||
{
|
||||
case "all":
|
||||
DisplayConfigAll();
|
||||
break;
|
||||
|
||||
case "raw":
|
||||
DisplayConfigRaw();
|
||||
break;
|
||||
|
||||
default:
|
||||
DisplayConfigName(display);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("add")]
|
||||
[Command("append")]
|
||||
public static void AppendConfigVariable(string name, string value)
|
||||
{
|
||||
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}\".");
|
||||
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.");
|
||||
|
||||
object parsed = TypeParsers.ParseAll(value);
|
||||
if (parsed is string parsedStr
|
||||
&& chosenField.FieldType.IsEnum
|
||||
&& Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj;
|
||||
|
||||
Type arrayType = chosenField.FieldType.GetElementType()!;
|
||||
|
||||
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)
|
||||
{
|
||||
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}\".");
|
||||
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.");
|
||||
|
||||
object parsed = TypeParsers.ParseAll(value);
|
||||
if (parsed is string parsedStr
|
||||
&& chosenField.FieldType.IsEnum
|
||||
&& Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj;
|
||||
|
||||
Type arrayType = chosenField.FieldType.GetElementType()!;
|
||||
|
||||
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")
|
||||
{
|
||||
switch (name.Trim().ToLower())
|
||||
{
|
||||
case "all":
|
||||
Config.LoadedConfig = Config.Defaults;
|
||||
DisplayConfig("all");
|
||||
break;
|
||||
|
||||
default:
|
||||
ResetConfigVar(name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("set")]
|
||||
public static void SetConfigVariable(string name, string value)
|
||||
{
|
||||
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}\".");
|
||||
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.");
|
||||
|
||||
object parsed = TypeParsers.ParseAll(value);
|
||||
if (parsed is string parsedStr
|
||||
&& chosenField.FieldType.IsEnum
|
||||
&& Enum.TryParse(chosenField.FieldType, parsedStr, true, out object? obj)) parsed = obj;
|
||||
|
||||
chosenField.SetValue(Config.LoadedConfig, parsed);
|
||||
Config.UpdateChanges();
|
||||
DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name);
|
||||
}
|
||||
|
||||
private static void DisplayConfigAll()
|
||||
{
|
||||
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 DisplayConfigItem<T>(T item, int indents = 0, string name = "", bool newLine = true)
|
||||
{
|
||||
Write(new string(' ', indents * 4), newLine: false);
|
||||
if (!string.IsNullOrWhiteSpace(name)) Write($"{name}: ", newLine: false);
|
||||
|
||||
if (item is null) Write("null", ConsoleColor.DarkRed, newLine);
|
||||
else if (item is Array itemArray)
|
||||
{
|
||||
if (itemArray.Length < 1)
|
||||
{
|
||||
Write("[]", ConsoleColor.DarkGray, newLine);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
}, 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);
|
||||
}
|
||||
|
||||
private static void ResetConfigVar(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 valid config variable named \"{name}\".");
|
||||
|
||||
chosenField.SetValue(Config.LoadedConfig, chosenField.GetValue(Config.Defaults));
|
||||
Config.UpdateChanges();
|
||||
DisplayConfigItem(chosenField.GetValue(Config.LoadedConfig), name: chosenField.Name);
|
||||
}
|
||||
}
|
||||
182
SrcMod/Shell/Modules/ExtractionModule.cs
Normal file
182
SrcMod/Shell/Modules/ExtractionModule.cs
Normal file
@ -0,0 +1,182 @@
|
||||
namespace SrcMod.Shell.Modules;
|
||||
|
||||
[Module("extract")]
|
||||
public static class ExtractionModule
|
||||
{
|
||||
[Command("gz")]
|
||||
[Command("gzip")]
|
||||
public static void ExtractGZip(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(full);
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
string absSource = Path.GetFullPath(source),
|
||||
localDest = Path.GetRelativePath(Program.Shell!.WorkingDirectory, destination);
|
||||
|
||||
if (File.Exists(destination)) throw new($"File already exists at \"{destination}\".");
|
||||
string message = $"Extracting file at \"{source}\" into \"{localDest}\"...";
|
||||
Write(message);
|
||||
|
||||
FileStream writer = new(destination, FileMode.CreateNew),
|
||||
reader = new(absSource, FileMode.Open);
|
||||
GZipStream gzip = new(reader, CompressionMode.Decompress);
|
||||
|
||||
gzip.CopyTo(writer);
|
||||
|
||||
gzip.Close();
|
||||
reader.Close();
|
||||
writer.Close();
|
||||
|
||||
Console.CursorLeft = 0;
|
||||
Console.CursorTop -= (message.Length / Console.BufferWidth) + 1;
|
||||
Write(new string(' ', message.Length), newLine: false);
|
||||
}
|
||||
|
||||
[Command("rar")]
|
||||
public static void ExtractRar(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(full);
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
if (!Directory.Exists(destination)) Directory.CreateDirectory(destination);
|
||||
|
||||
FileStream reader = new(source, FileMode.Open);
|
||||
RarArchive rar = RarArchive.Open(reader);
|
||||
|
||||
IReader data = rar.ExtractAllEntries();
|
||||
data.WriteAllToDirectory(destination, new()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
PreserveFileTime = true
|
||||
});
|
||||
|
||||
rar.Dispose();
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
[Command("tar")]
|
||||
[Command("tarball")]
|
||||
public static void ExtractTar(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(full);
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
if (!Directory.Exists(destination)) Directory.CreateDirectory(destination);
|
||||
|
||||
FileStream reader = new(source, FileMode.Open);
|
||||
TarFile.ExtractToDirectory(reader, Path.GetFileName(destination), true);
|
||||
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
[Command("targz")]
|
||||
[Command("tar.gz")]
|
||||
[Command("tar-gz")]
|
||||
public static void ExtractTarGz(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(full));
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
string absSource = Path.GetFullPath(source),
|
||||
temp = Path.Combine(Path.GetDirectoryName(absSource)!, Path.GetFileNameWithoutExtension(absSource));
|
||||
|
||||
ExtractGZip(source, temp);
|
||||
ExtractTar(temp, destination);
|
||||
|
||||
File.Delete(temp);
|
||||
}
|
||||
|
||||
[Command("zip")]
|
||||
public static void ExtractZip(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(full);
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
if (!Directory.Exists(destination)) Directory.CreateDirectory(destination);
|
||||
|
||||
FileStream reader = new(source, FileMode.Open);
|
||||
ZipArchive zip = new(reader, ZipArchiveMode.Read);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(zip.Comment)) Write(zip.Comment);
|
||||
|
||||
zip.ExtractToDirectory(destination, true);
|
||||
|
||||
zip.Dispose();
|
||||
reader.Dispose();
|
||||
}
|
||||
|
||||
[Command("7z")]
|
||||
[Command("7zip")]
|
||||
[Command("sevenzip")]
|
||||
public static void Extract7Zip(string source, string? destination = null)
|
||||
{
|
||||
if (!File.Exists(source)) throw new($"No file exists at \"{source}\".");
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
string full = Path.GetFullPath(source);
|
||||
string folder = Program.Shell!.WorkingDirectory;
|
||||
string name = Path.GetFileNameWithoutExtension(full);
|
||||
|
||||
destination = $"{folder}\\{name}";
|
||||
}
|
||||
|
||||
if (!Directory.Exists(destination)) Directory.CreateDirectory(destination);
|
||||
|
||||
FileStream reader = new(source, FileMode.Open);
|
||||
SevenZipArchive zip = SevenZipArchive.Open(reader);
|
||||
|
||||
IReader data = zip.ExtractAllEntries();
|
||||
data.WriteAllToDirectory(destination, new()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
PreserveAttributes = true,
|
||||
PreserveFileTime = true
|
||||
});
|
||||
|
||||
zip.Dispose();
|
||||
reader.Dispose();
|
||||
}
|
||||
}
|
||||
12
SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs
Normal file
12
SrcMod/Shell/Modules/ObjectModels/CanCancelAttribute.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace SrcMod.Shell.Modules.ObjectModels;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
public class CanCancelAttribute : Attribute
|
||||
{
|
||||
public readonly bool CanCancel;
|
||||
|
||||
public CanCancelAttribute(bool canCancel)
|
||||
{
|
||||
CanCancel = canCancel;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
namespace SrcMod.Shell.Modules.ObjectModels;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
|
||||
public class CommandAttribute : Attribute
|
||||
{
|
||||
public readonly string NameId;
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
public class CommandInfo
|
||||
{
|
||||
public bool CanBeCancelled { get; private set; }
|
||||
public required ModuleInfo Module { get; init; }
|
||||
public required MethodInfo Method { get; init; }
|
||||
public string Name { get; private set; }
|
||||
@ -11,32 +12,47 @@ public class CommandInfo
|
||||
|
||||
private CommandInfo()
|
||||
{
|
||||
CanBeCancelled = false;
|
||||
Name = string.Empty;
|
||||
NameId = string.Empty;
|
||||
Parameters = Array.Empty<ParameterInfo>();
|
||||
RequiredParameters = 0;
|
||||
}
|
||||
|
||||
public static CommandInfo? FromMethod(ModuleInfo parentModule, MethodInfo info)
|
||||
public static CommandInfo[] FromMethod(ModuleInfo parentModule, MethodInfo info)
|
||||
{
|
||||
CommandAttribute? attribute = info.GetCustomAttribute<CommandAttribute>();
|
||||
if (attribute is null) return null;
|
||||
// This is a little redundant as we're duplicating a bunch of info,
|
||||
// but honestly, it isn't too bad. Maybe there will be an improvement
|
||||
// in the future, maybe not. But not for a while because this works.
|
||||
|
||||
if (info.ReturnType != typeof(void)) return null;
|
||||
if (info.ReturnType != typeof(void)) return Array.Empty<CommandInfo>();
|
||||
ParameterInfo[] param = info.GetParameters();
|
||||
|
||||
int required = 0;
|
||||
while (required < param.Length && !param[required].IsOptional) required++;
|
||||
|
||||
return new()
|
||||
List<CommandInfo> commands = new();
|
||||
|
||||
CommandAttribute[] attributes = info.GetCustomAttributes<CommandAttribute>().ToArray();
|
||||
if (attributes.Length <= 0) return Array.Empty<CommandInfo>();
|
||||
|
||||
CanCancelAttribute? cancel = info.GetCustomAttribute<CanCancelAttribute>();
|
||||
|
||||
foreach (CommandAttribute attribute in attributes)
|
||||
{
|
||||
commands.Add(new()
|
||||
{
|
||||
CanBeCancelled = cancel is null || cancel.CanCancel,
|
||||
Method = info,
|
||||
Module = parentModule,
|
||||
Name = info.Name,
|
||||
NameId = attribute.NameId,
|
||||
Parameters = param,
|
||||
RequiredParameters = required
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return commands.ToArray();
|
||||
}
|
||||
|
||||
public void Invoke(params string[] args)
|
||||
|
||||
@ -18,7 +18,7 @@ public class ModuleInfo
|
||||
NameIsPrefix = true;
|
||||
}
|
||||
|
||||
public static ModuleInfo? FromModule(Type info)
|
||||
public static ModuleInfo? FromType(Type info)
|
||||
{
|
||||
ModuleAttribute? attribute = info.GetCustomAttribute<ModuleAttribute>();
|
||||
if (attribute is null) return null;
|
||||
@ -37,9 +37,9 @@ public class ModuleInfo
|
||||
List<CommandInfo> commands = new();
|
||||
foreach (MethodInfo method in info.GetMethods())
|
||||
{
|
||||
CommandInfo? cmd = CommandInfo.FromMethod(module, method);
|
||||
if (cmd is null) continue;
|
||||
commands.Add(cmd);
|
||||
CommandInfo[] cmds = CommandInfo.FromMethod(module, method);
|
||||
if (cmds.Length <= 0) continue;
|
||||
commands.AddRange(cmds);
|
||||
}
|
||||
|
||||
module.Commands.AddRange(commands);
|
||||
|
||||
@ -2,35 +2,73 @@
|
||||
|
||||
public static class TypeParsers
|
||||
{
|
||||
public static bool CanParse(object? obj) => obj is not null && obj is bool or sbyte or byte or short or ushort
|
||||
or int or uint or long or ulong or Int128 or UInt128 or nint or nuint or Half or float or double or decimal
|
||||
or char or DateOnly or DateTime or DateTimeOffset or Guid or TimeOnly or TimeSpan;
|
||||
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 DateTimeOffset dateTimeOffset)) return dateTimeOffset;
|
||||
if (TryParse(msg, out Guid guid)) return guid;
|
||||
if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly;
|
||||
if (TryParse(msg, out TimeSpan timeSpan)) return timeSpan;
|
||||
|
||||
return msg;
|
||||
if (TryParseBool(msg, out bool resBool)) return resBool;
|
||||
else if (TryParse(msg, out sbyte int8)) return int8;
|
||||
else if (TryParse(msg, out byte uInt8)) return uInt8;
|
||||
else if (TryParse(msg, out short int16)) return int16;
|
||||
else if (TryParse(msg, out ushort uInt16)) return uInt16;
|
||||
else if (TryParse(msg, out int int32)) return int32;
|
||||
else if (TryParse(msg, out uint uInt32)) return uInt32;
|
||||
else if (TryParse(msg, out long int64)) return int64;
|
||||
else if (TryParse(msg, out ulong uInt64)) return uInt64;
|
||||
else if (TryParse(msg, out Int128 int128)) return int128;
|
||||
else if (TryParse(msg, out UInt128 uInt128)) return uInt128;
|
||||
else if (TryParse(msg, out nint intPtr)) return intPtr;
|
||||
else if (TryParse(msg, out nuint uIntPtr)) return uIntPtr;
|
||||
else if (TryParse(msg, out Half float16)) return float16;
|
||||
else if (TryParse(msg, out float float32)) return float32;
|
||||
else if (TryParse(msg, out double float64)) return float64;
|
||||
else if (TryParse(msg, out decimal float128)) return float128;
|
||||
else if (TryParse(msg, out char resChar)) return resChar;
|
||||
else if (TryParse(msg, out DateOnly dateOnly)) return dateOnly;
|
||||
else if (TryParse(msg, out DateTime dateTime)) return dateTime;
|
||||
else if (TryParse(msg, out DateTimeOffset dateTimeOffset)) return dateTimeOffset;
|
||||
else if (TryParse(msg, out Guid guid)) return guid;
|
||||
else if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly;
|
||||
else if (TryParse(msg, out TimeSpan timeSpan)) return timeSpan;
|
||||
else return msg;
|
||||
}
|
||||
|
||||
public static bool TryParseBool(string msg, out bool result)
|
||||
{
|
||||
string trimmed = msg.Trim().ToLower();
|
||||
|
||||
string[] trues = new string[]
|
||||
{
|
||||
"t",
|
||||
"true",
|
||||
"1",
|
||||
"y",
|
||||
"yes"
|
||||
},
|
||||
falses = new string[]
|
||||
{
|
||||
"f",
|
||||
"false",
|
||||
"0",
|
||||
"n",
|
||||
"no"
|
||||
};
|
||||
|
||||
foreach (string t in trues) if (trimmed == t)
|
||||
{
|
||||
result = true;
|
||||
return true;
|
||||
}
|
||||
foreach (string f in falses) if (trimmed == f)
|
||||
{
|
||||
result = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = false;
|
||||
return false;
|
||||
}
|
||||
public static bool TryParse<T>(string msg, out T? result) where T : IParsable<T>
|
||||
=> T.TryParse(msg, null, out result);
|
||||
}
|
||||
|
||||
8
SrcMod/Shell/ObjectModels/AskMode.cs
Normal file
8
SrcMod/Shell/ObjectModels/AskMode.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace SrcMod.Shell.ObjectModels;
|
||||
|
||||
public enum AskMode : sbyte
|
||||
{
|
||||
Never = -1,
|
||||
Ask = 0,
|
||||
Always = 1
|
||||
}
|
||||
239
SrcMod/Shell/ObjectModels/Config.cs
Normal file
239
SrcMod/Shell/ObjectModels/Config.cs
Normal file
@ -0,0 +1,239 @@
|
||||
namespace SrcMod.Shell.ObjectModels;
|
||||
|
||||
public class Config
|
||||
{
|
||||
public const string FilePath = "config.json";
|
||||
|
||||
public static bool HasDisplayableError => false;
|
||||
public static bool HasDisplayableWarning => p_printedLastSteamWarning;
|
||||
|
||||
public static Config Defaults => new();
|
||||
|
||||
private static readonly FieldInfo[] p_configSharedFields;
|
||||
private static readonly FieldInfo[] p_changeSharedFields;
|
||||
|
||||
public static Config LoadedConfig
|
||||
{
|
||||
get => p_applied;
|
||||
set
|
||||
{
|
||||
p_applied = value;
|
||||
UpdateChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private static Config p_applied;
|
||||
private static Changes? p_changes;
|
||||
|
||||
private static bool p_printedLastSteamWarning;
|
||||
|
||||
// These variables should only exist in the Config class so they aren't marked as shared.
|
||||
private readonly string p_steamLocation;
|
||||
|
||||
static Config()
|
||||
{
|
||||
// Generate shared fields between the config class and its changes equivalent.
|
||||
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)
|
||||
{
|
||||
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 bool UseLocalModDirectories;
|
||||
|
||||
internal Config()
|
||||
{
|
||||
// Locate some steam stuff.
|
||||
const string steamLocationKey = @"Software\Valve\Steam";
|
||||
RegistryKey? key = Registry.CurrentUser.OpenSubKey(steamLocationKey);
|
||||
if (key is null)
|
||||
{
|
||||
Write("[FATAL] Cannot locate Steam installation. Do you have Steam installed?",
|
||||
ConsoleColor.DarkRed);
|
||||
Thread.Sleep(1000);
|
||||
BaseModule.QuitShell(-1);
|
||||
|
||||
// This should never run, and is just here to supress
|
||||
// a couple compiler warnings.
|
||||
p_steamLocation = string.Empty;
|
||||
GameDirectories = Array.Empty<string>();
|
||||
RunUnsafeCommands = AskMode.Ask;
|
||||
return;
|
||||
}
|
||||
p_steamLocation = (string)key.GetValue("SteamPath")!;
|
||||
|
||||
// Assign config variables.
|
||||
|
||||
string gameDirDataPath = Path.Combine(p_steamLocation, @"steamapps\libraryfolders.vdf");
|
||||
|
||||
FileStream gameDirData = new(gameDirDataPath, FileMode.Open);
|
||||
try
|
||||
{
|
||||
LibraryFolder[]? folders = SerializeVkv.Deserialize<LibraryFolder[]>(gameDirData);
|
||||
if (folders is null)
|
||||
{
|
||||
if (!p_printedLastSteamWarning)
|
||||
Write("[WARNING] Error parsing Steam game directories.", ConsoleColor.DarkYellow);
|
||||
GameDirectories = Array.Empty<string>();
|
||||
p_printedLastSteamWarning = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameDirectories = new string[folders.Length];
|
||||
for (int i = 0; i < folders.Length; i++) GameDirectories[i] = folders[i].path;
|
||||
p_printedLastSteamWarning = false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!p_printedLastSteamWarning)
|
||||
{
|
||||
#if RELEASE
|
||||
Write("[WARNING] Error parsing Steam game directories.", ConsoleColor.DarkYellow);
|
||||
#else
|
||||
Write(ex, ConsoleColor.DarkYellow);
|
||||
#endif
|
||||
}
|
||||
GameDirectories = Array.Empty<string>();
|
||||
p_printedLastSteamWarning = true;
|
||||
}
|
||||
|
||||
RunUnsafeCommands = AskMode.Ask;
|
||||
UseLocalModDirectories = true;
|
||||
}
|
||||
|
||||
public Config ApplyChanges(Changes changes)
|
||||
{
|
||||
for (int i = 0; i < p_configSharedFields.Length; i++)
|
||||
{
|
||||
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<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;
|
||||
}
|
||||
|
||||
public static void LoadConfig(string basePath)
|
||||
{
|
||||
string fullPath = Path.Combine(basePath, FilePath);
|
||||
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
p_applied = Defaults;
|
||||
p_changes = null;
|
||||
return;
|
||||
}
|
||||
StreamReader reader = new(fullPath);
|
||||
JsonTextReader jsonReader = new(reader);
|
||||
p_changes = Tools.SerializerJson.Deserialize<Changes?>(jsonReader);
|
||||
jsonReader.Close();
|
||||
reader.Close();
|
||||
|
||||
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.Any())
|
||||
{
|
||||
if (File.Exists(fullPath)) File.Delete(fullPath);
|
||||
return;
|
||||
}
|
||||
|
||||
StreamWriter writer = new(fullPath);
|
||||
JsonTextWriter jsonWriter = new(writer)
|
||||
{
|
||||
Indentation = 4
|
||||
};
|
||||
Tools.SerializerJson.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? UseLocalModDirectories;
|
||||
|
||||
public bool Any() => typeof(Changes).GetFields().Any(x => x.GetValue(this) is not null);
|
||||
}
|
||||
}
|
||||
59
SrcMod/Shell/ObjectModels/Source/GameInfo.cs
Normal file
59
SrcMod/Shell/ObjectModels/Source/GameInfo.cs
Normal file
@ -0,0 +1,59 @@
|
||||
namespace SrcMod.Shell.ObjectModels.Source;
|
||||
|
||||
// Referencing https://developer.valvesoftware.com/wiki/Gameinfo.txt.
|
||||
public class GameInfo
|
||||
{
|
||||
// Name
|
||||
public string Game;
|
||||
public string Title;
|
||||
public string? Title2;
|
||||
public bool? GameLogo;
|
||||
|
||||
// Options
|
||||
public string? Type;
|
||||
public bool? NoDifficulty;
|
||||
public bool? HasPortals;
|
||||
public bool? NoCrosshair;
|
||||
public bool? AdvCrosshair;
|
||||
public bool? NoModels;
|
||||
public bool? NoHIModel;
|
||||
|
||||
public Dictionary<string, int>? Hidden_Maps;
|
||||
public Dictionary<string, string>? CommandLine;
|
||||
|
||||
// Steam games list
|
||||
public string? Developer;
|
||||
public string? Developer_URL;
|
||||
public string? Manual;
|
||||
public string? Icon;
|
||||
|
||||
// Engine and tools
|
||||
public bool? Nodegraph;
|
||||
public string? GameData;
|
||||
public string? InstancePath;
|
||||
public bool? SupportsDX8;
|
||||
public bool? SupportsVR;
|
||||
public bool? SupportsXBox360;
|
||||
public FileSystemData FileSystem;
|
||||
|
||||
public GameInfo()
|
||||
{
|
||||
Game = string.Empty;
|
||||
Title = string.Empty;
|
||||
FileSystem = new();
|
||||
}
|
||||
|
||||
public class FileSystemData
|
||||
{
|
||||
public int SteamAppID;
|
||||
public int? AdditionalContentId;
|
||||
public int? ToolsAppId;
|
||||
|
||||
public Dictionary<string, string> SearchPaths;
|
||||
|
||||
public FileSystemData()
|
||||
{
|
||||
SearchPaths = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
SrcMod/Shell/ObjectModels/Steam/LibraryFolder.cs
Normal file
13
SrcMod/Shell/ObjectModels/Steam/LibraryFolder.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace SrcMod.Shell.ObjectModels.Steam;
|
||||
|
||||
public class LibraryFolder
|
||||
{
|
||||
public string path;
|
||||
public Dictionary<int, ulong> apps;
|
||||
|
||||
public LibraryFolder()
|
||||
{
|
||||
path = string.Empty;
|
||||
apps = new();
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,13 @@ public class Shell
|
||||
{
|
||||
public const string Author = "That_One_Nerd";
|
||||
public const string Name = "SrcMod";
|
||||
public const string Version = "Alpha 0.1.0";
|
||||
public const string Version = "Beta 0.5.0";
|
||||
|
||||
public bool HasAnyDisplayableError => HasDisplayableError || Config.HasDisplayableError;
|
||||
public bool HasAnyDisplayableWarning => HasDisplayableWarning || Config.HasDisplayableWarning;
|
||||
|
||||
public bool HasDisplayableError => p_printedLastReloadError;
|
||||
public bool HasDisplayableWarning => false;
|
||||
|
||||
public readonly string? ShellDirectory;
|
||||
|
||||
@ -16,6 +22,13 @@ public class Shell
|
||||
public List<HistoryItem> History;
|
||||
public string WorkingDirectory;
|
||||
|
||||
private bool p_lastCancel;
|
||||
private bool p_printedCancel;
|
||||
|
||||
private bool p_printedLastReloadError;
|
||||
|
||||
private BackgroundWorker? p_activeCommand;
|
||||
|
||||
public Shell()
|
||||
{
|
||||
Console.CursorVisible = false;
|
||||
@ -43,6 +56,10 @@ public class Shell
|
||||
|
||||
WorkingDirectory = Directory.GetCurrentDirectory();
|
||||
|
||||
// Load config.
|
||||
if (ShellDirectory is null) Write("[WARNING] Could not load config from shell location. Defaults will be used.");
|
||||
else Config.LoadConfig(ShellDirectory);
|
||||
|
||||
// Load modules and commands.
|
||||
List<Assembly?> possibleAsms = new()
|
||||
{
|
||||
@ -62,7 +79,7 @@ public class Shell
|
||||
}
|
||||
foreach (Type t in possibleModules)
|
||||
{
|
||||
ModuleInfo? module = ModuleInfo.FromModule(t);
|
||||
ModuleInfo? module = ModuleInfo.FromType(t);
|
||||
if (module is not null)
|
||||
{
|
||||
LoadedModules.Add(module);
|
||||
@ -79,11 +96,35 @@ public class Shell
|
||||
Write(" by ", ConsoleColor.White, false);
|
||||
Write($"{Author}", ConsoleColor.DarkYellow);
|
||||
|
||||
p_lastCancel = false;
|
||||
p_activeCommand = null;
|
||||
Console.CancelKeyPress += HandleCancel;
|
||||
|
||||
ActiveGame = null;
|
||||
|
||||
ReloadDirectoryInfo();
|
||||
}
|
||||
|
||||
public bool LoadModule(Type moduleType)
|
||||
{
|
||||
if (LoadedModules.Any(x => x.Type.FullName == moduleType.FullName)) return false;
|
||||
|
||||
ModuleInfo? module = ModuleInfo.FromType(moduleType);
|
||||
if (module is null) return false;
|
||||
|
||||
LoadedModules.Add(module);
|
||||
LoadedCommands.AddRange(module.Commands);
|
||||
|
||||
return true;
|
||||
}
|
||||
public bool LoadModule<T>() => LoadModule(typeof(T));
|
||||
public int LoadModules(Assembly moduleAssembly)
|
||||
{
|
||||
int loaded = 0;
|
||||
foreach (Type moduleType in moduleAssembly.GetTypes()) if (LoadModule(moduleType)) loaded++;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public void AddHistory(HistoryItem item) => History.Add(item);
|
||||
public void UndoItem()
|
||||
{
|
||||
@ -106,27 +147,72 @@ public class Shell
|
||||
|
||||
public string ReadLine()
|
||||
{
|
||||
Console.CursorVisible = true;
|
||||
Write("\n", newLine: false);
|
||||
if (HasAnyDisplayableError) Write($"(Error) ", ConsoleColor.DarkRed, false);
|
||||
else if (HasAnyDisplayableWarning) Write($"(Warning) ", ConsoleColor.DarkYellow, false);
|
||||
|
||||
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);
|
||||
if (ActiveMod is not null) Write($"{ActiveMod} ", ConsoleColor.Magenta, false);
|
||||
|
||||
if (ActiveMod is not null && Config.LoadedConfig.UseLocalModDirectories)
|
||||
{
|
||||
string directory = Path.GetRelativePath(ActiveMod.RootDirectory, WorkingDirectory);
|
||||
if (directory == ".") directory = string.Empty;
|
||||
Write($"~\\{directory}", ConsoleColor.DarkGreen, false);
|
||||
}
|
||||
else Write($"{WorkingDirectory}", ConsoleColor.DarkGreen, false);
|
||||
|
||||
if (ActiveGame is not null) Write($" ({ActiveGame})", ConsoleColor.Blue, false);
|
||||
Write(null);
|
||||
|
||||
Write($" {Name}", ConsoleColor.DarkCyan, false);
|
||||
Write(" > ", ConsoleColor.White, false);
|
||||
|
||||
bool printed = false;
|
||||
|
||||
if (p_lastCancel && !p_printedCancel)
|
||||
{
|
||||
// Print the warning. A little bit of mess because execution must
|
||||
// continue without funny printing errors but it's alright I guess.
|
||||
|
||||
int originalLeft = Console.CursorLeft;
|
||||
|
||||
Console.CursorTop -= 3;
|
||||
Write("Press ^C again to exit the shell.", ConsoleColor.Red);
|
||||
PlayWarningSound();
|
||||
|
||||
p_printedCancel = true;
|
||||
Console.CursorTop += 2;
|
||||
|
||||
Console.CursorLeft = originalLeft;
|
||||
printed = true;
|
||||
}
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.CursorVisible = true;
|
||||
string message = Console.ReadLine()!;
|
||||
Console.CursorVisible = false;
|
||||
Console.ResetColor();
|
||||
|
||||
Console.CursorVisible = false;
|
||||
if (!printed)
|
||||
{
|
||||
p_lastCancel = false;
|
||||
p_printedCancel = false;
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public void InvokeCommand(string cmd)
|
||||
{
|
||||
if (cmd is null)
|
||||
{
|
||||
// This usually won't happen, but might if for example
|
||||
// the shell cancel interrupt is called. This probably
|
||||
// happens for other shell interrupts are called.
|
||||
Write(null);
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> parts = new();
|
||||
string active = string.Empty;
|
||||
|
||||
@ -171,20 +257,49 @@ public class Shell
|
||||
int start = module.NameIsPrefix ? 2 : 1;
|
||||
string[] args = parts.GetRange(start, parts.Count - start).ToArray();
|
||||
|
||||
void runCommand(object? sender, DoWorkEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
command.Invoke(args);
|
||||
}
|
||||
#if RELEASE
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red);
|
||||
if (LoadingBarEnabled) LoadingBarEnd();
|
||||
if (LoadingBar.Enabled) LoadingBar.End();
|
||||
}
|
||||
#endif
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if RELEASE
|
||||
Write($"[ERROR] {ex.Message}", ConsoleColor.Red);
|
||||
if (LoadingBarEnabled) LoadingBarEnd();
|
||||
if (LoadingBar.Enabled) LoadingBar.End();
|
||||
#else
|
||||
Write($"[ERROR] {ex}", ConsoleColor.Red);
|
||||
if (LoadingBar.Enabled) LoadingBar.End();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
p_activeCommand = new();
|
||||
p_activeCommand.DoWork += runCommand;
|
||||
p_activeCommand.RunWorkerAsync();
|
||||
|
||||
p_activeCommand.WorkerSupportsCancellation = command.CanBeCancelled;
|
||||
|
||||
while (p_activeCommand is not null && p_activeCommand.IsBusy) Thread.Yield();
|
||||
|
||||
if (p_activeCommand is not null)
|
||||
{
|
||||
p_activeCommand.Dispose();
|
||||
p_activeCommand = null;
|
||||
}
|
||||
|
||||
if (ShellDirectory is null) Write("[WARNING] Could not save config to shell location. Any changes will be ignored.");
|
||||
else Config.SaveConfig(ShellDirectory);
|
||||
|
||||
ReloadDirectoryInfo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -192,13 +307,85 @@ public class Shell
|
||||
Write($"[ERROR] Could not find command \"{cmd}\".", ConsoleColor.Red);
|
||||
}
|
||||
|
||||
private static void PlayErrorSound()
|
||||
{
|
||||
Winmm.PlaySound("SystemHand", nint.Zero,
|
||||
(uint)(Winmm.PlaySoundFlags.Alias | Winmm.PlaySoundFlags.Async));
|
||||
}
|
||||
private static void PlayWarningSound()
|
||||
{
|
||||
Winmm.PlaySound("SystemAsterisk", nint.Zero,
|
||||
(uint)(Winmm.PlaySoundFlags.Alias | Winmm.PlaySoundFlags.Async));
|
||||
}
|
||||
|
||||
public void ReloadDirectoryInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
ActiveMod = Mod.ReadDirectory(WorkingDirectory);
|
||||
ActiveGame = ActiveMod?.BaseGame;
|
||||
|
||||
// Update title.
|
||||
string title = "SrcMod";
|
||||
if (ActiveMod is not null) title += $" - {ActiveMod.Name}";
|
||||
Console.Title = title;
|
||||
|
||||
p_printedLastReloadError = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!p_printedLastReloadError)
|
||||
{
|
||||
#if RELEASE
|
||||
Write("[ERROR] Error reloading directory information. Some data may not update.",
|
||||
ConsoleColor.Red);
|
||||
#else
|
||||
Write(ex, ConsoleColor.Red);
|
||||
#endif
|
||||
}
|
||||
|
||||
p_printedLastReloadError = true;
|
||||
Console.Title = "SrcMod (Error)";
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleCancel(object? sender, ConsoleCancelEventArgs args)
|
||||
{
|
||||
if (p_activeCommand is not null && p_activeCommand.IsBusy)
|
||||
{
|
||||
if (p_activeCommand.WorkerSupportsCancellation)
|
||||
{
|
||||
// Kill the active command.
|
||||
p_activeCommand.CancelAsync();
|
||||
p_activeCommand.Dispose();
|
||||
p_activeCommand = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Command doesn't support cancellation.
|
||||
// Warn the user.
|
||||
PlayErrorSound();
|
||||
}
|
||||
|
||||
p_lastCancel = false;
|
||||
p_printedCancel = false;
|
||||
args.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Due to some funny multithreading issues, we want to make the warning label
|
||||
// single-threaded on the shell.
|
||||
if (!p_lastCancel)
|
||||
{
|
||||
// Enable the warning. The "ReadLine" method will do the rest.
|
||||
p_lastCancel = true;
|
||||
args.Cancel = true; // "Cancel" referring to the cancellation of the cancel operation.
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually kill the shell. We do still have to worry about some multithreaded
|
||||
// nonsense, but a bearable amount of it.
|
||||
Console.ResetColor();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>srcmod</AssemblyName>
|
||||
@ -13,16 +13,7 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<ApplicationIcon>Logo.ico</ApplicationIcon>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
<WarningLevel>9999</WarningLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<DebugType>embedded</DebugType>
|
||||
<WarningLevel>9999</WarningLevel>
|
||||
<NoWin32Manifest>true</NoWin32Manifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -31,6 +22,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nerd_STF" Version="2.3.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Valve.NET\Valve.NET.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,14 +1,29 @@
|
||||
using System.Text;
|
||||
|
||||
namespace SrcMod.Shell;
|
||||
namespace SrcMod.Shell;
|
||||
|
||||
public static class Tools
|
||||
{
|
||||
private static int loadingPosition = -1;
|
||||
private static int lastLoadingBufferSize = 0;
|
||||
private static int lastLoadingValue = -1;
|
||||
public static JsonSerializer SerializerJson { get; private set; }
|
||||
public static VkvSerializer SerializeVkv { get; private set; }
|
||||
|
||||
public static bool LoadingBarEnabled { get; private set; }
|
||||
static Tools()
|
||||
{
|
||||
SerializerJson = JsonSerializer.Create(new()
|
||||
{
|
||||
Formatting = Formatting.Indented,
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
});
|
||||
|
||||
SerializeVkv = new VkvSerializer(new()
|
||||
{
|
||||
closeWhenFinished = true,
|
||||
indentSize = 4,
|
||||
resetStreamPosition = false,
|
||||
serializeProperties = true,
|
||||
spacing = SpacingMode.DoubleTab,
|
||||
useEscapeCodes = true,
|
||||
useQuotes = true
|
||||
});
|
||||
}
|
||||
|
||||
public static void DisplayWithPages(IEnumerable<string> lines, ConsoleColor? color = null)
|
||||
{
|
||||
@ -71,64 +86,6 @@ public static class Tools
|
||||
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;
|
||||
LoadingBarEnabled = false;
|
||||
}
|
||||
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 == lastLoadingValue) return;
|
||||
lastLoadingValue = filled;
|
||||
|
||||
Int2 oldPos = (Console.CursorLeft, Console.CursorTop);
|
||||
|
||||
// Erase last bar.
|
||||
Console.SetCursorPosition(0, loadingPosition);
|
||||
Console.Write(new string(' ', lastLoadingBufferSize));
|
||||
Console.CursorLeft = 0;
|
||||
|
||||
// Add new bar.
|
||||
lastLoadingBufferSize = 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);
|
||||
LoadingBarEnabled = true;
|
||||
}
|
||||
|
||||
public static void Write(object? message, ConsoleColor? col = null, bool newLine = true)
|
||||
{
|
||||
ConsoleColor prevCol = Console.ForegroundColor;
|
||||
@ -138,11 +95,35 @@ public static class Tools
|
||||
else Console.Write(message);
|
||||
|
||||
Console.ForegroundColor = prevCol;
|
||||
|
||||
if (newLine && LoadingBar.Enabled && Console.CursorTop >= Console.BufferHeight - 1)
|
||||
{
|
||||
LoadingBar.position--;
|
||||
LoadingBar.Set(LoadingBar.value, LoadingBar.color);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ValidateUnsafe()
|
||||
{
|
||||
switch (Config.LoadedConfig.RunUnsafeCommands)
|
||||
{
|
||||
case AskMode.Always:
|
||||
Write("[INFO] The shell has been configured to always run unsafe commands. " +
|
||||
"This can be changed in the config.", ConsoleColor.DarkGray);
|
||||
return true;
|
||||
|
||||
case AskMode.Never:
|
||||
Write("[ERROR] The shell has been configured to never run unsafe commands. " +
|
||||
"This can be changed in the config.", ConsoleColor.Red);
|
||||
return false;
|
||||
|
||||
case AskMode.Ask or _:
|
||||
Write("You are about to execute an unsafe command.\nProceed? > ", ConsoleColor.DarkYellow, false);
|
||||
Int2 start = (Console.CursorLeft, Console.CursorTop);
|
||||
Write("\nTip: You can disable this dialog in the config.", ConsoleColor.DarkGray);
|
||||
int finish = Console.CursorTop;
|
||||
|
||||
Console.SetCursorPosition(start.x, start.y);
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.CursorVisible = true;
|
||||
@ -150,7 +131,10 @@ public static class Tools
|
||||
Console.CursorVisible = false;
|
||||
Console.ResetColor();
|
||||
|
||||
Console.SetCursorPosition(0, finish);
|
||||
|
||||
return result == "y" || result == "yes" || result == "t" ||
|
||||
result == "true" || result == "p" || result == "proceed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
SrcMod/SrcMod.sln
Normal file
31
SrcMod/SrcMod.sln
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.4.33213.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shell", "Shell\Shell.csproj", "{6EC87235-F2A5-4313-A6DE-A4EE7CB7B341}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Valve.NET", "Valve.NET\Valve.NET.csproj", "{8FC96202-2F7E-4FBE-B08E-FCC38AA62D96}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{6EC87235-F2A5-4313-A6DE-A4EE7CB7B341}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6EC87235-F2A5-4313-A6DE-A4EE7CB7B341}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6EC87235-F2A5-4313-A6DE-A4EE7CB7B341}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6EC87235-F2A5-4313-A6DE-A4EE7CB7B341}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8FC96202-2F7E-4FBE-B08E-FCC38AA62D96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8FC96202-2F7E-4FBE-B08E-FCC38AA62D96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8FC96202-2F7E-4FBE-B08E-FCC38AA62D96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8FC96202-2F7E-4FBE-B08E-FCC38AA62D96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {4E25AC28-DD70-4BB6-9083-07D6371DECCF}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@ -1,9 +1,9 @@
|
||||
global using Nerd_STF.Mathematics;
|
||||
global using SrcMod.Shell;
|
||||
global using SrcMod.Shell.Modules.ObjectModels;
|
||||
global using System;
|
||||
global using System;
|
||||
global using System.Collections;
|
||||
global using System.Collections.Generic;
|
||||
global using System.ComponentModel;
|
||||
global using System.Diagnostics;
|
||||
global using System.Formats.Tar;
|
||||
global using System.IO;
|
||||
global using System.IO.Compression;
|
||||
global using System.Linq;
|
||||
@ -11,4 +11,7 @@ global using System.Reflection;
|
||||
global using System.Runtime.InteropServices;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using static SrcMod.Shell.Tools;
|
||||
global using Valve;
|
||||
global using Valve.Miscellaneous;
|
||||
global using Valve.Vkv;
|
||||
global using Valve.Vkv.ObjectModels;
|
||||
74
SrcMod/Valve.NET/Miscellaneous/TypeParsers.cs
Normal file
74
SrcMod/Valve.NET/Miscellaneous/TypeParsers.cs
Normal file
@ -0,0 +1,74 @@
|
||||
namespace Valve.Miscellaneous;
|
||||
|
||||
public static class TypeParsers
|
||||
{
|
||||
public static bool CanParse(object? obj) => obj is not null && obj is bool or sbyte or byte or short or ushort
|
||||
or int or uint or long or ulong or Int128 or UInt128 or nint or nuint or Half or float or double or decimal
|
||||
or char or DateOnly or DateTime or DateTimeOffset or Guid or TimeOnly or TimeSpan;
|
||||
public static object ParseAll(string msg)
|
||||
{
|
||||
if (TryParseBool(msg, out bool resBool)) return resBool;
|
||||
else if (TryParse(msg, out sbyte int8)) return int8;
|
||||
else if (TryParse(msg, out byte uInt8)) return uInt8;
|
||||
else if (TryParse(msg, out short int16)) return int16;
|
||||
else if (TryParse(msg, out ushort uInt16)) return uInt16;
|
||||
else if (TryParse(msg, out int int32)) return int32;
|
||||
else if (TryParse(msg, out uint uInt32)) return uInt32;
|
||||
else if (TryParse(msg, out long int64)) return int64;
|
||||
else if (TryParse(msg, out ulong uInt64)) return uInt64;
|
||||
else if (TryParse(msg, out Int128 int128)) return int128;
|
||||
else if (TryParse(msg, out UInt128 uInt128)) return uInt128;
|
||||
else if (TryParse(msg, out nint intPtr)) return intPtr;
|
||||
else if (TryParse(msg, out nuint uIntPtr)) return uIntPtr;
|
||||
else if (TryParse(msg, out Half float16)) return float16;
|
||||
else if (TryParse(msg, out float float32)) return float32;
|
||||
else if (TryParse(msg, out double float64)) return float64;
|
||||
else if (TryParse(msg, out decimal float128)) return float128;
|
||||
else if (TryParse(msg, out char resChar)) return resChar;
|
||||
else if (TryParse(msg, out DateOnly dateOnly)) return dateOnly;
|
||||
else if (TryParse(msg, out DateTime dateTime)) return dateTime;
|
||||
else if (TryParse(msg, out DateTimeOffset dateTimeOffset)) return dateTimeOffset;
|
||||
else if (TryParse(msg, out Guid guid)) return guid;
|
||||
else if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly;
|
||||
else if (TryParse(msg, out TimeSpan timeSpan)) return timeSpan;
|
||||
else return msg;
|
||||
}
|
||||
|
||||
public static bool TryParseBool(string msg, out bool result)
|
||||
{
|
||||
string trimmed = msg.Trim().ToLower();
|
||||
|
||||
string[] trues = new string[]
|
||||
{
|
||||
"t",
|
||||
"true",
|
||||
"1",
|
||||
"y",
|
||||
"yes"
|
||||
},
|
||||
falses = new string[]
|
||||
{
|
||||
"f",
|
||||
"false",
|
||||
"0",
|
||||
"n",
|
||||
"no"
|
||||
};
|
||||
|
||||
foreach (string t in trues) if (trimmed == t)
|
||||
{
|
||||
result = true;
|
||||
return true;
|
||||
}
|
||||
foreach (string f in falses) if (trimmed == f)
|
||||
{
|
||||
result = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = false;
|
||||
return false;
|
||||
}
|
||||
public static bool TryParse<T>(string msg, out T? result) where T : IParsable<T>
|
||||
=> T.TryParse(msg, null, out result);
|
||||
}
|
||||
17
SrcMod/Valve.NET/Valve.NET.csproj
Normal file
17
SrcMod/Valve.NET/Valve.NET.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>valve.net</AssemblyName>
|
||||
<RootNamespace>Valve</RootNamespace>
|
||||
<OutputPath>../Compiled/Valve.NET</OutputPath>
|
||||
<Title>Valve.NET</Title>
|
||||
<Authors>That_One_Nerd</Authors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<NoWin32Manifest>true</NoWin32Manifest>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
6
SrcMod/Valve.NET/Vkv/ObjectModels/IVkvConvertible.cs
Normal file
6
SrcMod/Valve.NET/Vkv/ObjectModels/IVkvConvertible.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Valve.Vkv.ObjectModels;
|
||||
|
||||
public interface IVkvConvertible
|
||||
{
|
||||
public VkvNode ToNodeTree();
|
||||
}
|
||||
4
SrcMod/Valve.NET/Vkv/ObjectModels/VkvIgnoreAttribute.cs
Normal file
4
SrcMod/Valve.NET/Vkv/ObjectModels/VkvIgnoreAttribute.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Valve.Vkv.ObjectModels;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
|
||||
public class VkvIgnoreAttribute : Attribute { }
|
||||
9
SrcMod/Valve.NET/Vkv/ObjectModels/VkvKeyNameAttribute.cs
Normal file
9
SrcMod/Valve.NET/Vkv/ObjectModels/VkvKeyNameAttribute.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Valve.Vkv.ObjectModels;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
|
||||
public class VkvKeyNameAttribute : Attribute
|
||||
{
|
||||
public readonly string name;
|
||||
|
||||
public VkvKeyNameAttribute(string name) => this.name = name;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Valve.Vkv.ObjectModels;
|
||||
|
||||
public class VkvSerializationException : Exception
|
||||
{
|
||||
public VkvSerializationException() : base() { }
|
||||
public VkvSerializationException(string message) : base(message) { }
|
||||
public VkvSerializationException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
8
SrcMod/Valve.NET/Vkv/SpacingMode.cs
Normal file
8
SrcMod/Valve.NET/Vkv/SpacingMode.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public enum SpacingMode
|
||||
{
|
||||
SingleSpace = 0,
|
||||
IndentSizeSpacing,
|
||||
DoubleTab,
|
||||
}
|
||||
449
SrcMod/Valve.NET/Vkv/VkvConvert.cs
Normal file
449
SrcMod/Valve.NET/Vkv/VkvConvert.cs
Normal file
@ -0,0 +1,449 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public static class VkvConvert
|
||||
{
|
||||
private static readonly Dictionary<string, string> p_escapeCodes = new()
|
||||
{
|
||||
{ "\\", @"\\" }, // This must be first.
|
||||
{ "\'", @"\'" },
|
||||
{ "\"", @"\""" },
|
||||
{ "\0", @"\0" },
|
||||
{ "\a", @"\a" },
|
||||
{ "\b", @"\b" },
|
||||
{ "\f", @"\f" },
|
||||
{ "\n", @"\n" },
|
||||
{ "\r", @"\r" },
|
||||
{ "\t", @"\t" },
|
||||
{ "\v", @"\v" }
|
||||
};
|
||||
|
||||
#region DeserializeNode
|
||||
public static VkvNode? DeserializeNode(StreamReader reader) =>
|
||||
DeserializeNode(reader, VkvOptions.Default);
|
||||
public static VkvNode? DeserializeNode(StreamReader reader, VkvOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DeserializeNode(reader, options, out _, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!options.noExceptions) throw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static VkvNode? DeserializeNode(StreamReader reader, VkvOptions options, out string name,
|
||||
string? first)
|
||||
{
|
||||
string? header = first ?? (reader.ReadLine()?.Trim());
|
||||
if (header is null || string.IsNullOrEmpty(header))
|
||||
{
|
||||
name = string.Empty;
|
||||
return null;
|
||||
}
|
||||
|
||||
string[] parts = SplitContent(header, options);
|
||||
if (parts.Length > 2) throw new VkvSerializationException("Too many values in node.");
|
||||
|
||||
VkvNode node;
|
||||
|
||||
name = DeserializeString(parts[0], options);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
string value = DeserializeString(parts[1], options);
|
||||
node = new VkvSingleNode(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
string? next = reader.ReadLine()?.Trim();
|
||||
if (next is null) throw new VkvSerializationException("Expected starting '{', found end-of-file.");
|
||||
else if (next != "{") throw new VkvSerializationException($"Expected starting '{{', found \"{next}\".");
|
||||
VkvTreeNode tree = new();
|
||||
string? current;
|
||||
while ((current = reader.ReadLine()?.Trim()) is not null)
|
||||
{
|
||||
if (current == "}") break;
|
||||
VkvNode? output = DeserializeNode(reader, options, out string subName, current);
|
||||
tree[subName] = output;
|
||||
}
|
||||
if (current is null) throw new VkvSerializationException("Reached end-of-file while deserializing group.");
|
||||
node = tree;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static string DeserializeString(string content, VkvOptions options)
|
||||
{
|
||||
if (options.useQuotes)
|
||||
{
|
||||
if (!content.StartsWith('\"') || !content.EndsWith('\"'))
|
||||
throw new VkvSerializationException("No quotes found around content.");
|
||||
content = content[1..^1];
|
||||
}
|
||||
if (options.useEscapeCodes)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> escapeCode in p_escapeCodes.Reverse())
|
||||
content = content.Replace(escapeCode.Value, escapeCode.Key);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private static string[] SplitContent(string content, VkvOptions options)
|
||||
{
|
||||
content = content.Replace('\t', ' ');
|
||||
if (options.useQuotes)
|
||||
{
|
||||
List<string> values = new();
|
||||
string current = string.Empty;
|
||||
bool inQuote = false;
|
||||
for (int i = 0; i < content.Length; i++)
|
||||
{
|
||||
char c = content[i];
|
||||
if (c == '\"' && !(i > 0 && content[i - 1] == '\\')) inQuote = !inQuote;
|
||||
|
||||
if (c == ' ' && !inQuote)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(current)) values.Add(current);
|
||||
current = string.Empty;
|
||||
}
|
||||
else current += c;
|
||||
}
|
||||
if (inQuote) throw new VkvSerializationException("Reached end-of-line while inside quotations.");
|
||||
if (!string.IsNullOrEmpty(current)) values.Add(current);
|
||||
return values.ToArray();
|
||||
}
|
||||
else return content.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FromNodeTree
|
||||
public static T? FromNodeTree<T>(VkvNode? node, VkvOptions options) => (T?)FromNodeTree(typeof(T), node, options);
|
||||
public static object? FromNodeTree(Type outputType, VkvNode? node, VkvOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (node is null) return null;
|
||||
|
||||
if (node is VkvSingleNode single) return FromSingleNode(outputType, single);
|
||||
else if (node is VkvTreeNode tree) return FromTreeNode(outputType, tree, options);
|
||||
else throw new VkvSerializationException("Unknown VKV node type.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!options.noExceptions) throw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static object? FromSingleNode(Type outputType, VkvSingleNode node)
|
||||
{
|
||||
object? value = node.value;
|
||||
if (value is null) return null;
|
||||
else if (value is string str)
|
||||
{
|
||||
value = TypeParsers.ParseAll(str);
|
||||
if (value is string still && outputType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(outputType, still, true, out object? res) && res is not null)
|
||||
value = res;
|
||||
}
|
||||
}
|
||||
return Convert.ChangeType(value, outputType);
|
||||
}
|
||||
|
||||
private static object? FromTreeNode(Type outputType, VkvTreeNode node, VkvOptions options)
|
||||
{
|
||||
if (outputType.IsArray)
|
||||
return FromTreeNodeArray(outputType, node, options);
|
||||
|
||||
else if (outputType.GetInterface("IList") is not null)
|
||||
return FromTreeNodeList(outputType, node, options);
|
||||
|
||||
else if (outputType.GetInterface("IDictionary") is not null)
|
||||
return FromTreeNodeDictionary(outputType, node, options);
|
||||
|
||||
object? instance = Activator.CreateInstance(outputType);
|
||||
if (instance is null) return null;
|
||||
|
||||
IEnumerable<FieldInfo> validFields = from field in outputType.GetFields()
|
||||
let isPublic = field.IsPublic
|
||||
let isStatic = field.IsStatic
|
||||
let isIgnored = field.CustomAttributes.Any(x =>
|
||||
x.AttributeType == typeof(VkvIgnoreAttribute))
|
||||
let isConst = field.IsLiteral
|
||||
where isPublic && !isStatic && !isIgnored && !isConst
|
||||
select field;
|
||||
|
||||
IEnumerable<PropertyInfo> validProperties;
|
||||
if (options.serializeProperties)
|
||||
{
|
||||
validProperties = from prop in outputType.GetProperties()
|
||||
let canSet = prop.SetMethod is not null
|
||||
let isPublic = canSet && prop.SetMethod!.IsPublic
|
||||
let isStatic = canSet && prop.SetMethod!.IsStatic
|
||||
let isIgnored = prop.CustomAttributes.Any(x =>
|
||||
x.AttributeType == typeof(VkvIgnoreAttribute))
|
||||
where canSet && isPublic && !isStatic && !isIgnored
|
||||
select prop;
|
||||
}
|
||||
else validProperties = Array.Empty<PropertyInfo>();
|
||||
|
||||
foreach (FieldInfo field in validFields)
|
||||
{
|
||||
VkvKeyNameAttribute? namingAttribute = field.GetCustomAttribute<VkvKeyNameAttribute>();
|
||||
|
||||
VkvNode? subNode = node[namingAttribute?.name ?? field.Name];
|
||||
if (subNode is null) continue;
|
||||
|
||||
object? result = FromNodeTree(field.FieldType, subNode, options);
|
||||
if (result is null) continue;
|
||||
field.SetValue(instance, result);
|
||||
}
|
||||
foreach (PropertyInfo prop in validProperties)
|
||||
{
|
||||
VkvKeyNameAttribute? namingAttribute = prop.GetCustomAttribute<VkvKeyNameAttribute>();
|
||||
|
||||
VkvNode? subNode = node[namingAttribute?.name ?? prop.Name];
|
||||
if (subNode is null) continue;
|
||||
|
||||
object? result = FromNodeTree(prop.PropertyType, subNode, options);
|
||||
if (result is null) continue;
|
||||
prop.SetValue(instance, result);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static object? FromTreeNodeArray(Type outputType, VkvTreeNode node, VkvOptions options)
|
||||
{
|
||||
Type elementType = outputType.GetElementType()!;
|
||||
Array array = Array.CreateInstance(elementType, node.SubNodeCount);
|
||||
|
||||
int index = 0;
|
||||
foreach (KeyValuePair<string, VkvNode?> subNode in node)
|
||||
{
|
||||
string indexStr = index.ToString();
|
||||
if (subNode.Key != indexStr) throw new VkvSerializationException($"Cannot convert node tree to array.");
|
||||
array.SetValue(FromNodeTree(elementType, subNode.Value, options), index);
|
||||
index++;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
private static object? FromTreeNodeList(Type outputType, VkvTreeNode node, VkvOptions options)
|
||||
{
|
||||
IList? instance = (IList?)Activator.CreateInstance(outputType);
|
||||
if (instance is null) return null;
|
||||
|
||||
// There is no guarentee that the first type argument corresponds to the element type,
|
||||
// but as far as I know there isn't a better way.
|
||||
Type elementType = outputType.IsGenericType ? outputType.GenericTypeArguments[0] : typeof(object);
|
||||
|
||||
int index = 0;
|
||||
foreach (KeyValuePair<string, VkvNode?> subNode in node)
|
||||
{
|
||||
string indexStr = index.ToString();
|
||||
if (subNode.Key != indexStr) throw new VkvSerializationException($"Cannot convert node tree to array.");
|
||||
instance.Add(FromNodeTree(elementType, subNode.Value, options));
|
||||
index++;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static object? FromTreeNodeDictionary(Type outputType, VkvTreeNode node, VkvOptions options)
|
||||
{
|
||||
IDictionary? instance = (IDictionary?)Activator.CreateInstance(outputType);
|
||||
if (instance is null) return null;
|
||||
|
||||
// There is no guarentee that the first and second type arguments represent the
|
||||
// key and value types, but as far as I know there isn't a better way.
|
||||
bool canUseGenerics = outputType.GenericTypeArguments.Length >= 2;
|
||||
Type keyType = canUseGenerics ? outputType.GenericTypeArguments[0] : typeof(object),
|
||||
valueType = canUseGenerics ? outputType.GenericTypeArguments[1] : typeof(object);
|
||||
|
||||
foreach (KeyValuePair<string, VkvNode?> subNode in node)
|
||||
{
|
||||
object key = TypeParsers.ParseAll(subNode.Key);
|
||||
if (key is string still && keyType.IsEnum)
|
||||
{
|
||||
if (Enum.TryParse(keyType, still, true, out object? res) && res is not null)
|
||||
key = res;
|
||||
}
|
||||
key = Convert.ChangeType(key, keyType);
|
||||
|
||||
object? value = FromNodeTree(valueType, subNode.Value, options);
|
||||
|
||||
instance.Add(key, value);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SerializeNode
|
||||
public static void SerializeNode(StreamWriter writer, VkvNode? node, string name,
|
||||
VkvOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
SerializeNode(writer, node, name, options, 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!options.noExceptions) throw;
|
||||
}
|
||||
}
|
||||
public static void SerializeNode(StreamWriter writer, VkvNode? node, string name) =>
|
||||
SerializeNode(writer, node, name, VkvOptions.Default);
|
||||
|
||||
private static void SerializeNode(StreamWriter writer, VkvNode? node, string name,
|
||||
VkvOptions options, int indentLevel)
|
||||
{
|
||||
if (node is null) return;
|
||||
else if (node is VkvSingleNode single) SerializeSingleNode(writer, single, name, options, indentLevel);
|
||||
else if (node is VkvTreeNode tree) SerializeTreeNode(writer, tree, name, options, indentLevel);
|
||||
else throw new("Unknown node type.");
|
||||
}
|
||||
|
||||
private static void SerializeSingleNode(StreamWriter writer, VkvSingleNode node, string name,
|
||||
VkvOptions options, int indentLevel)
|
||||
{
|
||||
string? serializedValue = SerializeObject(node.value);
|
||||
if (serializedValue is null) return;
|
||||
|
||||
writer.Write(new string(' ', indentLevel));
|
||||
writer.Write(SerializeString(name, options));
|
||||
|
||||
switch (options.spacing)
|
||||
{
|
||||
case SpacingMode.SingleSpace: writer.Write(' ');
|
||||
break;
|
||||
|
||||
case SpacingMode.IndentSizeSpacing: writer.Write(new string(' ', options.indentSize));
|
||||
break;
|
||||
|
||||
case SpacingMode.DoubleTab: writer.Write("\t\t");
|
||||
break;
|
||||
|
||||
default: throw new VkvSerializationException($"Unknown spacing mode \"{options.spacing}\".");
|
||||
}
|
||||
|
||||
serializedValue = SerializeString(serializedValue, options);
|
||||
writer.WriteLine(serializedValue);
|
||||
}
|
||||
private static void SerializeTreeNode(StreamWriter writer, VkvTreeNode node, string name,
|
||||
VkvOptions options, int indentLevel)
|
||||
{
|
||||
if (node.SubNodeCount <= 0) return;
|
||||
|
||||
writer.Write(new string(' ', indentLevel));
|
||||
writer.WriteLine(SerializeString(name, options));
|
||||
writer.WriteLine(new string(' ', indentLevel) + '{');
|
||||
|
||||
foreach (KeyValuePair<string, VkvNode?> subNode in node)
|
||||
SerializeNode(writer, subNode.Value, subNode.Key, options, indentLevel + options.indentSize);
|
||||
|
||||
writer.WriteLine(new string(' ', indentLevel) + '}');
|
||||
}
|
||||
|
||||
private static string? SerializeObject(object? obj)
|
||||
{
|
||||
if (obj is null) return null;
|
||||
return obj.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string SerializeString(string content, VkvOptions options)
|
||||
{
|
||||
if (options.useEscapeCodes)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> escapeCode in p_escapeCodes)
|
||||
content = content.Replace(escapeCode.Key, escapeCode.Value);
|
||||
}
|
||||
if (options.useQuotes) content = $"\"{content}\"";
|
||||
return content;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ToNodeTree
|
||||
public static VkvNode? ToNodeTree(object? obj) => ToNodeTree(obj, VkvOptions.Default);
|
||||
public static VkvNode? ToNodeTree(object? obj, VkvOptions options)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj is null) return null;
|
||||
Type type = obj.GetType();
|
||||
|
||||
if (type.IsPrimitive || TypeParsers.CanParse(obj)) return new VkvSingleNode(obj);
|
||||
else if (type.IsPointer) throw new("Cannot serialize a pointer.");
|
||||
|
||||
VkvTreeNode tree = new();
|
||||
|
||||
if (obj is IVkvConvertible vkv) return vkv.ToNodeTree();
|
||||
else if (obj is IDictionary dictionary)
|
||||
{
|
||||
object[] keys = new object[dictionary.Count],
|
||||
values = new object[dictionary.Count];
|
||||
dictionary.Keys.CopyTo(keys, 0);
|
||||
dictionary.Values.CopyTo(values, 0);
|
||||
for (int i = 0; i < dictionary.Count; i++)
|
||||
{
|
||||
tree[SerializeObject(keys.GetValue(i))!] = ToNodeTree(values.GetValue(i), options);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
else if (obj is ICollection enumerable)
|
||||
{
|
||||
int index = 0;
|
||||
foreach (object item in enumerable)
|
||||
{
|
||||
tree[SerializeObject(index)!] = ToNodeTree(item, options);
|
||||
index++;
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
IEnumerable<FieldInfo> validFields = from field in type.GetFields()
|
||||
let isPublic = field.IsPublic
|
||||
let isStatic = field.IsStatic
|
||||
let isIgnored = field.CustomAttributes.Any(x =>
|
||||
x.AttributeType == typeof(VkvIgnoreAttribute))
|
||||
let isConst = field.IsLiteral
|
||||
where isPublic && !isStatic && !isIgnored && !isConst
|
||||
select field;
|
||||
|
||||
IEnumerable<PropertyInfo> validProperties;
|
||||
if (options.serializeProperties)
|
||||
{
|
||||
validProperties = from prop in type.GetProperties()
|
||||
let canGet = prop.GetMethod is not null
|
||||
let isPublic = canGet && prop.GetMethod!.IsPublic
|
||||
let isStatic = canGet && prop.GetMethod!.IsStatic
|
||||
let isIgnored = prop.CustomAttributes.Any(x =>
|
||||
x.AttributeType == typeof(VkvIgnoreAttribute))
|
||||
where canGet && isPublic && !isStatic && !isIgnored
|
||||
select prop;
|
||||
}
|
||||
else validProperties = Array.Empty<PropertyInfo>();
|
||||
|
||||
foreach (FieldInfo field in validFields)
|
||||
{
|
||||
VkvKeyNameAttribute? namingAttribute = field.GetCustomAttribute<VkvKeyNameAttribute>();
|
||||
tree[namingAttribute?.name ?? field.Name] = ToNodeTree(field.GetValue(obj), options);
|
||||
}
|
||||
foreach (PropertyInfo prop in validProperties)
|
||||
{
|
||||
VkvKeyNameAttribute? namingAttribute = prop.GetCustomAttribute<VkvKeyNameAttribute>();
|
||||
tree[namingAttribute?.name ?? prop.Name] = ToNodeTree(prop.GetValue(obj), options);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!options.noExceptions) throw;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
3
SrcMod/Valve.NET/Vkv/VkvNode.cs
Normal file
3
SrcMod/Valve.NET/Vkv/VkvNode.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public abstract class VkvNode { }
|
||||
27
SrcMod/Valve.NET/Vkv/VkvOptions.cs
Normal file
27
SrcMod/Valve.NET/Vkv/VkvOptions.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public record class VkvOptions
|
||||
{
|
||||
public static VkvOptions Default => new();
|
||||
|
||||
public bool closeWhenFinished;
|
||||
public int indentSize;
|
||||
public bool noExceptions;
|
||||
public bool resetStreamPosition;
|
||||
public bool serializeProperties;
|
||||
public SpacingMode spacing;
|
||||
public bool useEscapeCodes;
|
||||
public bool useQuotes;
|
||||
|
||||
public VkvOptions()
|
||||
{
|
||||
closeWhenFinished = true;
|
||||
indentSize = 4;
|
||||
noExceptions = false;
|
||||
resetStreamPosition = false;
|
||||
serializeProperties = true;
|
||||
spacing = SpacingMode.DoubleTab;
|
||||
useEscapeCodes = false;
|
||||
useQuotes = false;
|
||||
}
|
||||
}
|
||||
57
SrcMod/Valve.NET/Vkv/VkvSerializer.cs
Normal file
57
SrcMod/Valve.NET/Vkv/VkvSerializer.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public class VkvSerializer
|
||||
{
|
||||
public VkvOptions Options => p_options;
|
||||
|
||||
private readonly VkvOptions p_options;
|
||||
|
||||
public VkvSerializer() : this(VkvOptions.Default) { }
|
||||
public VkvSerializer(VkvOptions options)
|
||||
{
|
||||
p_options = options;
|
||||
}
|
||||
|
||||
public VkvNode? Deserialize(Stream stream)
|
||||
{
|
||||
long pos = stream.Position;
|
||||
StreamReader reader = new(stream, leaveOpen: !p_options.closeWhenFinished);
|
||||
try
|
||||
{
|
||||
VkvNode? result = VkvConvert.DeserializeNode(reader, p_options);
|
||||
reader.Close();
|
||||
if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
reader.Close();
|
||||
if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
public T? Deserialize<T>(Stream stream)
|
||||
{
|
||||
VkvNode? result = Deserialize(stream);
|
||||
return VkvConvert.FromNodeTree<T>(result, p_options);
|
||||
}
|
||||
public object? Deserialize(Type outputType, Stream stream)
|
||||
{
|
||||
VkvNode? result = Deserialize(stream);
|
||||
return VkvConvert.FromNodeTree(outputType, result, p_options);
|
||||
}
|
||||
|
||||
public void Serialize(Stream stream, object? value, string parentNodeName)
|
||||
{
|
||||
VkvNode? nodeTree = VkvConvert.ToNodeTree(value, p_options);
|
||||
Serialize(stream, nodeTree, parentNodeName);
|
||||
}
|
||||
public void Serialize(Stream stream, VkvNode? parentNode, string parentNodeName)
|
||||
{
|
||||
long pos = stream.Position;
|
||||
StreamWriter writer = new(stream, leaveOpen: !p_options.closeWhenFinished);
|
||||
VkvConvert.SerializeNode(writer, parentNode, parentNodeName, p_options);
|
||||
writer.Close();
|
||||
|
||||
if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
11
SrcMod/Valve.NET/Vkv/VkvSingleNode.cs
Normal file
11
SrcMod/Valve.NET/Vkv/VkvSingleNode.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public class VkvSingleNode : VkvNode
|
||||
{
|
||||
public object? value;
|
||||
|
||||
public VkvSingleNode(object? value = null) : base()
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
45
SrcMod/Valve.NET/Vkv/VkvTreeNode.cs
Normal file
45
SrcMod/Valve.NET/Vkv/VkvTreeNode.cs
Normal file
@ -0,0 +1,45 @@
|
||||
namespace Valve.Vkv;
|
||||
|
||||
public class VkvTreeNode : VkvNode, IEnumerable<KeyValuePair<string, VkvNode?>>
|
||||
{
|
||||
public int SubNodeCount => p_subNodes.Count;
|
||||
|
||||
private readonly Dictionary<string, VkvNode?> p_subNodes;
|
||||
|
||||
public VkvTreeNode(Dictionary<string, VkvNode?>? subNodes = null) : base()
|
||||
{
|
||||
p_subNodes = subNodes ?? new();
|
||||
}
|
||||
|
||||
public VkvNode? this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (p_subNodes.TryGetValue(key, out VkvNode? value)) return value;
|
||||
else return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (p_subNodes.ContainsKey(key)) p_subNodes[key] = value;
|
||||
else p_subNodes.Add(key, value);
|
||||
}
|
||||
}
|
||||
public VkvNode? this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (p_subNodes.Count >= index || index < 0) return null;
|
||||
return p_subNodes.Values.ElementAt(index);
|
||||
}
|
||||
set
|
||||
{
|
||||
if (p_subNodes.Count >= index || index < 0) throw new IndexOutOfRangeException();
|
||||
p_subNodes[p_subNodes.Keys.ElementAt(index)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string key, VkvNode? value) => this[key] = value;
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public IEnumerator<KeyValuePair<string, VkvNode?>> GetEnumerator() => p_subNodes.GetEnumerator();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user