Compare commits

..

78 Commits

Author SHA1 Message Date
f479f63662
Merge pull request #105 from That-One-Nerd/main-canary
Ready for the first beta release.
2023-05-18 13:05:42 -04:00
8804ac08d9 Synced type parsers. 2023-05-18 11:12:27 -04:00
287cd26bcb Shell now syncs its active game with the mod's base game. 2023-05-18 11:06:28 -04:00
3a961fffbb Forgot to actually make the variable do something. 2023-05-16 16:18:11 -04:00
7cad137f39 Added a no exception mode for the VKV serializers. 2023-05-16 16:10:32 -04:00
82afefd3e7 Added the VkvKeyName attribute. 2023-05-16 09:02:19 -04:00
858bd580b6 Fixed a config parsing error with booleans. 2023-05-16 08:31:06 -04:00
dfda870eac Added final tweaks to parsing mod info (for now). 2023-05-16 08:05:13 -04:00
f25b366a4d Made a bunch of progress parsing mod info, almost done. 2023-05-15 10:50:58 -04:00
5b2b0abfa3 Added some error handling in the shell. 2023-05-12 10:47:13 -04:00
2abadc4071 GameInfo doesn't crash anymore, but still needs to be improved. 2023-05-12 07:41:48 -04:00
be6e4a496b Some progress on gameinfo parsing (DOESNT WORK YET) 2023-05-11 21:45:03 -04:00
62d3ba6492 Active mod is now detected in parent directories. 2023-05-11 17:43:17 -04:00
7854a576a7 Reformatted command prompt header and added relative paths. 2023-05-11 17:33:11 -04:00
92b22d6749 One more tiny thing. 2023-05-11 17:13:02 -04:00
45e23b9924 Added some comments. 2023-05-11 17:11:17 -04:00
c0afa7dbb4 Automatically detects steam install location now. 2023-05-11 15:38:41 -04:00
321d6acb7f Merge remote-tracking branch 'origin/shell-systems' into steam-connections 2023-05-10 17:15:01 -04:00
eee6306428 SrcMod is a windows-only thing now. 2023-05-10 17:14:05 -04:00
bcf02f4ecd Fixed issue #89. One line fix. 2023-05-10 16:28:21 -04:00
d83c16827f Shell now automatically detects steam directories (prone to bugs). 2023-05-10 14:07:43 -04:00
13c9adf781 Added different tabbing options for VKV key and value seperation. 2023-05-10 13:55:46 -04:00
7c96f30d1b Finished VKV deserialization development :) 2023-05-10 13:34:22 -04:00
c1a90cfc99 Array and list support for VKV. 2023-05-10 12:50:30 -04:00
ab0453e0ab Nice progress on the node tree conversion. 2023-05-10 09:56:12 -04:00
7e1492c664 Small improvements. 2023-05-10 07:59:15 -04:00
ab372b3eec Man, I meant to do this too. Double whoops. 2023-05-09 22:19:26 -04:00
154d21ee13 Whoops. 2023-05-09 22:18:00 -04:00
3c0c3068a4 Moved the valve parsers to their own library. 2023-05-09 19:55:13 -04:00
856811687e Renamed VDF stuff to VKV as it should be and moved to subfolder. 2023-05-09 18:01:12 -04:00
71edd4fffa Moved some vkv objects into an objectmodels folder 2023-05-09 17:56:43 -04:00
e6198cd035 Some small node tree conversion progress. 2023-05-09 14:08:28 -04:00
7428ac9110 Hopefully VDF deserialization works now. 2023-05-09 09:46:55 -04:00
1ee60cdf65 Finished the first complete VDF tree creator. 2023-05-08 08:44:50 -04:00
bb520424ac More tree construction progress. Small. 2023-05-07 21:23:04 -04:00
54ea2b5203 Some progress in creating the vdf node tree. 2023-05-07 14:21:24 -04:00
20413603e3 Forgot we're in beta now. 2023-05-07 13:54:25 -04:00
03128d3a57 Improved serialization of node tree and began tree generation. 2023-05-07 13:39:59 -04:00
46f4c779d2 More development on the serializer. 2023-04-27 09:10:34 -04:00
4e2a8ec05c Some development with the serializer. 2023-04-26 20:38:28 -04:00
4d64b0bae6 Small starts on the serializer. 2023-04-26 14:09:28 -04:00
864c39be7f Tiny change. New testing module (for me obviously). 2023-04-26 09:49:01 -04:00
97a799e10d
Merge pull request #72 from That-One-Nerd/shell-configuration
Ready for alpha 0.4.0 (the final alpha version (hopefully))
2023-04-25 17:42:31 -04:00
a0c50c4cab
Changed version to 0.4.0 2023-04-25 17:42:02 -04:00
2f8565529f Merge remote-tracking branch 'origin/shell-systems' into shell-configuration 2023-04-25 17:31:55 -04:00
3f5319adc0 Updated with main. 2023-04-25 17:30:49 -04:00
2e61bcfbec Automated as much of the config system as possible. 2023-04-25 17:27:14 -04:00
6f65417fe7 Merged with the small debug update. 2023-04-25 17:22:21 -04:00
2dc37cd6ac Debug mode gives better errors now (tiny patch) 2023-04-25 17:19:07 -04:00
d8df50a928 Tiny change. 2023-04-24 17:33:51 -04:00
35885b8724 Fixed the config not saving to its file. 2023-04-20 08:20:18 -04:00
b446feefe7 Made the config remove system automated. 2023-04-20 07:27:38 -04:00
409817f208 Merge remote-tracking branch 'origin/main' into shell-configuration 2023-04-20 07:21:47 -04:00
2420e16fa4 Made the config append system fully automated. 2023-04-19 20:37:54 -04:00
b5517e724a Added automatic config resetting capabilities. 2023-04-19 19:28:30 -04:00
dd62e66871 Added automated config setting. Next up resetting. 2023-04-19 14:02:23 -04:00
647993ef1f One teeny tiny more thing. 2023-04-19 07:28:40 -04:00
318c8119a0 Small change in the config display system. 2023-04-19 07:21:59 -04:00
8677ff464d Made the display system of the config data a gazillion times better. 2023-04-18 16:03:01 -04:00
aac52a43ac
Merge pull request #57 from That-One-Nerd/shell-configuration
New shell release with some config files and commands.
2023-04-18 09:16:54 -04:00
25c6d152a2 Removed some test stuff. 2023-04-18 08:57:45 -04:00
fd4162aa32 How on earth did I miss this? 2023-04-18 08:52:04 -04:00
3ecb967b91 Merged the new shell improvements. 2023-04-18 08:40:48 -04:00
4d2e98ce42 Moved the loading bar to its own class. Small fix. 2023-04-18 08:31:40 -04:00
b869de9be8 Directory info is now reloaded after every command. One liner fix. 2023-04-17 21:02:39 -04:00
b7a4333e2d Added the ability to load custom assemblies to the shell. 2023-04-17 20:27:40 -04:00
ec15e78564 Added some config modifier commands. Definitely not very automated, stuff to do later. 2023-04-17 20:12:52 -04:00
8bfd8c142c Quick update, added some more config display options. 2023-04-17 19:38:31 -04:00
4d342ae958 Added some config stuff. Next is modifying the config. 2023-04-17 19:11:33 -04:00
3742b4a230 Some config display changes. WIP 2023-04-17 17:58:28 -04:00
0c07c9cfe5 Some tiny stuff. More tomorrow 2023-04-10 19:23:46 -04:00
b2bc9fa7ee New config system. Tracks changes instead of the whole thing. 2023-04-10 18:46:40 -04:00
68336df868 Made some basic start to the config stuff. More will come later. 2023-04-10 13:27:55 -04:00
24fde24746
Merge pull request #44 from That-One-Nerd/misc-commands
Added a couple misc commands and fixed some bugs.
2023-04-01 20:39:55 -04:00
a2b9daa107 Made a command that can be used to run other processes. 2023-04-01 20:35:30 -04:00
daae786bec Added commands to make new directories and new files. 2023-04-01 20:16:37 -04:00
e8a5cbc846 Oh look, another one liner. Fixed #34 2023-04-01 15:22:38 -04:00
4c919a152c Fixed issue #28. Literally a one-liner. 2023-04-01 15:05:58 -04:00
32 changed files with 2048 additions and 303 deletions

4
.gitignore vendored
View File

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

View File

@ -0,0 +1,33 @@
namespace SrcMod.Shell.Extensions;
public static class ConversionExtension
{
public static T Cast<T>(this object obj) => (T)Cast(obj, typeof(T));
public static object Cast(this object obj, Type newType) => Convert.ChangeType(obj, newType);
public static object CastArray(this object[] obj, Type newElementType)
{
Array result = Array.CreateInstance(newElementType, obj.Length);
for (int i = 0; i < obj.Length; i++) result.SetValue(obj[i].Cast(newElementType), i);
return result;
}
public static T[] CastArray<T>(this object[] obj)
{
Array result = Array.CreateInstance(typeof(T), obj.Length);
for (int i = 0; i < obj.Length; i++) result.SetValue(obj[i].Cast<T>(), i);
return (T[])result;
}
public static object CastArray(this Array obj, Type newElementType)
{
Array result = Array.CreateInstance(newElementType, obj.Length);
for (int i = 0; i < obj.Length; i++) result.SetValue(obj.GetValue(i)!.Cast(newElementType), i);
return result;
}
public static T[] CastArray<T>(this Array obj)
{
Array result = Array.CreateInstance(typeof(T), obj.Length);
for (int i = 0; i < obj.Length; i++) result.SetValue(obj.GetValue(i)!.Cast<T>(), i);
return (T[])result;
}
}

View File

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

View 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);
}
}

View File

@ -1,12 +1,20 @@
global using Nerd_STF.Mathematics;
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;
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;
@ -16,4 +24,5 @@ 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;

View File

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

View File

@ -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);
@ -100,110 +100,6 @@ 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(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("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("cut")]
[Command("move")]
public static void MoveFile(string source, string destination)
@ -273,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)
{
@ -290,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);
@ -309,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;
@ -325,7 +377,7 @@ public static class BaseModule
Write(message, newLine: false);
}
LoadingBarEnd();
LoadingBar.End();
Console.CursorLeft = 0;
Write(new string(' ', Console.BufferWidth), newLine: false);
@ -342,13 +394,6 @@ public static class BaseModule
});
}
[Command("exit")]
[Command("quit")]
public static void QuitShell(int code = 0)
{
Environment.Exit(code);
}
[Command("undo")]
public static void UndoCommand(int amount = 1)
{

View File

@ -168,7 +168,7 @@ public static class CompressionModule
int failed = 0;
LoadingBarStart();
LoadingBar.Start();
for (int i = 0; i < files.Count; i++)
{
bool failedThisTime = false;
@ -181,7 +181,7 @@ public static class CompressionModule
failedThisTime = true;
failed++;
}
LoadingBarSet((i + 1) / (float)files.Count, failedThisTime ? ConsoleColor.Red : ConsoleColor.DarkGreen); ;
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;
@ -194,7 +194,7 @@ public static class CompressionModule
archive.Dispose();
writer.Dispose();
LoadingBarEnd();
LoadingBar.End();
Console.CursorLeft = 0;
Write(new string(' ', Console.BufferWidth), newLine: false);

View 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);
}
}

View File

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

View File

@ -0,0 +1,8 @@
namespace SrcMod.Shell.ObjectModels;
public enum AskMode : sbyte
{
Never = -1,
Ask = 0,
Always = 1
}

View 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);
}
}

View 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();
}
}
}

View 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();
}
}

View File

@ -1,12 +1,16 @@
using System.ComponentModel;
namespace SrcMod.Shell;
namespace SrcMod.Shell;
public class Shell
{
public const string Author = "That_One_Nerd";
public const string Name = "SrcMod";
public const string Version = "Alpha 0.3.1";
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;
@ -18,10 +22,12 @@ public class Shell
public List<HistoryItem> History;
public string WorkingDirectory;
private bool lastCancel;
private bool printedCancel;
private bool p_lastCancel;
private bool p_printedCancel;
private BackgroundWorker? activeCommand;
private bool p_printedLastReloadError;
private BackgroundWorker? p_activeCommand;
public Shell()
{
@ -50,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()
{
@ -86,8 +96,8 @@ public class Shell
Write(" by ", ConsoleColor.White, false);
Write($"{Author}", ConsoleColor.DarkYellow);
lastCancel = false;
activeCommand = null;
p_lastCancel = false;
p_activeCommand = null;
Console.CancelKeyPress += HandleCancel;
ActiveGame = null;
@ -95,6 +105,26 @@ public class Shell
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()
{
@ -117,9 +147,21 @@ public class Shell
public string ReadLine()
{
Write($"\n{WorkingDirectory}", ConsoleColor.DarkGreen, false);
if (ActiveGame is not null) Write($" {ActiveGame}", ConsoleColor.DarkYellow, false);
if (ActiveMod is not null) Write($" {ActiveMod}", ConsoleColor.Magenta, false);
Write("\n", newLine: false);
if (HasAnyDisplayableError) Write($"(Error) ", ConsoleColor.DarkRed, false);
else if (HasAnyDisplayableWarning) Write($"(Warning) ", ConsoleColor.DarkYellow, 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);
@ -127,7 +169,7 @@ public class Shell
bool printed = false;
if (lastCancel && !printedCancel)
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.
@ -138,7 +180,7 @@ public class Shell
Write("Press ^C again to exit the shell.", ConsoleColor.Red);
PlayWarningSound();
printedCancel = true;
p_printedCancel = true;
Console.CursorTop += 2;
Console.CursorLeft = originalLeft;
@ -153,8 +195,8 @@ public class Shell
if (!printed)
{
lastCancel = false;
printedCancel = false;
p_lastCancel = false;
p_printedCancel = false;
}
return message;
@ -217,39 +259,47 @@ public class Shell
void runCommand(object? sender, DoWorkEventArgs e)
{
#if RELEASE
try
{
#endif
command.Invoke(args);
#if RELEASE
}
#if RELEASE
catch (TargetInvocationException ex)
{
Write($"[ERROR] {ex.InnerException!.Message}", ConsoleColor.Red);
if (LoadingBarEnabled) LoadingBarEnd();
}
catch (Exception ex)
{
Write($"[ERROR] {ex.Message}", ConsoleColor.Red);
if (LoadingBarEnabled) LoadingBarEnd();
if (LoadingBar.Enabled) LoadingBar.End();
}
#endif
}
activeCommand = new();
activeCommand.DoWork += runCommand;
activeCommand.RunWorkerAsync();
activeCommand.WorkerSupportsCancellation = command.CanBeCancelled;
while (activeCommand is not null && activeCommand.IsBusy) Thread.Yield();
if (activeCommand is not null)
catch (Exception ex)
{
activeCommand.Dispose();
activeCommand = null;
#if RELEASE
Write($"[ERROR] {ex.Message}", ConsoleColor.Red);
if (LoadingBar.Enabled) LoadingBar.End();
#else
Write($"[ERROR] {ex}", ConsoleColor.Red);
if (LoadingBar.Enabled) LoadingBar.End();
#endif
}
}
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;
}
}
@ -269,25 +319,46 @@ public class Shell
}
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 (activeCommand is not null && activeCommand.IsBusy)
if (p_activeCommand is not null && p_activeCommand.IsBusy)
{
if (activeCommand.WorkerSupportsCancellation)
if (p_activeCommand.WorkerSupportsCancellation)
{
// Kill the active command.
activeCommand.CancelAsync();
activeCommand.Dispose();
activeCommand = null;
p_activeCommand.CancelAsync();
p_activeCommand.Dispose();
p_activeCommand = null;
}
else
{
@ -296,18 +367,18 @@ public class Shell
PlayErrorSound();
}
lastCancel = false;
printedCancel = false;
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 (!lastCancel)
if (!p_lastCancel)
{
// Enable the warning. The "ReadLine" method will do the rest.
lastCancel = true;
p_lastCancel = true;
args.Cancel = true; // "Cancel" referring to the cancellation of the cancel operation.
return;
}

View File

@ -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,7 +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>

View File

@ -1,16 +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;
private static float loadingBarValue = 0;
private static ConsoleColor loadingBarColor = Console.ForegroundColor;
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)
{
@ -73,73 +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);
loadingBarValue = value;
loadingBarColor = color ?? Console.ForegroundColor;
// 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++;
while (oldPos.y >= Console.BufferHeight)
{
Console.WriteLine();
oldPos.y--;
loadingPosition--;
}
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;
LoadingBarEnabled = true;
LoadingBarSet(value, color);
}
public static void Write(object? message, ConsoleColor? col = null, bool newLine = true)
{
ConsoleColor prevCol = Console.ForegroundColor;
@ -150,16 +96,34 @@ public static class Tools
Console.ForegroundColor = prevCol;
if (newLine && LoadingBarEnabled && Console.CursorTop >= Console.BufferHeight - 1)
if (newLine && LoadingBar.Enabled && Console.CursorTop >= Console.BufferHeight - 1)
{
loadingPosition--;
LoadingBarSet(loadingBarValue, loadingBarColor);
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;
@ -167,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
View 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

View File

@ -0,0 +1,17 @@
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;
global using Valve.Miscellaneous;
global using Valve.Vkv;
global using Valve.Vkv.ObjectModels;

View 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);
}

View 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>

View File

@ -0,0 +1,6 @@
namespace Valve.Vkv.ObjectModels;
public interface IVkvConvertible
{
public VkvNode ToNodeTree();
}

View File

@ -0,0 +1,4 @@
namespace Valve.Vkv.ObjectModels;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class VkvIgnoreAttribute : Attribute { }

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

View File

@ -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) { }
}

View File

@ -0,0 +1,8 @@
namespace Valve.Vkv;
public enum SpacingMode
{
SingleSpace = 0,
IndentSizeSpacing,
DoubleTab,
}

View 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
}

View File

@ -0,0 +1,3 @@
namespace Valve.Vkv;
public abstract class VkvNode { }

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

View 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);
}
}

View File

@ -0,0 +1,11 @@
namespace Valve.Vkv;
public class VkvSingleNode : VkvNode
{
public object? value;
public VkvSingleNode(object? value = null) : base()
{
this.value = value;
}
}

View 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();
}