diff --git a/Sledge.Formats.Bsp.Tests/Sledge.Formats.Bsp.Tests.csproj b/Sledge.Formats.Bsp.Tests/Sledge.Formats.Bsp.Tests.csproj
index cd3a58a..d58d8da 100644
--- a/Sledge.Formats.Bsp.Tests/Sledge.Formats.Bsp.Tests.csproj
+++ b/Sledge.Formats.Bsp.Tests/Sledge.Formats.Bsp.Tests.csproj
@@ -8,9 +8,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.Configuration.Tests/MSTestSettings.cs b/Sledge.Formats.Configuration.Tests/MSTestSettings.cs
new file mode 100644
index 0000000..aaf278c
--- /dev/null
+++ b/Sledge.Formats.Configuration.Tests/MSTestSettings.cs
@@ -0,0 +1 @@
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
diff --git a/Sledge.Formats.Configuration.Tests/RegistryUtil.cs b/Sledge.Formats.Configuration.Tests/RegistryUtil.cs
new file mode 100644
index 0000000..fda99e3
--- /dev/null
+++ b/Sledge.Formats.Configuration.Tests/RegistryUtil.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics;
+using Microsoft.Win32;
+using Sledge.Formats.Configuration.Registry;
+
+namespace Sledge.Formats.Configuration.Tests;
+
+#pragma warning disable CA1416 // platform warning
+public static class RegistryUtil
+{
+ ///
+ /// Very rough .reg file parser
+ ///
+ public static InMemoryRegistry CreateRegistryFromRegString(string regString)
+ {
+ var reg = new InMemoryRegistry();
+
+ var spl = regString.Split('\n').Select(x => x.Trim()).ToList();
+
+ if (spl[0] != "Windows Registry Editor Version 5.00") throw new NotSupportedException($"Unknown registry type: {spl[0]}");
+
+ var currentHive = RegistryHive.CurrentUser;
+ string? currentKey = null;
+
+ foreach (var line in spl.Skip(1))
+ {
+ if (string.IsNullOrWhiteSpace(line)) continue;
+ if (line[0] == '[')
+ {
+ var keyPath = line.Trim('[', ']').Split('\\');
+ var hiveStr = keyPath[0][4..].Replace("_", "");
+ currentHive = Enum.Parse(hiveStr, true);
+ currentKey = string.Join('\\', keyPath.Skip(1));
+ }
+ else if (currentKey != null)
+ {
+ var kv = line.Split('=', 2);
+ var key = kv[0].Trim('"');
+ var val = kv[1];
+ if (val.StartsWith('"'))
+ {
+ reg.OpenBaseKey(currentHive, RegistryView.Default).CreateSubKey(currentKey).SetValue(key, val.Trim('"').Replace(@"\\", @"\"), RegistryValueKind.String);
+ }
+ else if (val.StartsWith("dword:"))
+ {
+ reg.OpenBaseKey(currentHive, RegistryView.Default).CreateSubKey(currentKey).SetValue(key, Convert.ToInt32(val[6..], 16), RegistryValueKind.DWord);
+ }
+ else
+ {
+ throw new NotSupportedException($"Unknown value type: {val}");
+ }
+ }
+ }
+
+ return reg;
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration.Tests/Sledge.Formats.Configuration.Tests.csproj b/Sledge.Formats.Configuration.Tests/Sledge.Formats.Configuration.Tests.csproj
new file mode 100644
index 0000000..378f019
--- /dev/null
+++ b/Sledge.Formats.Configuration.Tests/Sledge.Formats.Configuration.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ latest
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Configuration.Tests/TestWorldcraftConfiguration.cs b/Sledge.Formats.Configuration.Tests/TestWorldcraftConfiguration.cs
new file mode 100644
index 0000000..2a49489
--- /dev/null
+++ b/Sledge.Formats.Configuration.Tests/TestWorldcraftConfiguration.cs
@@ -0,0 +1,111 @@
+using Microsoft.Win32;
+using Sledge.Formats.Configuration.Worldcraft;
+
+namespace Sledge.Formats.Configuration.Tests;
+
+#pragma warning disable CA1416
+
+[TestClass]
+public sealed class TestWorldcraftConfiguration
+{
+ ///
+ /// If you don't have worldcraft settings in your registry, this test will fail
+ ///
+ [TestMethod]
+ public void TestLoadSettingsFromLocalComputer()
+ {
+ var config = WorldcraftConfiguration.LoadFromRegistry(WorldcraftConfigurationLoadSettings.Default);
+ Console.WriteLine("Undo levels: " + config.General.UndoLevels);
+ Console.WriteLine("Textures: " + string.Join("; ", config.TextureDirectories));
+ }
+
+ [TestMethod]
+ public void TestLoadSettings()
+ {
+ var reg = RegistryUtil.CreateRegistryFromRegString(Worldcraft33RegString);
+ var config = WorldcraftConfiguration.LoadFromRegistry(new WorldcraftConfigurationLoadSettings
+ {
+ LoadGameConfigurations = false,
+ AutodetectRegistryLocation = false,
+ RegistryLocation = reg.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default).OpenSubKey(@"Software\Valve\Worldcraft")
+ });
+
+ Assert.AreEqual(@"C:\Users\WDAGUtilityAccount\Desktop\Worldcraft 3.3", config.General.InstallDirectory, StringComparer.InvariantCultureIgnoreCase);
+ Assert.AreEqual(true, config.General.UseIndependentWindowConfigurations);
+ Assert.AreEqual(false, config.General.LoadDefaultWindowPositionsWithMaps);
+ Assert.AreEqual(0x32, config.General.UndoLevels);
+ Assert.AreEqual(true, config.General.AllowGroupingWhileIgnoreGroupsChecked);
+ Assert.AreEqual(false, config.General.StretchArchesToFitOriginalBoundingRectangle);
+
+ CollectionAssert.AreEqual(new[] { @"c:\users\wdagutilityaccount\desktop\zhlt.wad" }, config.TextureDirectories, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ private const string Worldcraft33RegString = """
+ Windows Registry Editor Version 5.00
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve]
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft]
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\2D Views]
+ "Crosshairs"=dword:00000001
+ "GroupCarve"=dword:00000001
+ "Scrollbars"=dword:00000000
+ "RotateConstrain"=dword:00000001
+ "Draw Vertices"=dword:00000000
+ "Default Grid"=dword:00000020
+ "WhiteOnBlack"=dword:00000000
+ "GridHigh10"=dword:00000000
+ "GridIntensity"=dword:00000042
+ "HideSmallGrid"=dword:00000000
+ "Nudge"=dword:00000001
+ "OrientPrimitives"=dword:00000001
+ "AutoSelect"=dword:00000001
+ "SelectByHandles"=dword:00000001
+ "GridHighSpec"=dword:00000008
+ "KeepCloneGroup"=dword:00000000
+ "Gridhigh64"=dword:00000001
+ "GridDots"=dword:00000000
+ "Centeroncamera"=dword:00000001
+ "Usegroupcolors"=dword:00000000
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\3D Views]
+ "Hardware"=dword:00000000
+ "Reverse Y"=dword:00000001
+ "BackPlane"=dword:00001ef9
+ "UseMouseLook"=dword:00000000
+ "ModelDistance"=dword:00000190
+ "AnimateModels"=dword:00000001
+ "ForwardSpeedMax"=dword:00000b0d
+ "TimeToMaxSpeed"=dword:00000783
+ "FilterTextures"=dword:00000000
+ "ReverseSelection"=dword:00000001
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\Configured]
+ "Installed"=dword:6767bfdf
+ "Configured"=dword:00000002
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\Custom2DColors]
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\General]
+ "Directory"="C:\\Users\\WDAGUtilityAccount\\Desktop\\Worldcraft 3.3"
+ "TextureFileCount"=dword:00000001
+ "TextureFile0"="c:\\users\\wdagutilityaccount\\desktop\\zhlt.wad"
+ "Brightness"=dword:0000000a
+ "Undo Levels"=dword:00000032
+ "Locking Textures"=dword:00000000
+ "Texture Alignment"=dword:00000000
+ "Independent Windows"=dword:00000001
+ "Load Default Positions"=dword:00000000
+ "GroupWhileIgnore"=dword:00000001
+ "StretchArches"=dword:00000000
+ "NewBars"=dword:00000001
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\Recent File List]
+ "File1"="C:\\Users\\WDAGUtilityAccount\\Desktop\\Worldcraft 3.3\\123"
+
+ [HKEY_CURRENT_USER\SOFTWARE\Valve\Worldcraft\Settings]
+
+
+ """;
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Registry/IRegistry.cs b/Sledge.Formats.Configuration/Registry/IRegistry.cs
new file mode 100644
index 0000000..1a34e0d
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/IRegistry.cs
@@ -0,0 +1,21 @@
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// An interface for any type which can provide access to the registry. In most circumstances, the implementation
+ /// will be . This service exists to support effective testing of registry access.
+ ///
+ public interface IRegistry
+ {
+ /// Opens a new that represents the requested key on the local machine with the specified view.
+ /// The requested registry key.
+ /// The HKEY to open.
+ /// The registry view to use.
+ ///
+ /// or is invalid.
+ /// The user does not have the necessary registry rights.
+ /// The user does not have the permissions required to perform this action.
+ IRegistryKey OpenBaseKey(RegistryHive hKey, RegistryView view);
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/IRegistryKey.cs b/Sledge.Formats.Configuration/Registry/IRegistryKey.cs
new file mode 100644
index 0000000..04e74b9
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/IRegistryKey.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Security.AccessControl;
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// Interface which represents a key-level node in the Windows registry. This class is a registry encapsulation, primarily used
+ /// to allow us to swap out for the for testing.
+ ///
+ public interface IRegistryKey : IDisposable
+ {
+ /// Retrieves an array of strings that contains all the subkey names.
+ /// An array of strings that contains the names of the subkeys for the current key.
+ /// The user does not have the permissions required to read from the key.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ /// A system error occurred, for example the current key has been deleted.
+ ///
+ ///
+ ///
+ string[] GetSubKeyNames();
+
+ /// Retrieves a subkey as read-only.
+ /// The subkey requested, or null if the operation failed.
+ /// The name or path of the subkey to open as read-only.
+ ///
+ /// is null
+ /// The is closed (closed keys cannot be accessed).
+ /// The user does not have the permissions required to read the registry key.
+ ///
+ ///
+ ///
+ IRegistryKey OpenSubKey(string name);
+
+ /// Retrieves a specified subkey, and specifies whether write access is to be applied to the key.
+ /// The subkey requested, or null if the operation failed.
+ /// Name or path of the subkey to open.
+ /// Set to true if you need write access to the key.
+ ///
+ /// is null.
+ /// The is closed (closed keys cannot be accessed).
+ /// The user does not have the permissions required to access the registry key in the specified mode.
+ ///
+ ///
+ ///
+ ///
+ IRegistryKey OpenSubKey(string name, bool writable);
+
+ /// Retrieves the specified subkey for read or read/write access.
+ /// The subkey requested, or null if the operation failed.
+ /// The name or path of the subkey to create or open.
+ /// One of the enumeration values that specifies whether the key is opened for read or read/write access.
+ ///
+ /// is null
+ ///
+ /// contains an invalid value.
+ /// The is closed (closed keys cannot be accessed).
+ /// The user does not have the permissions required to read the registry key.
+ IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck);
+
+ /// Retrieves the specified subkey for read or read/write access, requesting the specified access rights.
+ /// The subkey requested, or null if the operation failed.
+ /// The name or path of the subkey to create or open.
+ /// One of the enumeration values that specifies whether the key is opened for read or read/write access.
+ /// A bitwise combination of enumeration values that specifies the desired security access.
+ ///
+ /// is null
+ ///
+ /// contains an invalid value.
+ /// The is closed (closed keys cannot be accessed).
+ ///
+ /// includes invalid registry rights values.-or-The user does not have the requested permissions.
+ IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck, RegistryRights rights);
+
+ /// Retrieves the value associated with the specified name. Returns null if the name/value pair does not exist in the registry.
+ /// The value associated with , or null if is not found.
+ /// The name of the value to retrieve. This string is not case-sensitive.
+ /// The user does not have the permissions required to read from the registry key.
+ /// The that contains the specified value is closed (closed keys cannot be accessed).
+ /// The that contains the specified value has been marked for deletion.
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ object GetValue(string name);
+
+ /// Retrieves the value associated with the specified name. If the name is not found, returns the default value that you provide.
+ /// The value associated with , with any embedded environment variables left unexpanded, or if is not found.
+ /// The name of the value to retrieve. This string is not case-sensitive.
+ /// The value to return if does not exist.
+ /// The user does not have the permissions required to read from the registry key.
+ /// The that contains the specified value is closed (closed keys cannot be accessed).
+ /// The that contains the specified value has been marked for deletion.
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ object GetValue(string name, object defaultValue);
+
+ /// Retrieves the value associated with the specified name and retrieval options. If the name is not found, returns the default value that you provide.
+ /// The value associated with , processed according to the specified , or if is not found.
+ /// The name of the value to retrieve. This string is not case-sensitive.
+ /// The value to return if does not exist.
+ /// One of the enumeration values that specifies optional processing of the retrieved value.
+ /// The user does not have the permissions required to read from the registry key.
+ /// The that contains the specified value is closed (closed keys cannot be accessed).
+ /// The that contains the specified value has been marked for deletion.
+ ///
+ /// is not a valid value; for example, an invalid value is cast to .
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ object GetValue(string name, object defaultValue, RegistryValueOptions options);
+
+ /// Creates a new subkey or opens an existing subkey for write access.
+ /// The newly created subkey, or null if the operation failed. If a zero-length string is specified for , the current object is returned.
+ /// The name or path of the subkey to create or open. This string is not case-sensitive.
+ ///
+ /// is null.
+ /// The user does not have the permissions required to create or open the registry key.
+ /// The on which this method is being invoked is closed (closed keys cannot be accessed).
+ /// The cannot be written to; for example, it was not opened as a writable key , or the user does not have the necessary access rights.
+ /// The nesting level exceeds 510.-or-A system error occurred, such as deletion of the key, or an attempt to create a key in the root.
+ ///
+ ///
+ ///
+ ///
+ IRegistryKey CreateSubKey(string subkey);
+
+ /// Creates a new subkey or opens an existing subkey for write access, using the specified permission check option.
+ /// The newly created subkey, or null if the operation failed. If a zero-length string is specified for , the current object is returned.
+ /// The name or path of the subkey to create or open. This string is not case-sensitive.
+ /// One of the enumeration values that specifies whether the key is opened for read or read/write access.
+ ///
+ /// is null.
+ /// The user does not have the permissions required to create or open the registry key.
+ ///
+ /// contains an invalid value.
+ /// The on which this method is being invoked is closed (closed keys cannot be accessed).
+ /// The cannot be written to; for example, it was not opened as a writable key, or the user does not have the necessary access rights.
+ /// The nesting level exceeds 510.-or-A system error occurred, such as deletion of the key, or an attempt to create a key in the root.
+ IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck);
+
+ /// Creates a new subkey or opens an existing subkey for write access, using the specified permission check option and registry security.
+ /// The newly created subkey, or null if the operation failed. If a zero-length string is specified for , the current object is returned.
+ /// The name or path of the subkey to create or open. This string is not case-sensitive.
+ /// One of the enumeration values that specifies whether the key is opened for read or read/write access.
+ /// The access control security for the new key.
+ ///
+ /// is null.
+ /// The user does not have the permissions required to create or open the registry key.
+ ///
+ /// contains an invalid value.
+ /// The on which this method is being invoked is closed (closed keys cannot be accessed).
+ /// The current cannot be written to; for example, it was not opened as a writable key, or the user does not have the necessary access rights.
+ /// The nesting level exceeds 510.-or-A system error occurred, such as deletion of the key, or an attempt to create a key in the root.
+ IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck,
+ RegistrySecurity registrySecurity);
+
+ /// Sets the specified name/value pair.
+ /// The name of the value to store.
+ /// The data to be stored.
+ ///
+ /// is null.
+ ///
+ /// is an unsupported data type.
+ /// The that contains the specified value is closed (closed keys cannot be accessed).
+ /// The is read-only, and cannot be written to; for example, the key has not been opened with write access. -or-The object represents a root-level node, and the operating system is Windows Millennium Edition or Windows 98.
+ /// The user does not have the permissions required to create or modify registry keys.
+ /// The object represents a root-level node, and the operating system is Windows 2000, Windows XP, or Windows Server 2003.
+ ///
+ ///
+ ///
+ ///
+ void SetValue(string name, object value);
+
+ /// Sets the value of a name/value pair in the registry key, using the specified registry data type.
+ /// The name of the value to be stored.
+ /// The data to be stored.
+ /// The registry data type to use when storing the data.
+ ///
+ /// is null.
+ /// The type of did not match the registry data type specified by , therefore the data could not be converted properly.
+ /// The that contains the specified value is closed (closed keys cannot be accessed).
+ /// The is read-only, and cannot be written to; for example, the key has not been opened with write access.-or-The object represents a root-level node, and the operating system is Windows Millennium Edition or Windows 98.
+ /// The user does not have the permissions required to create or modify registry keys.
+ /// The object represents a root-level node, and the operating system is Windows 2000, Windows XP, or Windows Server 2003.
+ ///
+ ///
+ ///
+ ///
+ void SetValue(string name, object value, RegistryValueKind valueKind);
+
+ /// Deletes a subkey and any child subkeys recursively.
+ /// The subkey to delete. This string is not case-sensitive.
+ ///
+ /// is null.
+ /// Deletion of a root hive is attempted.-or- does not specify a valid registry subkey.
+ /// An I/O error has occurred.
+ /// The user does not have the permissions required to delete the key.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ ///
+ void DeleteSubKeyTree(string subkey);
+
+ /// Deletes the specified subkey and any child subkeys recursively, and specifies whether an exception is raised if the subkey is not found.
+ /// The name of the subkey to delete. This string is not case-sensitive.
+ /// Indicates whether an exception should be raised if the specified subkey cannot be found. If this argument is true and the specified subkey does not exist, an exception is raised. If this argument is false and the specified subkey does not exist, no action is taken.
+ /// An attempt was made to delete the root hive of the tree.-or- does not specify a valid registry subkey, and is true.
+ ///
+ /// is null.
+ /// The is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ /// The user does not have the permissions required to delete the key.
+ void DeleteSubKeyTree(string subkey, bool throwOnMissingSubKey);
+
+ /// Retrieves an array of strings that contains all the value names associated with this key.
+ /// An array of strings that contains the value names for the current key.
+ /// The user does not have the permissions required to read from the registry key.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ /// A system error occurred; for example, the current key has been deleted.
+ ///
+ ///
+ ///
+ string[] GetValueNames();
+
+ /// Deletes the specified value from this key.
+ /// The name of the value to delete.
+ ///
+ /// is not a valid reference to a value.
+ /// The user does not have the permissions required to delete the value.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The being manipulated is read-only.
+ ///
+ ///
+ ///
+ ///
+ void DeleteValue(string name);
+
+ /// Deletes the specified value from this key, and specifies whether an exception is raised if the value is not found.
+ /// The name of the value to delete.
+ /// Indicates whether an exception should be raised if the specified value cannot be found. If this argument is true and the specified value does not exist, an exception is raised. If this argument is false and the specified value does not exist, no action is taken.
+ ///
+ /// is not a valid reference to a value and is true. -or- is null.
+ /// The user does not have the permissions required to delete the value.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The being manipulated is read-only.
+ ///
+ ///
+ ///
+ ///
+ void DeleteValue(string name, bool throwOnMissingValue);
+
+ /// Deletes the specified subkey.
+ /// The name of the subkey to delete. This string is not case-sensitive.
+ /// The has child subkeys
+ /// The parameter does not specify a valid registry key
+ ///
+ /// is null
+ /// The user does not have the permissions required to delete the key.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ ///
+ void DeleteSubKey(string subkey);
+
+ /// Deletes the specified subkey, and specifies whether an exception is raised if the subkey is not found.
+ /// The name of the subkey to delete. This string is not case-sensitive.
+ /// Indicates whether an exception should be raised if the specified subkey cannot be found. If this argument is true and the specified subkey does not exist, an exception is raised. If this argument is false and the specified subkey does not exist, no action is taken.
+ ///
+ /// has child subkeys.
+ ///
+ /// does not specify a valid registry key, and is true.
+ ///
+ /// is null.
+ /// The user does not have the permissions required to delete the key.
+ /// The being manipulated is closed (closed keys cannot be accessed).
+ /// The user does not have the necessary registry rights.
+ ///
+ ///
+ ///
+ ///
+ void DeleteSubKey(string subkey, bool throwOnMissingSubKey);
+
+ /// Retrieves the name of the key.
+ /// The absolute (qualified) name of the key.
+ /// The is closed (closed keys cannot be accessed).
+ string Name { get; }
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/InMemoryRegistry.cs b/Sledge.Formats.Configuration/Registry/InMemoryRegistry.cs
new file mode 100644
index 0000000..a747c42
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/InMemoryRegistry.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// The In-Memory registry implements with a simple in-memory structure.
+ /// It is designed to support testing scenarios.
+ ///
+ /// Note: A better string format for testing might be the *.reg file format:
+ /// https://support.microsoft.com/en-us/help/310516/how-to-add-modify-or-delete-registry-subkeys-and-values-by-using-a-reg
+ ///
+ ///
+ public class InMemoryRegistry : IRegistry
+ {
+ private readonly Dictionary, InMemoryRegistryKey> _rootKeys = new Dictionary, InMemoryRegistryKey>();
+
+ private const int IndentSpaces = 2;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InMemoryRegistry()
+ {
+ var hivesAndNames = new[]
+ {
+ Tuple.Create(RegistryHive.CurrentUser, "HKEY_CURRENT_USER"),
+ Tuple.Create(RegistryHive.LocalMachine, "HKEY_LOCAL_MACHINE"),
+ Tuple.Create(RegistryHive.ClassesRoot, "HKEY_CLASSES_ROOT"),
+ Tuple.Create(RegistryHive.Users, "HKEY_USERS"),
+ Tuple.Create(RegistryHive.PerformanceData, "HKEY_PERFORMANCE_DATA"),
+ Tuple.Create(RegistryHive.CurrentConfig, "HKEY_CURRENT_CONFIG")
+ };
+ foreach (var hn in hivesAndNames)
+ {
+ foreach (var view in Enum.GetValues(typeof(RegistryView)).OfType())
+ {
+ _rootKeys.Add(Tuple.Create(view, hn.Item1), new InMemoryRegistryKey(view, hn.Item2));
+ }
+ }
+ }
+
+ ///
+ public IRegistryKey OpenBaseKey(RegistryHive hKey, RegistryView view)
+ {
+ // Find and return the root key for the given view.
+ var rootKey = _rootKeys.Where(kv => kv.Key.Item1 == view && kv.Key.Item2 == hKey).Select(kvp => kvp.Value).FirstOrDefault();
+ if (rootKey == null) throw new InvalidOperationException($"Cannot find {view} root key for hive '{hKey}'");
+ return rootKey;
+ }
+
+ ///
+ /// Adds the given structure to the registry view. Generally used for testing only.
+ ///
+ /// The registry view.
+ /// The structure, which matches the MSDN documentation for shell extensions.
+ /// Thrown if the structure is malformed.
+ public void AddStructure(RegistryView registryView, string structure)
+ {
+ // Helper function to get the number of spaces which start a line.
+ var initialSpaceRex = new Regex(@"^([ ]+)[^ ]");
+ int Spaces(string line)
+ {
+ var m = initialSpaceRex.Match(line);
+ return m.Success ? m.Groups[1].Length : 0;
+ }
+
+ // Loop through the lines, building a stack of keys which we set.
+ var keyStack = new Stack();
+ var lineNum = 0;
+ foreach (var line in structure.Split(new[] { Environment.NewLine }, StringSplitOptions.None))
+ {
+ lineNum++;
+
+ // Skip empty lines.
+ if (string.IsNullOrEmpty(line)) continue;
+
+ // Get the depth of the line, which is indented by sets of spaces.
+ var spaces = Spaces(line);
+ if ((spaces % IndentSpaces) != 0) throw new InvalidOperationException($@"Line {lineNum}: Invalid indentation. Line starts with {spaces} spaces. Lines should start with a number of spaces which is a multiple of {IndentSpaces}.");
+ var depth = spaces / IndentSpaces;
+
+ // Pop the stack if we need to.
+ while (depth < keyStack.Count) keyStack.Pop();
+
+ // If we have zero spaces, we're loading a hive.
+ if (depth == 0)
+ {
+ // Load the hive.
+ var hive = _rootKeys.Where(kv => kv.Key.Item1 == registryView && kv.Value.Name == line).Select(kvp => kvp.Value).FirstOrDefault();
+ if (hive == null) throw new InvalidOperationException($@"Line {lineNum}: {line} is not a known registry hive key.");
+ keyStack.Push(hive);
+ continue;
+ }
+
+ // If we are at the current depth, we're either moving to a child key or setting a value.
+ if (depth == keyStack.Count)
+ {
+ // Are we setting a value?
+ var rexVal = new Regex(@"^(.*) = (.*)$");
+ var match = rexVal.Match(line.TrimStart());
+ if (match.Success)
+ {
+ var name = match.Groups[1].Value;
+ var value = match.Groups[2].Value;
+
+ // Don't forget - '(Default)' is a magic string for empty (i.e. the default value)...
+ keyStack.Peek().SetValue(name == "(Default)" ? string.Empty : name, value);
+ }
+ else
+ {
+ // We're opening or creating a subkey.
+ var subkeyName = line.TrimStart();
+ keyStack.Push(keyStack.Peek().CreateSubKey(subkeyName));
+ }
+ continue;
+ }
+
+ // If we get here, we've got a malformed file.
+ throw new InvalidOperationException($@"Line {lineNum}: This line is at an invalid depth.");
+ }
+ }
+
+ ///
+ /// Prints a registry key with the given depth.
+ ///
+ /// The key.
+ /// The depth.
+ /// The key, printed at the given depth.
+ private static string PrintKey(IRegistryKey key, int depth)
+ {
+ var indent = new string(' ', depth * IndentSpaces);
+
+ // Get the value strings.
+ var values = key.GetValueNames()
+ .Select(v => $"{indent}{(string.IsNullOrEmpty(v) ? "(Default)" : v)} = {key.GetValue(v)}")
+ .OrderBy(s => s);
+
+ // Get the subkey strings.
+ var subKeys = key.GetSubKeyNames()
+ .OrderBy(sk => sk)
+ .Select(sk =>
+ $"{indent}{sk}{Environment.NewLine}{PrintKey(key.OpenSubKey(sk), depth + 1)}");
+
+ return string.Join(Environment.NewLine, values.Concat(subKeys));
+ }
+
+ ///
+ /// Prints the specified registry view. Used for functional testing.
+ ///
+ /// The registry view.
+ /// The registry view as a string.
+ public string Print(RegistryView registryView)
+ {
+ string v = string.Empty;
+ // Go through the hives. We'll only print them if they have keys.
+ foreach (var rootKey in _rootKeys.Where(rk => rk.Key.Item1 == registryView).OrderBy(k => k.Value.Name))
+ {
+ string print = rootKey.Value.Name;
+ string val = PrintKey(rootKey.Value, 1);
+ if (!string.IsNullOrEmpty(val))
+ {
+ v += print + Environment.NewLine;
+ v += val + Environment.NewLine;
+ v += Environment.NewLine;
+ }
+ }
+
+ return v.Trim();
+ }
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/InMemoryRegistryKey.cs b/Sledge.Formats.Configuration/Registry/InMemoryRegistryKey.cs
new file mode 100644
index 0000000..0599e0f
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/InMemoryRegistryKey.cs
@@ -0,0 +1,191 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.AccessControl;
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// An In-Memory registry key. Primarily used for testing scenarios.
+ ///
+ ///
+ public class InMemoryRegistryKey : IRegistryKey
+ {
+ private const char Separator = '\\';
+ private readonly string _name;
+ private readonly RegistryView _view;
+ private readonly Dictionary _subkeys = new Dictionary();
+ private readonly Dictionary _values = new Dictionary();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The registry view.
+ /// The name.
+ public InMemoryRegistryKey(RegistryView view, string name)
+ {
+ _view = view;
+ _name = name;
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ ///
+ public string[] GetSubKeyNames()
+ {
+ return _subkeys.Values.Select(v => v.Name).ToArray();
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name)
+ {
+ return OpenSubKey(name, RegistryKeyPermissionCheck.ReadSubTree);
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, bool writable)
+ {
+ return OpenSubKey(name,
+ writable ? RegistryKeyPermissionCheck.ReadWriteSubTree : RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.FullControl);
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck)
+ {
+ return OpenSubKey(name, permissionCheck, RegistryRights.FullControl);
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck, RegistryRights rights)
+ {
+ var currentKey = this;
+ var subkeyNames = name.Split(Separator);
+ foreach (var subkeyName in subkeyNames)
+ {
+ var subkeyNameLower = subkeyName.ToLower();
+ if (currentKey._subkeys.ContainsKey(subkeyNameLower) == false)
+ return null;
+ currentKey = currentKey._subkeys[subkeyNameLower];
+ }
+
+ return currentKey;
+ }
+
+ ///
+ public object GetValue(string name)
+ {
+ return GetValue(name, null, RegistryValueOptions.None);
+ }
+
+ ///
+ public object GetValue(string name, object defaultValue)
+ {
+ return GetValue(name, defaultValue, RegistryValueOptions.None);
+ }
+
+ ///
+ public object GetValue(string name, object defaultValue, RegistryValueOptions options)
+ {
+ // Coerce null into the empty string (i.e. '(Default)' value in the registry).
+ var valueName = name ?? string.Empty;
+ return _values.TryGetValue(valueName, out var value) ? value : defaultValue;
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey)
+ {
+ return CreateSubKey(subkey, RegistryKeyPermissionCheck.Default, new RegistrySecurity());
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck)
+ {
+ return CreateSubKey(subkey, permissionCheck, new RegistrySecurity());
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck, RegistrySecurity registrySecurity)
+ {
+ var currentKey = this;
+ var subkeyNames = subkey.Split(Separator);
+ foreach (var subkeyName in subkeyNames)
+ {
+ var subkeyNameLower = subkeyName.ToLower();
+ if (currentKey._subkeys.ContainsKey(subkeyNameLower) == false)
+ currentKey._subkeys[subkeyNameLower] = new InMemoryRegistryKey(_view, subkeyName);
+ currentKey = currentKey._subkeys[subkeyNameLower];
+ }
+
+ return currentKey;
+ }
+
+ ///
+ public void SetValue(string name, object value)
+ {
+ // Coerce null into the empty string (i.e. '(Default)' value in the registry).
+ var valueName = name ?? string.Empty;
+ _values[valueName] = value;
+ }
+
+ ///
+ public void SetValue(string name, object value, RegistryValueKind valueKind)
+ {
+ // Coerce null into the empty string (i.e. '(Default)' value in the registry).
+ var valueName = name ?? string.Empty;
+ _values[valueName] = value;
+ }
+
+ ///
+ public void DeleteSubKeyTree(string subkey)
+ {
+ DeleteSubKeyTree(subkey, true);
+ }
+
+ ///
+ public void DeleteSubKeyTree(string subkey, bool throwOnMissingSubKey)
+ {
+ _subkeys.Remove(subkey);
+ }
+
+ ///
+ public string[] GetValueNames()
+ {
+ return _values.Keys.ToArray();
+ }
+
+ ///
+ public void DeleteValue(string name)
+ {
+ DeleteValue(name, true);
+ }
+
+ ///
+ public void DeleteValue(string name, bool throwOnMissingValue)
+ {
+ _values.Remove(name);
+ }
+
+ ///
+ public void DeleteSubKey(string subkey)
+ {
+ DeleteSubKey(subkey, true);
+ }
+
+ ///
+ public void DeleteSubKey(string subkey, bool throwOnMissingSubKey)
+ {
+ _subkeys.Remove(subkey);
+ }
+
+ ///
+ public string Name => _name;
+
+ ///
+ /// Gets the view for the key.
+ ///
+ public RegistryView View => _view;
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/LICENSE b/Sledge.Formats.Configuration/Registry/LICENSE
new file mode 100644
index 0000000..974896d
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/LICENSE
@@ -0,0 +1,24 @@
+
+Source: https://github.com/dwmkerr/dotnet-windows-registry
+
+MIT License
+
+Copyright (c) 2020 Dave Kerr
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Sledge.Formats.Configuration/Registry/RegistryExtensions.cs b/Sledge.Formats.Configuration/Registry/RegistryExtensions.cs
new file mode 100644
index 0000000..dcd84f9
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/RegistryExtensions.cs
@@ -0,0 +1,25 @@
+using Sledge.Formats.Configuration.Worldcraft;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ public static class RegistryExtensions
+ {
+ public static int GetIntValue(this IRegistryKey key, string name)
+ {
+ return key.GetValue(name, 0) as int? ?? 0;
+ }
+
+ public static bool GetBoolValue(this IRegistryKey key, string name)
+ {
+ return key.GetValue(name, 0) as int? == 1;
+ }
+
+ public static string GetStringValue(this IRegistryKey key, string name)
+ {
+ return key.GetValue(name, 0) as string;
+ }
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/WindowsRegistry.cs b/Sledge.Formats.Configuration/Registry/WindowsRegistry.cs
new file mode 100644
index 0000000..cb59ce3
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/WindowsRegistry.cs
@@ -0,0 +1,20 @@
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// This class implements , providing test-able access to the registry. Clients should
+ /// never use the registry directly, it should use so that we can test these interactions.
+ ///
+ ///
+ public class WindowsRegistry : IRegistry
+ {
+ ///
+ public IRegistryKey OpenBaseKey(RegistryHive hKey, RegistryView view)
+ {
+ // Proxy directly to the windows registry.
+ var key = RegistryKey.OpenBaseKey(hKey, view);
+ return new WindowsRegistryKey(key);
+ }
+ }
+}
diff --git a/Sledge.Formats.Configuration/Registry/WindowsRegistryKey.cs b/Sledge.Formats.Configuration/Registry/WindowsRegistryKey.cs
new file mode 100644
index 0000000..21fdf50
--- /dev/null
+++ b/Sledge.Formats.Configuration/Registry/WindowsRegistryKey.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Security.AccessControl;
+using Microsoft.Win32;
+
+namespace Sledge.Formats.Configuration.Registry
+{
+ ///
+ /// A Windows Registry Key. Essentially a wrapper around .
+ ///
+ ///
+ public class WindowsRegistryKey : IRegistryKey
+ {
+ private readonly RegistryKey _registryKey;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The registry key.
+ /// registryKey
+ public WindowsRegistryKey(RegistryKey registryKey)
+ {
+ _registryKey = registryKey ?? throw new ArgumentNullException(nameof(registryKey));
+ }
+
+ ///
+ public void Dispose()
+ {
+ _registryKey.Dispose();
+ }
+
+ ///
+ public string[] GetSubKeyNames()
+ {
+ return _registryKey.GetSubKeyNames();
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name)
+ {
+ var subkey = _registryKey.OpenSubKey(name);
+ return subkey != null ? new WindowsRegistryKey(subkey) : null;
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, bool writable)
+ {
+ var subkey = _registryKey.OpenSubKey(name, writable);
+ return subkey != null ? new WindowsRegistryKey(subkey) : null;
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck)
+ {
+ var subkey = _registryKey.OpenSubKey(name, permissionCheck);
+ return subkey != null ? new WindowsRegistryKey(subkey) : null;
+ }
+
+ ///
+ public IRegistryKey OpenSubKey(string name, RegistryKeyPermissionCheck permissionCheck, RegistryRights rights)
+ {
+ var subkey = _registryKey.OpenSubKey(name, permissionCheck, rights);
+ return subkey != null ? new WindowsRegistryKey(subkey) : null;
+ }
+
+ ///
+ public object GetValue(string name)
+ {
+ return _registryKey.GetValue(name);
+ }
+
+ ///
+ public object GetValue(string name, object defaultValue)
+ {
+ return _registryKey.GetValue(name, defaultValue);
+ }
+
+ ///
+ public object GetValue(string name, object defaultValue, RegistryValueOptions options)
+ {
+ return _registryKey.GetValue(name, defaultValue, options);
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey)
+ {
+ return new WindowsRegistryKey(_registryKey.CreateSubKey(subkey));
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck)
+ {
+ return new WindowsRegistryKey(_registryKey.CreateSubKey(subkey, permissionCheck));
+ }
+
+ ///
+ public IRegistryKey CreateSubKey(string subkey, RegistryKeyPermissionCheck permissionCheck, RegistrySecurity registrySecurity)
+ {
+ return new WindowsRegistryKey(_registryKey.CreateSubKey(subkey, permissionCheck, registrySecurity));
+ }
+
+ ///
+ public void SetValue(string name, object value)
+ {
+ _registryKey.SetValue(name, value);
+ }
+
+ ///
+ public void SetValue(string name, object value, RegistryValueKind valueKind)
+ {
+ _registryKey.SetValue(name, value, valueKind);
+ }
+
+ ///
+ public void DeleteSubKeyTree(string subkey)
+ {
+ _registryKey.DeleteSubKeyTree(subkey);
+ }
+
+ ///
+ public void DeleteSubKeyTree(string subkey, bool throwOnMissingSubKey)
+ {
+ _registryKey.DeleteSubKeyTree(subkey, throwOnMissingSubKey);
+ }
+
+ ///
+ public string[] GetValueNames()
+ {
+ return _registryKey.GetValueNames();
+ }
+
+ ///
+ public void DeleteValue(string name)
+ {
+ _registryKey.DeleteValue(name);
+ }
+
+ ///
+ public void DeleteValue(string name, bool throwOnMissingValue)
+ {
+ _registryKey.DeleteValue(name, throwOnMissingValue);
+ }
+
+ ///
+ public void DeleteSubKey(string subkey)
+ {
+ _registryKey.DeleteSubKey(subkey);
+ }
+
+ ///
+ public void DeleteSubKey(string subkey, bool throwOnMissingSubKey)
+ {
+ _registryKey.DeleteSubKey(subkey, throwOnMissingSubKey);
+ }
+
+ ///
+ public string Name => _registryKey.Name;
+ }
+}
diff --git a/Sledge.Formats.Configuration/Sledge.Formats.Configuration.csproj b/Sledge.Formats.Configuration/Sledge.Formats.Configuration.csproj
new file mode 100644
index 0000000..9ab8a1e
--- /dev/null
+++ b/Sledge.Formats.Configuration/Sledge.Formats.Configuration.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
diff --git a/Sledge.Formats.Configuration/Worldcraft/MapType.cs b/Sledge.Formats.Configuration/Worldcraft/MapType.cs
new file mode 100644
index 0000000..45d9b78
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/MapType.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public enum MapType
+ {
+ HalfLife,
+ [Obsolete] Quake,
+ [Obsolete] Quake2,
+ [Obsolete] Hexen2,
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/TextureFormat.cs b/Sledge.Formats.Configuration/Worldcraft/TextureFormat.cs
new file mode 100644
index 0000000..42da68b
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/TextureFormat.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public enum TextureFormat
+ {
+ Wad3,
+ [Obsolete] Wad2,
+ [Obsolete] Wal,
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/Worldcraft2DViewsConfiguration.cs b/Sledge.Formats.Configuration/Worldcraft/Worldcraft2DViewsConfiguration.cs
new file mode 100644
index 0000000..2b69496
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/Worldcraft2DViewsConfiguration.cs
@@ -0,0 +1,70 @@
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class Worldcraft2DViewsConfiguration
+ {
+ ///
+ /// Crosshair cursor
+ ///
+ public bool CrosshairCursor { get; set; }
+
+ ///
+ /// Default to 15 degree rotations
+ ///
+ public bool DefaultTo15DegreeRotations { get; set; }
+
+ ///
+ /// Display scrollbars
+ ///
+ public bool DisplayScrollbars { get; set; }
+
+ ///
+ /// Draw Vertices
+ ///
+ public bool DrawVertices { get; set; }
+
+ ///
+ /// White-on-Black color scheme
+ ///
+ public bool WhiteOnBlackColorScheme { get; set; }
+
+ ///
+ /// Keep group when clone-dragging
+ ///
+ public bool KeepGroupWhenCloneDragging { get; set; }
+
+ ///
+ /// Center on camera after movement in 3D
+ ///
+ public bool CenterOnCameraAfterMovement { get; set; }
+
+ ///
+ /// Use Visgroup colors for object lines
+ ///
+ public bool UseVisgroupColorsForObjectLines { get; set; }
+
+ ///
+ /// Arrow keys nudge selected object/vertex
+ ///
+ public bool ArrowKeysNudgeSelectedObject { get; set; }
+
+ ///
+ /// Reorient primitives on creation in the active 2D view
+ ///
+ public bool ReorientPrimitivesOnCreation { get; set; }
+
+ ///
+ /// Automatic infinite selection in 2D windows (no ENTER)
+ ///
+ public bool AutomaticInfiniteSelection { get; set; }
+
+ ///
+ /// Selection box selects by center handles only
+ ///
+ public bool SelectionBoxSelectsByCenterHandlesOnly { get; set; }
+
+ ///
+ /// Grid configuration
+ ///
+ public WorldcraftGridOptions Grid { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/Worldcraft3DViewsConfiguration.cs b/Sledge.Formats.Configuration/Worldcraft/Worldcraft3DViewsConfiguration.cs
new file mode 100644
index 0000000..dbddc59
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/Worldcraft3DViewsConfiguration.cs
@@ -0,0 +1,57 @@
+using System.Drawing;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class Worldcraft3DViewsConfiguration
+ {
+ ///
+ /// Back clipping plane (0-10000)
+ ///
+ public int BackClippingPlane { get; set; }
+
+ ///
+ /// Filter textures
+ ///
+ public bool FilterTextures { get; set; }
+
+ ///
+ /// Animate models
+ ///
+ public bool AnimateModels { get; set; }
+
+ ///
+ /// Model render distance (0-2000)
+ ///
+ public int ModelRenderDistance { get; set; }
+
+ ///
+ /// Use mouselook navigation
+ ///
+ public bool UseMouselookNavigation { get; set; }
+
+ ///
+ /// Reverse mouse Y axis
+ ///
+ public bool ReverseMouseYAxis { get; set; }
+
+ ///
+ /// Forward speed (0-10000)
+ ///
+ public int ForwardSpeed { get; set; }
+
+ ///
+ /// Time to top speed (0-10 seconds)
+ ///
+ public float TimeToTopSpeed { get; set; }
+
+ ///
+ /// Reverse selection order
+ ///
+ public bool ReverseSelectionOrder { get; set; }
+
+ ///
+ /// Background color
+ ///
+ public Color BackgroundColor { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfiguration.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfiguration.cs
new file mode 100644
index 0000000..b5cf2fd
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfiguration.cs
@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Sledge.Formats.Configuration.Registry;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftConfiguration
+ {
+ public WorldcraftGeneralConfiguration General { get; set; }
+ public Worldcraft2DViewsConfiguration Views2D { get; set; }
+ public Worldcraft3DViewsConfiguration Views3D { get; set; }
+ public List TextureDirectories { get; set; }
+ public List GameConfigurations { get; set; }
+
+ public static WorldcraftConfiguration LoadFromRegistry(WorldcraftConfigurationLoadSettings settings = null)
+ {
+ settings = settings ?? WorldcraftConfigurationLoadSettings.Default;
+ var key = settings.RegistryLocation;
+ if (settings.AutodetectRegistryLocation)
+ {
+ var reg = new WindowsRegistry();
+ var baseKey = reg.OpenBaseKey(settings.RegistryHive, settings.RegistryView);
+ key = FindDefaultRegistryKey(baseKey);
+ }
+ if (key == null) throw new FileNotFoundException("Could not find an installation of Worldcraft in the registry.");
+
+ var config = new WorldcraftConfiguration();
+ (config.General, config.TextureDirectories) = LoadGeneralRegistry(key.OpenSubKey(WorldcraftRegistryInfo.KeyGeneral));
+ config.Views2D = null;
+ config.Views3D = null;
+ config.GameConfigurations = null;
+ return config;
+ }
+
+ private static (WorldcraftGeneralConfiguration, List) LoadGeneralRegistry(IRegistryKey key)
+ {
+ var config = new WorldcraftGeneralConfiguration();
+ var textures = new List();
+ if (key != null)
+ {
+ config.InstallDirectory = key.GetStringValue(WorldcraftRegistryInfo.KeyGeneralDirectory);
+ config.UseIndependentWindowConfigurations = key.GetBoolValue(WorldcraftRegistryInfo.KeyGeneralIndependentWindows);
+ config.LoadDefaultWindowPositionsWithMaps = key.GetBoolValue(WorldcraftRegistryInfo.KeyGeneralLoadDefaultPositions);
+ config.UndoLevels = key.GetIntValue(WorldcraftRegistryInfo.KeyGeneralUndoLevels);
+ config.AllowGroupingWhileIgnoreGroupsChecked = key.GetBoolValue(WorldcraftRegistryInfo.KeyGeneralGroupWhileIgnore);
+ config.StretchArchesToFitOriginalBoundingRectangle = key.GetBoolValue(WorldcraftRegistryInfo.KeyGeneralStretchArches);
+
+ if (key.GetValue(WorldcraftRegistryInfo.KeyGeneralTextureFileCount, false) is int texFileCount && texFileCount > 0)
+ {
+ for (var i = 0; i < texFileCount; i++)
+ {
+ var file = key.GetStringValue($"{WorldcraftRegistryInfo.KeyGeneralTextureFilePrefix}{i}");
+ if (file != null) textures.Add(file);
+ }
+ }
+ }
+
+ return (config, textures);
+ }
+
+ private static IRegistryKey FindDefaultRegistryKey(IRegistryKey baseKey)
+ {
+ foreach (var path in WorldcraftRegistryInfo.DefaultRegistryPaths)
+ {
+ var key = baseKey.OpenSubKey(path);
+ if (key == null) continue;
+ if (key.GetSubKeyNames().Contains("2D Views")) return key;
+ }
+ throw new FileNotFoundException("No Worldcraft/Hammer installation could be found.");
+ }
+ }
+}
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfigurationLoadSettings.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfigurationLoadSettings.cs
new file mode 100644
index 0000000..757acb0
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftConfigurationLoadSettings.cs
@@ -0,0 +1,57 @@
+using System.IO;
+using System.Linq;
+using Microsoft.Win32;
+using Sledge.Formats.Configuration.Registry;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftConfigurationLoadSettings
+ {
+ ///
+ /// True to attempt to autodetect the registry location from the known default registry locations
+ ///
+ public bool AutodetectRegistryLocation { get; set; } = true;
+
+ ///
+ /// The registry hive to use
+ ///
+ public RegistryHive RegistryHive { get; set; } = RegistryHive.CurrentUser;
+
+ ///
+ /// The registry view to use
+ ///
+ public RegistryView RegistryView { get; set; } = RegistryView.Default;
+
+ ///
+ /// Set to a non-null value and set AutodetectRegistryLocation to false to specify the registry location.
+ /// The registry location will usually be called "Worldcraft" or "Valve Hammer Editor" and contain subkeys called "General", "2D Views", "3D Views", etc.
+ ///
+ public IRegistryKey RegistryLocation { get; set; }
+
+ ///
+ /// True to attempt to load game configurations from the install directory
+ ///
+ public bool LoadGameConfigurations { get; set; } = true;
+
+ ///
+ /// True to attempt to autodetect the install directory from the registry ([Worldcraft/General/Directory] registry key).
+ /// All worldcraft versions store the install directory in the registry except for version 1.0.
+ ///
+ public bool AutodetectInstallDirectory { get; set; } = true;
+
+ ///
+ /// Set to a non-null value and set AutodetectInstallDirectory to false to specify the install directory
+ ///
+ public string InstallDirectory { get; set; }
+
+ ///
+ /// The default settings for loading a configuration, with registry and install directory auto-detection enabled
+ ///
+ public static WorldcraftConfigurationLoadSettings Default => new WorldcraftConfigurationLoadSettings
+ {
+ AutodetectRegistryLocation = true,
+ LoadGameConfigurations = true,
+ AutodetectInstallDirectory = true,
+ };
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfiguration.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfiguration.cs
new file mode 100644
index 0000000..1e58a34
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfiguration.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftGameConfiguration
+ {
+ ///
+ /// Configuration name
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// List of game data files (.fgd)
+ ///
+ public List GameDataFiles { get; set; }
+
+ ///
+ /// Texture Format
+ ///
+ public TextureFormat TextureFormat { get; set; }
+
+ ///
+ /// Map Type
+ ///
+ public MapType MapType { get; set; }
+
+ ///
+ /// Default PointEntity class
+ ///
+ public string DefaultPointEntityClass { get; set; }
+
+ ///
+ /// Default SolidEntity class
+ ///
+ public string DefaultSolidEntityClass { get; set; }
+
+ ///
+ /// Game executable directory (ex: C:\HalfLife)
+ ///
+ public string GameExecutableDirectory { get; set; }
+
+ ///
+ /// Mod directory (ex: C:\HalfLife\tfc)
+ ///
+ public string ModDirectory { get; set; }
+
+ ///
+ /// Game directory (ex: C:\HalfLife\valve)
+ ///
+ public string GameDirectory { get; set; }
+
+ ///
+ /// RMF directory
+ ///
+ public string RmfDirectory { get; set; }
+
+ ///
+ /// Palette file
+ ///
+ [Obsolete] public string PaletteFile { get; set; }
+
+ ///
+ /// Build programs for this configuration
+ ///
+ public WorldcraftGameConfigurationBuildPrograms BuildPrograms { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfigurationBuildPrograms.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfigurationBuildPrograms.cs
new file mode 100644
index 0000000..d6127fd
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGameConfigurationBuildPrograms.cs
@@ -0,0 +1,35 @@
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftGameConfigurationBuildPrograms
+ {
+ ///
+ /// Game executable
+ ///
+ public string GameExecutable { get; set; }
+
+ ///
+ /// CSG executable
+ ///
+ public string CsgExecutable { get; set; }
+
+ ///
+ /// BSP executable
+ ///
+ public string BspExecutable { get; set; }
+
+ ///
+ /// VIS executable
+ ///
+ public string VisExecutable { get; set; }
+
+ ///
+ /// RAD executable
+ ///
+ public string RadExecutable { get; set; }
+
+ ///
+ /// Place compiled maps in this directory before running the game
+ ///
+ public string BspDirectory { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftGeneralConfiguration.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGeneralConfiguration.cs
new file mode 100644
index 0000000..ee5b368
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGeneralConfiguration.cs
@@ -0,0 +1,35 @@
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftGeneralConfiguration
+ {
+ ///
+ /// Worldcraft install directory
+ ///
+ public string InstallDirectory { get; set; }
+
+ ///
+ /// Use independent window configurations
+ ///
+ public bool UseIndependentWindowConfigurations { get; set; }
+
+ ///
+ /// Load default window positions with maps
+ ///
+ public bool LoadDefaultWindowPositionsWithMaps { get; set; }
+
+ ///
+ /// Undo levels
+ ///
+ public int UndoLevels { get; set; }
+
+ ///
+ /// Allow grouping/ungrouping while Ignore Groups is checked
+ ///
+ public bool AllowGroupingWhileIgnoreGroupsChecked { get; set; }
+
+ ///
+ /// Stretch arches to fit original bounding rectangle
+ ///
+ public bool StretchArchesToFitOriginalBoundingRectangle { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftGridOptions.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGridOptions.cs
new file mode 100644
index 0000000..944b2a0
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftGridOptions.cs
@@ -0,0 +1,40 @@
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public class WorldcraftGridOptions
+ {
+ ///
+ /// Size
+ ///
+ public int Size { get; set; }
+
+ ///
+ /// Intensity (0-100)
+ ///
+ public int Intensity { get; set; }
+
+ ///
+ /// Highlight every 64 units
+ ///
+ public bool HighlightEvery64Units { get; set; }
+
+ ///
+ /// Highlight every N grid lines, set to 0 to disable
+ ///
+ public int HighlightEveryNGridLines { get; set; }
+
+ ///
+ /// Hide grid smaller than 4 pixels
+ ///
+ public bool HideGridSmallerThan4Pixels { get; set; }
+
+ ///
+ /// Highlight every 1024 units
+ ///
+ public bool HighlightEvery1024Units { get; set; }
+
+ ///
+ /// Dotted Grid
+ ///
+ public bool DottedGrid { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.Configuration/Worldcraft/WorldcraftRegistryInfo.cs b/Sledge.Formats.Configuration/Worldcraft/WorldcraftRegistryInfo.cs
new file mode 100644
index 0000000..ea2cd4e
--- /dev/null
+++ b/Sledge.Formats.Configuration/Worldcraft/WorldcraftRegistryInfo.cs
@@ -0,0 +1,33 @@
+namespace Sledge.Formats.Configuration.Worldcraft
+{
+ public static class WorldcraftRegistryInfo
+ {
+ ///
+ /// The default registry paths where Worldcraft stores its settings, in descending order by version
+ ///
+ // ReSharper disable once MemberCanBePrivate.Global
+ public static readonly string[] DefaultRegistryPaths = {
+ @"Software\Valve\Valve Hammer Editor",
+ @"Software\Valve\Worldcraft",
+ @"Software\Worldcraft\Worldcraft"
+ };
+
+ public const string Key2DViews = "2D Views";
+ public const string Key3DViews = "3D Views";
+ public const string KeyGeneral = "General";
+ public const string KeyRecentFiles = "Recent File List";
+
+ public const string KeyGeneralDirectory = "Directory";
+ public const string KeyGeneralBrightness = "Brightness";
+ public const string KeyGeneralGroupWhileIgnore = "GroupWhileIgnore";
+ public const string KeyGeneralIndependentWindows = "Independent Windows";
+ public const string KeyGeneralLoadDefaultPositions = "Load Default Positions";
+ public const string KeyGeneralLockingTextures = "Locking Textures";
+ public const string KeyGeneralNewBars = "NewBars";
+ public const string KeyGeneralStretchArches = "StretchArches";
+ public const string KeyGeneralTextureAlignment = "Texture Alignment";
+ public const string KeyGeneralTextureFilePrefix = "TextureFile";
+ public const string KeyGeneralTextureFileCount = "TextureFileCount";
+ public const string KeyGeneralUndoLevels = "Undo Levels";
+ }
+}
\ No newline at end of file
diff --git a/Sledge.Formats.GameData.Tests/Sledge.Formats.GameData.Tests.csproj b/Sledge.Formats.GameData.Tests/Sledge.Formats.GameData.Tests.csproj
index 9966ba5..7394925 100644
--- a/Sledge.Formats.GameData.Tests/Sledge.Formats.GameData.Tests.csproj
+++ b/Sledge.Formats.GameData.Tests/Sledge.Formats.GameData.Tests.csproj
@@ -164,9 +164,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj b/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj
index 350ab67..aa67c6d 100644
--- a/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj
+++ b/Sledge.Formats.Map.Tests/Sledge.Formats.Map.Tests.csproj
@@ -40,9 +40,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.Model.Tests/Sledge.Formats.Model.Tests.csproj b/Sledge.Formats.Model.Tests/Sledge.Formats.Model.Tests.csproj
index fd7ae23..352650f 100644
--- a/Sledge.Formats.Model.Tests/Sledge.Formats.Model.Tests.csproj
+++ b/Sledge.Formats.Model.Tests/Sledge.Formats.Model.Tests.csproj
@@ -10,9 +10,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj b/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj
index 6686dc0..60c317d 100644
--- a/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj
+++ b/Sledge.Formats.Tests/Sledge.Formats.Tests.csproj
@@ -9,9 +9,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.Texture.Tests/Sledge.Formats.Texture.Tests.csproj b/Sledge.Formats.Texture.Tests/Sledge.Formats.Texture.Tests.csproj
index 75e73cb..d848770 100644
--- a/Sledge.Formats.Texture.Tests/Sledge.Formats.Texture.Tests.csproj
+++ b/Sledge.Formats.Texture.Tests/Sledge.Formats.Texture.Tests.csproj
@@ -7,9 +7,9 @@
-
-
-
+
+
+
diff --git a/Sledge.Formats.sln b/Sledge.Formats.sln
index 0c78020..7ece97b 100644
--- a/Sledge.Formats.sln
+++ b/Sledge.Formats.sln
@@ -38,6 +38,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sledge.Formats.Model.Tests"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sledge.Formats.Texture.ImageSharp", "Sledge.Formats.Texture.ImageSharp\Sledge.Formats.Texture.ImageSharp.csproj", "{841D69BB-CED0-4A44-A434-9D1272EBA0BA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sledge.Formats.Configuration", "Sledge.Formats.Configuration\Sledge.Formats.Configuration.csproj", "{83B158CD-C365-4AA2-8629-AFBC544BA50B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sledge.Formats.Configuration.Tests", "Sledge.Formats.Configuration.Tests\Sledge.Formats.Configuration.Tests.csproj", "{DA30E2F5-6B37-45C2-9D88-C148D8AF0EDB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -100,6 +104,14 @@ Global
{841D69BB-CED0-4A44-A434-9D1272EBA0BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{841D69BB-CED0-4A44-A434-9D1272EBA0BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{841D69BB-CED0-4A44-A434-9D1272EBA0BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83B158CD-C365-4AA2-8629-AFBC544BA50B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83B158CD-C365-4AA2-8629-AFBC544BA50B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83B158CD-C365-4AA2-8629-AFBC544BA50B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83B158CD-C365-4AA2-8629-AFBC544BA50B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DA30E2F5-6B37-45C2-9D88-C148D8AF0EDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DA30E2F5-6B37-45C2-9D88-C148D8AF0EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DA30E2F5-6B37-45C2-9D88-C148D8AF0EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DA30E2F5-6B37-45C2-9D88-C148D8AF0EDB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE