Merge pull request #105 from That-One-Nerd/main-canary
Ready for the first beta release.
This commit is contained in:
commit
f479f63662
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,11 +1,13 @@
|
|||||||
# Visual Studio stuff
|
# Visual Studio stuff
|
||||||
.vs/
|
.vs/
|
||||||
SrcMod/.vs/
|
SrcMod/.vs/
|
||||||
*.sln
|
|
||||||
|
|
||||||
# Compiled Files
|
# Compiled Files
|
||||||
SrcMod/Compiled
|
SrcMod/Compiled
|
||||||
SrcMod/Shell/obj/
|
SrcMod/Shell/obj/
|
||||||
|
|
||||||
|
SrcMod/Valve.NET/obj
|
||||||
|
|
||||||
# Personal Stuff
|
# Personal Stuff
|
||||||
|
SrcMod/Shell/Modules/TestingModule.cs
|
||||||
TODO.md
|
TODO.md
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
namespace SrcMod.Shell;
|
namespace SrcMod.Shell;
|
||||||
|
|
||||||
public class Game
|
public class Game : IEquatable<Game>
|
||||||
{
|
{
|
||||||
public static readonly Game Portal2 = new()
|
public static readonly Game Portal2 = new()
|
||||||
{
|
{
|
||||||
@ -8,12 +8,48 @@ public class Game
|
|||||||
NameId = "portal2",
|
NameId = "portal2",
|
||||||
SteamId = 620
|
SteamId = 620
|
||||||
};
|
};
|
||||||
|
public static readonly Game Unknown = new()
|
||||||
|
{
|
||||||
|
Name = "Unknown Game",
|
||||||
|
NameId = "unknown",
|
||||||
|
SteamId = -1,
|
||||||
|
IsUnknown = true
|
||||||
|
};
|
||||||
|
|
||||||
public required string Name { get; init; }
|
public string Name { get; private set; }
|
||||||
public required string NameId { get; init; }
|
public string NameId { get; private set; }
|
||||||
public required int SteamId { get; init; }
|
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 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,16 @@
|
|||||||
global using Nerd_STF.Mathematics;
|
global using Microsoft.Win32;
|
||||||
|
global using Nerd_STF.Mathematics;
|
||||||
global using Newtonsoft.Json;
|
global using Newtonsoft.Json;
|
||||||
global using SharpCompress.Archives.Rar;
|
global using SharpCompress.Archives.Rar;
|
||||||
global using SharpCompress.Archives.SevenZip;
|
global using SharpCompress.Archives.SevenZip;
|
||||||
global using SharpCompress.Readers;
|
global using SharpCompress.Readers;
|
||||||
global using SrcMod.Shell.Extensions;
|
global using SrcMod.Shell.Extensions;
|
||||||
global using SrcMod.Shell.Interop;
|
global using SrcMod.Shell.Interop;
|
||||||
|
global using SrcMod.Shell.Modules;
|
||||||
global using SrcMod.Shell.Modules.ObjectModels;
|
global using SrcMod.Shell.Modules.ObjectModels;
|
||||||
global using SrcMod.Shell.ObjectModels;
|
global using SrcMod.Shell.ObjectModels;
|
||||||
|
global using SrcMod.Shell.ObjectModels.Source;
|
||||||
|
global using SrcMod.Shell.ObjectModels.Steam;
|
||||||
global using System;
|
global using System;
|
||||||
global using System.Collections;
|
global using System.Collections;
|
||||||
global using System.Collections.Generic;
|
global using System.Collections.Generic;
|
||||||
@ -20,4 +24,5 @@ global using System.Reflection;
|
|||||||
global using System.Runtime.InteropServices;
|
global using System.Runtime.InteropServices;
|
||||||
global using System.Text;
|
global using System.Text;
|
||||||
global using System.Threading;
|
global using System.Threading;
|
||||||
|
global using Valve.Vkv;
|
||||||
global using static SrcMod.Shell.Tools;
|
global using static SrcMod.Shell.Tools;
|
||||||
@ -2,24 +2,183 @@
|
|||||||
|
|
||||||
public class Mod
|
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 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()
|
private Mod()
|
||||||
{
|
{
|
||||||
|
BaseGame = Game.Unknown;
|
||||||
|
SearchPaths = new();
|
||||||
|
HiddenMaps = Array.Empty<string>();
|
||||||
Name = string.Empty;
|
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)
|
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;
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,35 +2,73 @@
|
|||||||
|
|
||||||
public static class TypeParsers
|
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)
|
public static object ParseAll(string msg)
|
||||||
{
|
{
|
||||||
if (TryParse(msg, out sbyte int8)) return int8;
|
if (TryParseBool(msg, out bool resBool)) return resBool;
|
||||||
if (TryParse(msg, out byte uInt8)) return uInt8;
|
else if (TryParse(msg, out sbyte int8)) return int8;
|
||||||
if (TryParse(msg, out short int16)) return int16;
|
else if (TryParse(msg, out byte uInt8)) return uInt8;
|
||||||
if (TryParse(msg, out ushort uInt16)) return uInt16;
|
else if (TryParse(msg, out short int16)) return int16;
|
||||||
if (TryParse(msg, out int int32)) return int32;
|
else if (TryParse(msg, out ushort uInt16)) return uInt16;
|
||||||
if (TryParse(msg, out uint uInt32)) return uInt32;
|
else if (TryParse(msg, out int int32)) return int32;
|
||||||
if (TryParse(msg, out long int64)) return int64;
|
else if (TryParse(msg, out uint uInt32)) return uInt32;
|
||||||
if (TryParse(msg, out ulong uInt64)) return uInt64;
|
else if (TryParse(msg, out long int64)) return int64;
|
||||||
if (TryParse(msg, out Int128 int128)) return int128;
|
else if (TryParse(msg, out ulong uInt64)) return uInt64;
|
||||||
if (TryParse(msg, out UInt128 uInt128)) return uInt128;
|
else if (TryParse(msg, out Int128 int128)) return int128;
|
||||||
if (TryParse(msg, out nint intPtr)) return intPtr;
|
else if (TryParse(msg, out UInt128 uInt128)) return uInt128;
|
||||||
if (TryParse(msg, out nuint uIntPtr)) return uIntPtr;
|
else if (TryParse(msg, out nint intPtr)) return intPtr;
|
||||||
if (TryParse(msg, out Half float16)) return float16;
|
else if (TryParse(msg, out nuint uIntPtr)) return uIntPtr;
|
||||||
if (TryParse(msg, out float float32)) return float32;
|
else if (TryParse(msg, out Half float16)) return float16;
|
||||||
if (TryParse(msg, out double float64)) return float64;
|
else if (TryParse(msg, out float float32)) return float32;
|
||||||
if (TryParse(msg, out decimal float128)) return float128;
|
else if (TryParse(msg, out double float64)) return float64;
|
||||||
if (TryParse(msg, out char resChar)) return resChar;
|
else if (TryParse(msg, out decimal float128)) return float128;
|
||||||
if (TryParse(msg, out DateOnly dateOnly)) return dateOnly;
|
else if (TryParse(msg, out char resChar)) return resChar;
|
||||||
if (TryParse(msg, out DateTime dateTime)) return dateTime;
|
else if (TryParse(msg, out DateOnly dateOnly)) return dateOnly;
|
||||||
if (TryParse(msg, out DateTimeOffset dateTimeOffset)) return dateTimeOffset;
|
else if (TryParse(msg, out DateTime dateTime)) return dateTime;
|
||||||
if (TryParse(msg, out Guid guid)) return guid;
|
else if (TryParse(msg, out DateTimeOffset dateTimeOffset)) return dateTimeOffset;
|
||||||
if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly;
|
else if (TryParse(msg, out Guid guid)) return guid;
|
||||||
if (TryParse(msg, out TimeSpan timeSpan)) return timeSpan;
|
else if (TryParse(msg, out TimeOnly timeOnly)) return timeOnly;
|
||||||
|
else if (TryParse(msg, out TimeSpan timeSpan)) return timeSpan;
|
||||||
return msg;
|
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>
|
public static bool TryParse<T>(string msg, out T? result) where T : IParsable<T>
|
||||||
=> T.TryParse(msg, null, out result);
|
=> T.TryParse(msg, null, out result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,9 @@ public class Config
|
|||||||
{
|
{
|
||||||
public const string FilePath = "config.json";
|
public const string FilePath = "config.json";
|
||||||
|
|
||||||
|
public static bool HasDisplayableError => false;
|
||||||
|
public static bool HasDisplayableWarning => p_printedLastSteamWarning;
|
||||||
|
|
||||||
public static Config Defaults => new();
|
public static Config Defaults => new();
|
||||||
|
|
||||||
private static readonly FieldInfo[] p_configSharedFields;
|
private static readonly FieldInfo[] p_configSharedFields;
|
||||||
@ -22,8 +25,14 @@ public class Config
|
|||||||
private static Config p_applied;
|
private static Config p_applied;
|
||||||
private static Changes? p_changes;
|
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()
|
static Config()
|
||||||
{
|
{
|
||||||
|
// Generate shared fields between the config class and its changes equivalent.
|
||||||
p_applied = Defaults;
|
p_applied = Defaults;
|
||||||
|
|
||||||
FieldInfo[] configFields = (from field in typeof(Config).GetFields()
|
FieldInfo[] configFields = (from field in typeof(Config).GetFields()
|
||||||
@ -62,11 +71,67 @@ public class Config
|
|||||||
|
|
||||||
public string[] GameDirectories;
|
public string[] GameDirectories;
|
||||||
public AskMode RunUnsafeCommands;
|
public AskMode RunUnsafeCommands;
|
||||||
|
public bool UseLocalModDirectories;
|
||||||
|
|
||||||
internal Config()
|
internal Config()
|
||||||
{
|
{
|
||||||
GameDirectories = Array.Empty<string>();
|
// 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;
|
RunUnsafeCommands = AskMode.Ask;
|
||||||
|
UseLocalModDirectories = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Config ApplyChanges(Changes changes)
|
public Config ApplyChanges(Changes changes)
|
||||||
@ -132,7 +197,7 @@ public class Config
|
|||||||
}
|
}
|
||||||
StreamReader reader = new(fullPath);
|
StreamReader reader = new(fullPath);
|
||||||
JsonTextReader jsonReader = new(reader);
|
JsonTextReader jsonReader = new(reader);
|
||||||
p_changes = Serializer.Deserialize<Changes?>(jsonReader);
|
p_changes = Tools.SerializerJson.Deserialize<Changes?>(jsonReader);
|
||||||
jsonReader.Close();
|
jsonReader.Close();
|
||||||
reader.Close();
|
reader.Close();
|
||||||
|
|
||||||
@ -153,7 +218,7 @@ public class Config
|
|||||||
{
|
{
|
||||||
Indentation = 4
|
Indentation = 4
|
||||||
};
|
};
|
||||||
Serializer.Serialize(jsonWriter, p_changes);
|
Tools.SerializerJson.Serialize(jsonWriter, p_changes);
|
||||||
jsonWriter.Close();
|
jsonWriter.Close();
|
||||||
writer.Close();
|
writer.Close();
|
||||||
}
|
}
|
||||||
@ -167,6 +232,7 @@ public class Config
|
|||||||
{
|
{
|
||||||
public string[]? GameDirectories;
|
public string[]? GameDirectories;
|
||||||
public AskMode? RunUnsafeCommands;
|
public AskMode? RunUnsafeCommands;
|
||||||
|
public bool? UseLocalModDirectories;
|
||||||
|
|
||||||
public bool Any() => typeof(Changes).GetFields().Any(x => x.GetValue(this) is not null);
|
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 Author = "That_One_Nerd";
|
||||||
public const string Name = "SrcMod";
|
public const string Name = "SrcMod";
|
||||||
public const string Version = "Alpha 0.4.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;
|
public readonly string? ShellDirectory;
|
||||||
|
|
||||||
@ -16,10 +22,12 @@ public class Shell
|
|||||||
public List<HistoryItem> History;
|
public List<HistoryItem> History;
|
||||||
public string WorkingDirectory;
|
public string WorkingDirectory;
|
||||||
|
|
||||||
private bool lastCancel;
|
private bool p_lastCancel;
|
||||||
private bool printedCancel;
|
private bool p_printedCancel;
|
||||||
|
|
||||||
private BackgroundWorker? activeCommand;
|
private bool p_printedLastReloadError;
|
||||||
|
|
||||||
|
private BackgroundWorker? p_activeCommand;
|
||||||
|
|
||||||
public Shell()
|
public Shell()
|
||||||
{
|
{
|
||||||
@ -88,8 +96,8 @@ public class Shell
|
|||||||
Write(" by ", ConsoleColor.White, false);
|
Write(" by ", ConsoleColor.White, false);
|
||||||
Write($"{Author}", ConsoleColor.DarkYellow);
|
Write($"{Author}", ConsoleColor.DarkYellow);
|
||||||
|
|
||||||
lastCancel = false;
|
p_lastCancel = false;
|
||||||
activeCommand = null;
|
p_activeCommand = null;
|
||||||
Console.CancelKeyPress += HandleCancel;
|
Console.CancelKeyPress += HandleCancel;
|
||||||
|
|
||||||
ActiveGame = null;
|
ActiveGame = null;
|
||||||
@ -139,9 +147,21 @@ public class Shell
|
|||||||
|
|
||||||
public string ReadLine()
|
public string ReadLine()
|
||||||
{
|
{
|
||||||
Write($"\n{WorkingDirectory}", ConsoleColor.DarkGreen, false);
|
Write("\n", newLine: false);
|
||||||
if (ActiveGame is not null) Write($" {ActiveGame}", ConsoleColor.DarkYellow, false);
|
if (HasAnyDisplayableError) Write($"(Error) ", ConsoleColor.DarkRed, false);
|
||||||
if (ActiveMod is not null) Write($" {ActiveMod}", ConsoleColor.Magenta, 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(null);
|
||||||
|
|
||||||
Write($" {Name}", ConsoleColor.DarkCyan, false);
|
Write($" {Name}", ConsoleColor.DarkCyan, false);
|
||||||
@ -149,7 +169,7 @@ public class Shell
|
|||||||
|
|
||||||
bool printed = false;
|
bool printed = false;
|
||||||
|
|
||||||
if (lastCancel && !printedCancel)
|
if (p_lastCancel && !p_printedCancel)
|
||||||
{
|
{
|
||||||
// Print the warning. A little bit of mess because execution must
|
// Print the warning. A little bit of mess because execution must
|
||||||
// continue without funny printing errors but it's alright I guess.
|
// continue without funny printing errors but it's alright I guess.
|
||||||
@ -160,7 +180,7 @@ public class Shell
|
|||||||
Write("Press ^C again to exit the shell.", ConsoleColor.Red);
|
Write("Press ^C again to exit the shell.", ConsoleColor.Red);
|
||||||
PlayWarningSound();
|
PlayWarningSound();
|
||||||
|
|
||||||
printedCancel = true;
|
p_printedCancel = true;
|
||||||
Console.CursorTop += 2;
|
Console.CursorTop += 2;
|
||||||
|
|
||||||
Console.CursorLeft = originalLeft;
|
Console.CursorLeft = originalLeft;
|
||||||
@ -175,8 +195,8 @@ public class Shell
|
|||||||
|
|
||||||
if (!printed)
|
if (!printed)
|
||||||
{
|
{
|
||||||
lastCancel = false;
|
p_lastCancel = false;
|
||||||
printedCancel = false;
|
p_printedCancel = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
@ -262,18 +282,18 @@ public class Shell
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activeCommand = new();
|
p_activeCommand = new();
|
||||||
activeCommand.DoWork += runCommand;
|
p_activeCommand.DoWork += runCommand;
|
||||||
activeCommand.RunWorkerAsync();
|
p_activeCommand.RunWorkerAsync();
|
||||||
|
|
||||||
activeCommand.WorkerSupportsCancellation = command.CanBeCancelled;
|
p_activeCommand.WorkerSupportsCancellation = command.CanBeCancelled;
|
||||||
|
|
||||||
while (activeCommand is not null && activeCommand.IsBusy) Thread.Yield();
|
while (p_activeCommand is not null && p_activeCommand.IsBusy) Thread.Yield();
|
||||||
|
|
||||||
if (activeCommand is not null)
|
if (p_activeCommand is not null)
|
||||||
{
|
{
|
||||||
activeCommand.Dispose();
|
p_activeCommand.Dispose();
|
||||||
activeCommand = null;
|
p_activeCommand = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShellDirectory is null) Write("[WARNING] Could not save config to shell location. Any changes will be ignored.");
|
if (ShellDirectory is null) Write("[WARNING] Could not save config to shell location. Any changes will be ignored.");
|
||||||
@ -300,24 +320,45 @@ public class Shell
|
|||||||
|
|
||||||
public void ReloadDirectoryInfo()
|
public void ReloadDirectoryInfo()
|
||||||
{
|
{
|
||||||
ActiveMod = Mod.ReadDirectory(WorkingDirectory);
|
try
|
||||||
|
{
|
||||||
|
ActiveMod = Mod.ReadDirectory(WorkingDirectory);
|
||||||
|
ActiveGame = ActiveMod?.BaseGame;
|
||||||
|
|
||||||
// Update title.
|
// Update title.
|
||||||
string title = "SrcMod";
|
string title = "SrcMod";
|
||||||
if (ActiveMod is not null) title += $" - {ActiveMod.Name}";
|
if (ActiveMod is not null) title += $" - {ActiveMod.Name}";
|
||||||
Console.Title = title;
|
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)
|
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.
|
// Kill the active command.
|
||||||
activeCommand.CancelAsync();
|
p_activeCommand.CancelAsync();
|
||||||
activeCommand.Dispose();
|
p_activeCommand.Dispose();
|
||||||
activeCommand = null;
|
p_activeCommand = null;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -326,18 +367,18 @@ public class Shell
|
|||||||
PlayErrorSound();
|
PlayErrorSound();
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCancel = false;
|
p_lastCancel = false;
|
||||||
printedCancel = false;
|
p_printedCancel = false;
|
||||||
args.Cancel = true;
|
args.Cancel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Due to some funny multithreading issues, we want to make the warning label
|
// Due to some funny multithreading issues, we want to make the warning label
|
||||||
// single-threaded on the shell.
|
// single-threaded on the shell.
|
||||||
if (!lastCancel)
|
if (!p_lastCancel)
|
||||||
{
|
{
|
||||||
// Enable the warning. The "ReadLine" method will do the rest.
|
// 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.
|
args.Cancel = true; // "Cancel" referring to the cancellation of the cancel operation.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
<ImplicitUsings>disable</ImplicitUsings>
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>srcmod</AssemblyName>
|
<AssemblyName>srcmod</AssemblyName>
|
||||||
@ -16,16 +16,6 @@
|
|||||||
<NoWin32Manifest>true</NoWin32Manifest>
|
<NoWin32Manifest>true</NoWin32Manifest>
|
||||||
</PropertyGroup>
|
</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>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Logo.ico" />
|
<Content Include="Logo.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -36,4 +26,8 @@
|
|||||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Valve.NET\Valve.NET.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -2,15 +2,27 @@
|
|||||||
|
|
||||||
public static class Tools
|
public static class Tools
|
||||||
{
|
{
|
||||||
public static JsonSerializer Serializer { get; private set; }
|
public static JsonSerializer SerializerJson { get; private set; }
|
||||||
|
public static VkvSerializer SerializeVkv { get; private set; }
|
||||||
|
|
||||||
static Tools()
|
static Tools()
|
||||||
{
|
{
|
||||||
Serializer = JsonSerializer.Create(new()
|
SerializerJson = JsonSerializer.Create(new()
|
||||||
{
|
{
|
||||||
Formatting = Formatting.Indented,
|
Formatting = Formatting.Indented,
|
||||||
NullValueHandling = NullValueHandling.Ignore
|
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)
|
public static void DisplayWithPages(IEnumerable<string> lines, ConsoleColor? color = null)
|
||||||
|
|||||||
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
|
||||||
17
SrcMod/Valve.NET/Miscellaneous/GlobalUsings.cs
Normal file
17
SrcMod/Valve.NET/Miscellaneous/GlobalUsings.cs
Normal 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;
|
||||||
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