Compare commits

..

1 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
10 changed files with 20 additions and 869 deletions

View File

@ -2,17 +2,10 @@
internal static partial class Kernel32 internal static partial class Kernel32
{ {
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetConsoleScreenBufferInfo(nint hConsoleOutput, out ConsoleScreenBufferInfo lpConsoleScreenBufferInfo);
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint GetConsoleWindow();
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll", SetLastError = true)]
public static partial uint GetFinalPathNameByHandleA(nint hFile, [MarshalAs(UnmanagedType.LPTStr)] string lpszFilePath, public static partial uint GetFinalPathNameByHandleA(nint hFile, [MarshalAs(UnmanagedType.LPTStr)] string lpszFilePath,
uint cchFilePath, uint dwFlags); uint cchFilePath, uint dwFlags);
[LibraryImport("kernel32.dll", SetLastError = true)] [LibraryImport("kernel32.dll")]
public static partial nuint GlobalSize(nint hPtr); public static partial nuint GlobalSize(nint hPtr);
} }

View File

@ -1,15 +0,0 @@
namespace SrcMod.Shell.Interop.ObjectModels;
[StructLayout(LayoutKind.Sequential)]
internal struct ConsoleScreenBufferInfo
{
[MarshalAs(UnmanagedType.LPStruct)]
public Coord dwSize;
[MarshalAs(UnmanagedType.LPStruct)]
public Coord dwCursorPosition;
public int wAttributes;
[MarshalAs(UnmanagedType.LPStruct)]
public SmallRect srWindow;
[MarshalAs(UnmanagedType.LPStruct)]
public Coord dwMaximumWindowSize;
}

View File

@ -1,8 +0,0 @@
namespace SrcMod.Shell.Interop.ObjectModels;
[StructLayout(LayoutKind.Sequential)]
internal struct Coord
{
public short X;
public short Y;
}

View File

@ -1,10 +0,0 @@
namespace SrcMod.Shell.Interop.ObjectModels;
[StructLayout(LayoutKind.Sequential)]
internal struct SmallRect
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}

View File

@ -6,7 +6,6 @@ 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.Interop.ObjectModels;
global using SrcMod.Shell.Modules; 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;

View File

@ -1,665 +0,0 @@
namespace SrcMod.Shell.Modules.Valve;
[Module("vkv")]
public static class VkvModule
{
[Command("create")]
[CanCancel(false)]
public static void CreateVkv(string path)
{
if (File.Exists(path)) throw new($"File already exists at \"{path}\". Did you mean to run \"vkv edit\"?");
VkvNode parentNode = new VkvTreeNode()
{
{ "key", new VkvSingleNode("value") }
};
string parentNodeName = "tree";
VkvModifyWhole(ref parentNode, ref parentNodeName);
try
{
FileStream fs = new(path, FileMode.Create);
SerializeVkv.Serialize(fs, parentNode, parentNodeName);
}
catch (Exception ex)
{
#if DEBUG
throw;
#else
throw new($"Error serializing result to file: {ex.Message}");
#endif
}
}
[Command("edit")]
[CanCancel(false)]
public static void EditVkv(string path)
{
if (!File.Exists(path)) throw new($"No file exists at \"{path}\". Did you mean to run \"vkv create\"?");
VkvNode? parentNode;
string parentNodeName = string.Empty;
try
{
FileStream fs = new(path, FileMode.Open);
parentNode = SerializeVkv.Deserialize(fs, out parentNodeName);
if (parentNode is null) throw new("Deserialized VKV node is null.");
}
catch (Exception ex)
{
#if DEBUG
throw;
#else
throw new($"Error parsing file to Valve KeyValues format: {ex.Message}");
#endif
}
VkvModifyWhole(ref parentNode, ref parentNodeName);
try
{
FileStream fs = new(path, FileMode.Create);
SerializeVkv.Serialize(fs, parentNode, parentNodeName);
}
catch (Exception ex)
{
#if DEBUG
throw;
#else
throw new($"Error serializing result to file: {ex.Message}");
#endif
}
}
#region The VKV Modification System
private static void VkvModifyWhole(ref VkvNode rootNode, ref string rootNodeName)
{
// Generate reference context for passing to the modification methods.
VkvModifyContext context = new()
{
displayLines = VkvModifyGetLines(rootNode, rootNodeName, 0),
rootNode = rootNode,
rootNodeName = rootNodeName
};
// Make an initial printing of the vkv node.
VkvModifyPrintAll(ref context, false);
// Start modifying the root node.
VkvTreeNode? nullNode = null;
int nullInt = default;
VkvModifyNode(ref rootNode, ref rootNodeName, ref context, true, ref nullNode, ref nullInt);
// Done editing, let's reset the cursor position and exit the command.
Console.ResetColor();
Console.SetCursorPosition(0, context.startingCursor + context.displayLines.Count);
}
private static VkvModifyOption VkvModifyNode(ref VkvNode node, ref string nodeName,
ref VkvModifyContext context, bool isGlobal, ref VkvTreeNode? parentNode, ref int parentSubIndex)
{
#if DEBUG
string add = $" {nodeName}";
Console.Title += add;
#endif
int subIndex = -1; // Represents the index of the sub node currently being modified.
// If the variable is set to -1, it represents the title.
VkvSingleNode? single = node as VkvSingleNode;
VkvTreeNode? tree = node as VkvTreeNode;
VkvModifyOption? option = null;
VkvModifySelection selection = VkvModifySelection.Name;
while (true)
{
// Color the display white, wait for a key, then reset and handle the key press.
string line = context.displayLines[context.lineIndex];
Console.SetCursorPosition(0, context.startingCursor + context.lineIndex);
Console.Write(Whitify(line, selection));
Console.ResetColor();
if (!option.HasValue) option = Console.ReadKey(true).Key switch
{
ConsoleKey.DownArrow => VkvModifyOption.IncSubIndex,
ConsoleKey.UpArrow => VkvModifyOption.DecSubIndex,
ConsoleKey.RightArrow => VkvModifyOption.RShiftMode,
ConsoleKey.LeftArrow => VkvModifyOption.LShiftMode,
ConsoleKey.Enter => VkvModifyOption.Use,
ConsoleKey.Escape => VkvModifyOption.ExitAll,
_ => VkvModifyOption.Nothing
};
Console.CursorLeft = 0; // This is assuming the cursor hasn't moved, which it shouldn't.
Console.Write(line + new string(' ', Console.WindowWidth - line.Length));
// Now we handle the key press.
switch (option)
{
case VkvModifyOption.IncSubIndex:
if (tree is not null)
{
subIndex++;
if (subIndex == 0)
{
// We just shifted down from the title to the first sub node.
// We need to overlook the next line, '{'.
context.lineIndex += 2;
// Now we also need to start modification of the first sub node.
KeyValuePair<string, VkvNode>? subNode = tree[subIndex];
if (subNode is not null)
{
string subNodeKey = subNode.Value.Key;
VkvNode subNodeValue = subNode.Value.Value;
VkvModifyOption status =
VkvModifyNode(ref subNodeValue, ref subNodeKey, ref context, false, ref tree, ref subIndex);
// Update the parent node with our modified sub node.
tree![subIndex] = new(subNodeKey, subNodeValue);
// Set the next instruction.
option = status;
}
}
else if (subIndex == tree.SubNodeCount + 1)
{
// We're outside the maximum sub nodes. Let's increment the parent
// sub index and end this method.
// Incrementing the line index to overlook the next line, '}'
#if DEBUG
Console.Title = Console.Title[..^add.Length];
#endif
context.lineIndex++;
return VkvModifyOption.IncSubIndex;
}
else
{
// We're in a valid range. Let's just change the sub node we're
// focused on.
context.lineIndex++;
if (subIndex < tree.SubNodeCount)
{
// We are talking about an already existing node, so we also need
// to start modification of the first sub node.
KeyValuePair<string, VkvNode>? subNode = tree[subIndex];
if (subNode is not null)
{
string subNodeKey = subNode.Value.Key;
VkvNode subNodeValue = subNode.Value.Value;
VkvModifyOption status =
VkvModifyNode(ref subNodeValue, ref subNodeKey, ref context, false, ref tree, ref subIndex);
// Update the parent node with our modified sub node.
tree![subIndex] = new(subNodeKey, subNodeValue);
// Set the next instruction.
option = status;
}
}
else
{
// TODO: This is where we can decide to add sub nodes.
option = null;
selection = VkvModifySelection.CreateNew;
#if DEBUG
string secondAdd = " [CREATE NEW]";
add += secondAdd;
Console.Title += secondAdd;
#endif
}
}
}
else
{
// We aren't in a tree. We just change the parent sub index and
// end this method (and increment the line).
#if DEBUG
Console.Title = Console.Title[..^add.Length];
#endif
return VkvModifyOption.IncSubIndex;
}
break;
case VkvModifyOption.DecSubIndex:
// TODO: Implement when moving downward is complete.
// It's a little weird to not be able to move back up,
// I know, but it's gonna be weirder to implement, and
// I only want to do it once.
option = null;
break;
case VkvModifyOption.RShiftMode:
selection = selection switch
{
VkvModifySelection.Delete => VkvModifySelection.Name,
VkvModifySelection.Name => single is null ? selection : VkvModifySelection.Value,
_ => selection
};
option = null;
break;
case VkvModifyOption.LShiftMode:
selection = selection switch
{
VkvModifySelection.Name => isGlobal ? VkvModifySelection.Name : VkvModifySelection.Delete,
VkvModifySelection.Value => VkvModifySelection.Name,
_ => selection
};
option = null;
break;
case VkvModifyOption.Use:
switch (selection)
{
case VkvModifySelection.Delete:
string unrefNodeName = nodeName;
VkvNode unrefNode = node;
parentNode![parentSubIndex - 1] = null;
// Inefficient, yeah, but again, this is intended to
// be used by humans, not robots. Feel free to improve
// it if you want, but I probably won't.
List<string> newLines = VkvModifyGetLines(context.rootNode, context.rootNodeName, 0);
int endBuffer = context.displayLines.Count - newLines.Count;
context.displayLines = newLines;
VkvModifyPrintAll(ref context, true, true);
// TODO: Kinda works, but it's quite buggy. Fix later.
Console.SetCursorPosition(0, context.startingCursor + context.displayLines.Count);
for (int i = 0; i < endBuffer; i++) Console.WriteLine(new string(' ', Console.WindowWidth));
break;
case VkvModifySelection.Name:
VkvModifyRefactorName(ref nodeName, ref context);
break;
case VkvModifySelection.Value:
if (single is null)
{
option = null;
continue;
}
VkvModifyRefactorValue(ref single, ref context);
break;
case VkvModifySelection.CreateNew:
// TODO
break;
}
option = null;
break;
case VkvModifyOption.ExitAll: return VkvModifyOption.ExitAll;
default:
option = null;
continue;
}
}
}
private static void VkvModifyRefactorName(ref string nodeName, ref VkvModifyContext context)
{
string originalName = nodeName;
string edit = context.displayLines[context.lineIndex];
int firstQuote = edit.IndexOf('\"'),
secondQuote = edit[(firstQuote + 1)..].IndexOf('\"') + firstQuote + 1;
int displayIndex = secondQuote, nameIndex = nodeName.Length;
int additionalAnsiTakeoff = 0;
{
// I almost never do this ("this" being the brackets while
// already inside a method), but I also don't like
// keeping temporary variables around for no reason.
bool tempActive = false;
for (int i = 0; i < displayIndex; i++)
{
if (edit[i] == '\x1b') tempActive = true;
if (tempActive) additionalAnsiTakeoff++;
if (tempActive && edit[i] == 'm') tempActive = false;
}
}
while (true)
{
Console.CursorLeft = 0;
Console.Write(Whitify(edit, VkvModifySelection.Name, true));
Console.CursorLeft = displayIndex - additionalAnsiTakeoff;
Console.CursorVisible = true;
ConsoleKeyInfo key = Console.ReadKey(true);
Console.CursorVisible = false;
if (char.IsControl(key.KeyChar))
{
// TODO: Adding clipboard support might be cool (but also a pain).
bool end = false;
switch (key.Key)
{
case ConsoleKey.Backspace:
if (nameIndex > 0)
{
nodeName = nodeName.Remove(nameIndex - 1, 1);
edit = edit.Remove(displayIndex - 1, 1) + ' ';
displayIndex--;
nameIndex--;
}
break;
case ConsoleKey.Delete:
if (nameIndex < nodeName.Length)
{
nodeName = nodeName.Remove(nameIndex, 1);
edit = edit.Remove(displayIndex, 1) + ' ';
}
break;
case ConsoleKey.Escape:
nodeName = originalName;
edit = context.displayLines[context.lineIndex];
end = true;
break;
case ConsoleKey.Enter:
end = true;
break;
case ConsoleKey.LeftArrow:
if (nameIndex > 0)
{
displayIndex--;
nameIndex--;
}
break;
case ConsoleKey.RightArrow:
if (nameIndex < nodeName.Length)
{
displayIndex++;
nameIndex++;
}
break;
}
if (end) break;
}
else
{
nodeName = nodeName.Insert(nameIndex, key.KeyChar.ToString());
edit = edit.Insert(displayIndex, key.KeyChar.ToString()).TrimEnd();
displayIndex++;
nameIndex++;
}
}
context.displayLines[context.lineIndex] = edit.TrimEnd();
}
private static void VkvModifyRefactorValue(ref VkvSingleNode node, ref VkvModifyContext context)
{
string value = node.value?.ToString() ?? string.Empty,
edit = context.displayLines[context.lineIndex];
int firstQuote = edit.IndexOf('\"'),
secondQuote = edit[(firstQuote + 1)..].IndexOf('\"') + firstQuote + 1,
thirdQuote = edit[(secondQuote + 1)..].IndexOf('\"') + secondQuote + 1,
fourthQuote = edit[(thirdQuote + 1)..].IndexOf('\"') + thirdQuote + 1;
int displayIndex = fourthQuote, valueIndex = value.Length;
int additionalAnsiTakeoff = 0;
{
// See my opinions of brackets while already
// inside a method in the `VkvModifyRefactorName`
// method.
bool tempActive = false;
for (int i = 0; i < displayIndex; i++)
{
if (edit[i] == '\x1b') tempActive = true;
if (tempActive) additionalAnsiTakeoff++;
if (tempActive && edit[i] == 'm') tempActive = false;
}
}
while (true)
{
Console.CursorLeft = 0;
Console.Write(Whitify(edit, VkvModifySelection.Value, true));
Console.CursorLeft = displayIndex - additionalAnsiTakeoff;
Console.CursorVisible = true;
ConsoleKeyInfo key = Console.ReadKey(true);
Console.CursorVisible = false;
if (char.IsControl(key.KeyChar))
{
// TODO: Adding clipboard support might be cool (but also a pain).
bool end = false;
switch (key.Key)
{
case ConsoleKey.Backspace:
if (valueIndex > 0)
{
value = value.Remove(valueIndex - 1, 1);
edit = edit.Remove(displayIndex - 1, 1) + ' ';
displayIndex--;
valueIndex--;
}
break;
case ConsoleKey.Delete:
if (valueIndex < value.Length)
{
value = value.Remove(valueIndex, 1);
edit = edit.Remove(displayIndex, 1) + ' ';
}
break;
case ConsoleKey.Escape:
edit = context.displayLines[context.lineIndex];
end = true;
break;
case ConsoleKey.Enter:
node.value = value;
end = true;
break;
case ConsoleKey.LeftArrow:
if (valueIndex > 0)
{
displayIndex--;
valueIndex--;
}
break;
case ConsoleKey.RightArrow:
if (valueIndex < value.Length)
{
displayIndex++;
valueIndex++;
}
break;
}
if (end) break;
}
else
{
value = value.Insert(valueIndex, key.KeyChar.ToString());
edit = edit.Insert(displayIndex, key.KeyChar.ToString()).TrimEnd();
displayIndex++;
valueIndex++;
}
}
context.displayLines[context.lineIndex] = edit.TrimEnd();
}
private static void VkvModifyPrintAll(ref VkvModifyContext context, bool resetCursor, bool flushLine = false)
{
Int2 cursorPos = (Console.CursorLeft, Console.CursorTop);
Console.SetCursorPosition(0, context.startingCursor);
foreach (string line in context.displayLines)
{
Console.Write(line);
if (flushLine) Console.Write(new string(' ', Console.WindowWidth - line.Length));
Console.WriteLine();
Console.ResetColor();
}
if (resetCursor) Console.SetCursorPosition(cursorPos.x, cursorPos.y);
}
private static List<string> VkvModifyGetLines(VkvNode node, string nodeName, int indent)
{
int spaceCount = indent * 4,
nextSpaceCount = (indent + 1) * 4;
List<string> lines = new();
if (node is VkvSingleNode single) lines.Add(new string(' ', spaceCount) + $"\x1b[33m\"{nodeName}\"" +
$" \x1b[32m\"{single.value}\"");
else if (node is VkvTreeNode tree)
{
lines.Add(new string(' ', spaceCount) + $"\x1b[94m\"{nodeName}\"");
lines.Add(new string(' ', spaceCount) + "{");
foreach (KeyValuePair<string, VkvNode> pair in tree)
{
lines.AddRange(VkvModifyGetLines(pair.Value, pair.Key, indent + 1));
}
lines.Add(new string(' ', nextSpaceCount) + "\x1b[35m...");
lines.Add(new string(' ', spaceCount) + "}");
}
else lines.Add(new string(' ', spaceCount) + "\x1b[31mError");
return lines;
}
private static string Whitify(string content, VkvModifySelection selection, bool blink = false)
{
StringBuilder result = new();
// This is definitely optimizable, but I don't feel like doing that yet.
// Maybe in the future.
// For future reference, when (if) this is optimized, I am doing stuff like this in this
// method along with the name and value refactoring methods.
int firstQuote = content.IndexOf('\"'),
secondQuote = content[(firstQuote + 1)..].IndexOf('\"') + firstQuote + 1,
thirdQuote = content[(secondQuote + 1)..].IndexOf('\"') + secondQuote + 1,
fourthQuote = content[(thirdQuote + 1)..].IndexOf('\"') + thirdQuote + 1;
int startChar = 0;
while (char.IsWhiteSpace(content[startChar])) startChar++;
int endChar = content.Length - 1;
while (char.IsWhiteSpace(content[endChar])) endChar--;
switch (selection)
{
case VkvModifySelection.Name:
if (firstQuote < 0 || secondQuote < 0) return content;
result.Append(content[..firstQuote]);
if (blink)
{
result.Append("\"\x1b[5m");
result.Append(content[(firstQuote + 1)..secondQuote]);
result.Append("\x1b[25m\"");
}
else
{
result.Append("\x1b[107m");
result.Append(content[firstQuote..(secondQuote + 1)]);
result.Append("\x1b[0m");
}
result.Append(content[(secondQuote + 1)..]);
break;
case VkvModifySelection.Value:
if (thirdQuote < 0 || fourthQuote < 0) return content;
result.Append(content[..thirdQuote]);
if (blink)
{
result.Append("\"\x1b[5m");
result.Append(content[(thirdQuote + 1)..fourthQuote]);
result.Append("\x1b[25m\"");
}
else
{
result.Append("\x1b[107m");
result.Append(content[thirdQuote..(fourthQuote + 1)]);
result.Append("\x1b[0m");
}
result.Append(content[(fourthQuote + 1)..]);
break;
case VkvModifySelection.Delete:
const string addDelete = "[Delete]";
result.Append($"\x1b[107m\x1b[31m{addDelete}\x1b[0m ");
if (addDelete.Length + 1 > startChar) result.Append(content[startChar..]);
else result.Append(content[(addDelete.Length + 1)..]);
break;
case VkvModifySelection.CreateNew:
result.Append(content[..startChar]);
result.Append("\x1b[107m");
result.Append(content[startChar..(endChar + 1)]);
result.Append("\x1b[0m");
result.Append(content[(endChar + 1)..]);
break;
}
return result.ToString();
}
private class VkvModifyContext
{
public required List<string> displayLines;
public int lineIndex;
public required VkvNode rootNode;
public required string rootNodeName;
public readonly int startingCursor;
public VkvModifyContext()
{
lineIndex = 0;
startingCursor = Console.CursorTop;
}
}
private enum VkvModifyOption
{
Nothing,
IncSubIndex,
DecSubIndex,
RShiftMode,
LShiftMode,
Use,
ExitAll
}
private enum VkvModifySelection
{
Delete,
Name,
Value,
CreateNew
}
#endregion
}

View File

@ -4,7 +4,7 @@ public class Shell
{ {
public const string Author = "That_One_Nerd"; public const string Author = "That_One_Nerd";
public const string Name = "SrcMod"; public const string Name = "SrcMod";
public const string Version = "Beta 0.6.0"; public const string Version = "Beta 0.5.0";
public bool HasAnyDisplayableError => HasDisplayableError || Config.HasDisplayableError; public bool HasAnyDisplayableError => HasDisplayableError || Config.HasDisplayableError;
public bool HasAnyDisplayableWarning => HasDisplayableWarning || Config.HasDisplayableWarning; public bool HasAnyDisplayableWarning => HasDisplayableWarning || Config.HasDisplayableWarning;

View File

@ -19,19 +19,16 @@ public static class VkvConvert
#region DeserializeNode #region DeserializeNode
public static VkvNode? DeserializeNode(StreamReader reader) => public static VkvNode? DeserializeNode(StreamReader reader) =>
DeserializeNode(reader, VkvOptions.Default, out _); DeserializeNode(reader, VkvOptions.Default);
public static VkvNode? DeserializeNode(StreamReader reader, VkvOptions options) => public static VkvNode? DeserializeNode(StreamReader reader, VkvOptions options)
DeserializeNode(reader, options, out _);
public static VkvNode? DeserializeNode(StreamReader reader, VkvOptions options, out string name)
{ {
try try
{ {
return DeserializeNode(reader, options, out name, null); return DeserializeNode(reader, options, out _, null);
} }
catch catch
{ {
if (!options.noExceptions) throw; if (!options.noExceptions) throw;
name = string.Empty;
return null; return null;
} }
} }
@ -66,9 +63,7 @@ public static class VkvConvert
string? current; string? current;
while ((current = reader.ReadLine()?.Trim()) is not null) while ((current = reader.ReadLine()?.Trim()) is not null)
{ {
if (string.IsNullOrWhiteSpace(current)) continue; if (current == "}") break;
else if (current == "}") break;
VkvNode? output = DeserializeNode(reader, options, out string subName, current); VkvNode? output = DeserializeNode(reader, options, out string subName, current);
tree[subName] = output; tree[subName] = output;
} }

View File

@ -29,23 +29,6 @@ public class VkvSerializer
if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin); if (!p_options.closeWhenFinished && p_options.resetStreamPosition) stream.Seek(pos, SeekOrigin.Begin);
} }
} }
public VkvNode? Deserialize(Stream stream, out string name)
{
long pos = stream.Position;
StreamReader reader = new(stream, leaveOpen: !p_options.closeWhenFinished);
try
{
VkvNode? result = VkvConvert.DeserializeNode(reader, p_options, out name);
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) public T? Deserialize<T>(Stream stream)
{ {
VkvNode? result = Deserialize(stream); VkvNode? result = Deserialize(stream);

View File

@ -1,166 +1,45 @@
namespace Valve.Vkv; namespace Valve.Vkv;
public class VkvTreeNode : VkvNode, IEnumerable<KeyValuePair<string, VkvNode>> public class VkvTreeNode : VkvNode, IEnumerable<KeyValuePair<string, VkvNode?>>
{ {
public int SubNodeCount => p_subNodes.Count; public int SubNodeCount => p_subNodes.Count;
// These should never get out of sync, or bad things will happen. private readonly Dictionary<string, VkvNode?> p_subNodes;
private readonly List<string> p_subNodeKeys;
private readonly List<VkvNode> p_subNodes;
public VkvTreeNode(Dictionary<string, VkvNode?>? subNodes = null) : base() public VkvTreeNode(Dictionary<string, VkvNode?>? subNodes = null) : base()
{ {
p_subNodeKeys = new(); p_subNodes = subNodes ?? new();
p_subNodes = new();
if (subNodes is not null)
{
for (int i = 0; i < subNodes.Count; i++)
{
string key = subNodes.Keys.ElementAt(i);
VkvNode? value = subNodes.Values.ElementAt(i);
if (value is not null)
{
p_subNodeKeys.Add(key);
p_subNodes.Add(value);
}
}
}
} }
public VkvNode? this[string key] public VkvNode? this[string key]
{ {
get get
{ {
int index = p_subNodeKeys.IndexOf(key); if (p_subNodes.TryGetValue(key, out VkvNode? value)) return value;
else return null;
if (index == -1) return null;
else return p_subNodes[index];
} }
set set
{ {
int index = p_subNodeKeys.IndexOf(key); if (p_subNodes.ContainsKey(key)) p_subNodes[key] = value;
else p_subNodes.Add(key, value);
if (index == -1)
{
if (value is null) return;
p_subNodeKeys.Add(key);
p_subNodes.Add(value);
}
else
{
if (value is null)
{
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
else p_subNodes[index] = value;
} }
} }
} public VkvNode? this[int index]
public KeyValuePair<string, VkvNode>? this[int index]
{ {
get get
{ {
if (index >= SubNodeCount || index < 0) return null; if (p_subNodes.Count >= index || index < 0) return null;
return new(p_subNodeKeys[index], p_subNodes[index]); return p_subNodes.Values.ElementAt(index);
} }
set set
{ {
if (index >= SubNodeCount || index < 0) throw new IndexOutOfRangeException(); if (p_subNodes.Count >= index || index < 0) throw new IndexOutOfRangeException();
p_subNodes[p_subNodes.Keys.ElementAt(index)] = value;
if (value is null)
{
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
else
{
p_subNodeKeys[index] = value.Value.Key;
p_subNodes[index] = value.Value.Value;
}
}
}
public KeyValuePair<string, VkvNode>? this[Func<int, KeyValuePair<string, VkvNode>, bool> predicate]
{
get
{
for (int i = 0; i < SubNodeCount; i++)
{
KeyValuePair<string, VkvNode> pair = new(p_subNodeKeys[i], p_subNodes[i]);
if (predicate(i, pair)) return pair;
}
return null;
}
set
{
for (int i = 0; i < SubNodeCount; i++)
{
KeyValuePair<string, VkvNode> pair = new(p_subNodeKeys[i], p_subNodes[i]);
if (predicate(i, pair))
{
if (value.HasValue)
{
p_subNodeKeys[i] = value.Value.Key;
p_subNodes[i] = value.Value.Value;
}
else
{
p_subNodeKeys.RemoveAt(i);
p_subNodes.RemoveAt(i);
i--;
}
}
}
} }
} }
public void Add(string key, VkvNode value) => this[key] = value; public void Add(string key, VkvNode? value) => this[key] = value;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IEnumerator<KeyValuePair<string, VkvNode>> GetEnumerator() public IEnumerator<KeyValuePair<string, VkvNode?>> GetEnumerator() => p_subNodes.GetEnumerator();
{
for (int i = 0; i < SubNodeCount; i++)
{
yield return new(p_subNodeKeys[i], p_subNodes[i]);
}
}
public void Remove(string key)
{
int index = p_subNodeKeys.IndexOf(key);
if (index == -1) return;
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
public void Remove(VkvNode value)
{
int index = p_subNodes.IndexOf(value);
if (index == -1) return;
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
public void RemoveAll(string key)
{
int index;
while ((index = p_subNodeKeys.IndexOf(key)) != -1)
{
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
}
public void RemoveAll(VkvNode value)
{
int index;
while ((index = p_subNodes.IndexOf(value)) != -1)
{
p_subNodeKeys.RemoveAt(index);
p_subNodes.RemoveAt(index);
}
}
} }