From 0b2d13e61a2c2d12a9c41581a6869bdff1d155c0 Mon Sep 17 00:00:00 2001 From: ThevinSilva Date: Fri, 28 Nov 2025 00:46:09 +0000 Subject: [PATCH 1/2] Generated some potential Tests + Work in progress Bindings Parser --- EliteAPI.Tests/BindingTests.cs | 329 +++++++++++++++++++++++++++++ EliteAPI/bindings/Binding.cs | 18 ++ EliteAPI/bindings/BindingParser.cs | 125 +++++++++++ EliteAPI/bindings/Control.cs | 16 ++ EliteAPI/bindings/IBinding.cs | 9 + 5 files changed, 497 insertions(+) create mode 100644 EliteAPI.Tests/BindingTests.cs create mode 100644 EliteAPI/bindings/Binding.cs create mode 100644 EliteAPI/bindings/BindingParser.cs create mode 100644 EliteAPI/bindings/Control.cs create mode 100644 EliteAPI/bindings/IBinding.cs diff --git a/EliteAPI.Tests/BindingTests.cs b/EliteAPI.Tests/BindingTests.cs new file mode 100644 index 00000000..d115335d --- /dev/null +++ b/EliteAPI.Tests/BindingTests.cs @@ -0,0 +1,329 @@ +using System.Linq; +using EliteAPI.Bindings; +using FluentAssertions; +using TUnit; + +namespace EliteAPI.Tests.Bindings; + +public class BindingParserTests +{ + [Test] + public void Parse_Should_Read_Simple_Primary_Keyboard_Bindings() + { + const string xml = @" + + + + + + + + + + "; + + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls.Should().ContainKeys("YawLeftButton", "YawRightButton"); + + var yawLeft = controls["YawLeftButton"]; + yawLeft.Primary.HasValue.Should().BeTrue(); + yawLeft.Primary!.Value.Device.Should().Be("Keyboard"); + yawLeft.Primary!.Value.Key.Should().Be("Key_A"); + yawLeft.Secondary.HasValue.Should().BeFalse(); + yawLeft.IsToggle.Should().BeNull(); + yawLeft.IsInverted.Should().BeNull(); + yawLeft.Deadzone.Should().BeNull(); + + var yawRight = controls["YawRightButton"]; + yawRight.Primary.HasValue.Should().BeTrue(); + yawRight.Primary!.Value.Device.Should().Be("Keyboard"); + yawRight.Primary!.Value.Key.Should().Be("Key_D"); + yawRight.Secondary.HasValue.Should().BeFalse(); + } + + [Test] + public void Parse_Should_Treat_NoDevice_EmptyKey_As_Unbound() + { + const string xml = @" + + + + + + + + + + + "; + + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls.Should().ContainKeys("MouseReset", "RollLeftButton"); + + foreach (var control in controls.Values) + { + control.Primary.HasValue.Should().BeFalse(); + control.Secondary.HasValue.Should().BeFalse(); + control.IsToggle.Should().BeNull(); + control.IsInverted.Should().BeNull(); + control.Deadzone.Should().BeNull(); + } + } + + [Test] + public void Parse_Should_Map_Binding_Element_To_Primary_And_Read_Inverted_And_Deadzone() + { + const string xml = @" + + + + + + + "; + + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "YawAxisRaw"); + + control.Primary.HasValue.Should().BeTrue(); + var binding = control.Primary!.Value; + + binding.Device.Should().Be("Joystick"); + binding.Key.Should().Be("Joy_X"); + binding.Modifiers.Should().BeNull(); + + control.Secondary.HasValue.Should().BeFalse(); + control.IsInverted.Should().BeTrue(); + control.IsToggle.Should().BeNull(); + control.Deadzone.Should().Be(0.25f); + } + + [Test] + public void Parse_Should_Handle_All_ToggleOn_Variants() + { + const string xml = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + "; + + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls["BlockMouseDecay"].IsToggle.Should().BeFalse(); + controls["ToggleFlightAssist"].IsToggle.Should().BeTrue(); + + // Presence of with no Value attribute – treat as true + controls["YawToRollButton"].IsToggle.Should().BeTrue(); + + // No element at all + controls["MouseReset"].IsToggle.Should().BeNull(); + } + + [Test] + public void Parse_Should_Read_Modifiers_On_Primary_And_Secondary() + { + const string xml = @" + + + + + + + + + + + + + + + + + + + + "; + + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + // Primary modifiers + var photo = controls["PhotoCameraToggle"]; + photo.Primary.HasValue.Should().BeTrue(); + var photoPrimary = photo.Primary!.Value; + + photoPrimary.Device.Should().Be("Keyboard"); + photoPrimary.Key.Should().Be("Key_Space"); + photoPrimary.Modifiers.Should().NotBeNull(); + photoPrimary.Modifiers!.Should().HaveCount(2); + + var firstPhotoMod = (Binding)photoPrimary.Modifiers![0]; + var secondPhotoMod = (Binding)photoPrimary.Modifiers![1]; + + firstPhotoMod.Device.Should().Be("Keyboard"); + firstPhotoMod.Key.Should().Be("Key_LeftControl"); + secondPhotoMod.Device.Should().Be("Keyboard"); + secondPhotoMod.Key.Should().Be("Key_LeftAlt"); + + // Secondary modifiers + var reverse = controls["ToggleReverseThrottleInput"]; + reverse.Secondary.HasValue.Should().BeTrue(); + var reverseSecondary = reverse.Secondary!.Value; + + reverseSecondary.Device.Should().Be("Keyboard"); + reverseSecondary.Key.Should().Be("Key_A"); + reverseSecondary.Modifiers.Should().NotBeNull(); + reverseSecondary.Modifiers!.Should().HaveCount(2); + + var firstReverseMod = (Binding)reverseSecondary.Modifiers![0]; + var secondReverseMod = (Binding)reverseSecondary.Modifiers![1]; + + firstReverseMod.Device.Should().Be("Keyboard"); + firstReverseMod.Key.Should().Be("Key_LeftAlt"); + secondReverseMod.Device.Should().Be("Keyboard"); + secondReverseMod.Key.Should().Be("Key_RightShift"); + } + + [Test] + public void Parse_Should_Ignore_Hold_Child_And_Still_Parse_Binding_And_Toggle() + { + const string xml = @" + + + + + + + + + "; + + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "HumanoidItemWheelButton"); + + control.Primary.HasValue.Should().BeTrue(); + var primary = control.Primary!.Value; + + primary.Device.Should().Be("Keyboard"); + primary.Key.Should().Be("Key_LeftAlt"); + + // Hold isn't currently modelled on Binding – at minimum it must not break parsing + primary.Modifiers.Should().BeNull(); + + control.IsToggle.Should().BeFalse(); // Value=""0"" + } + + [Test] + public void Parse_Should_Treat_Key_Attribute_Case_Insensitive() + { + // Based on ToggleVanityCamera in HCS, but with a meaningful secondary key + const string xml = @" + + + + + + + "; + + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "ToggleVanityCamera"); + + control.Primary.HasValue.Should().BeTrue(); + control.Primary!.Value.Key.Should().Be("Key_RightShift"); + + control.Secondary.HasValue.Should().BeTrue(); + control.Secondary!.Value.Device.Should().Be("Keyboard"); + control.Secondary!.Value.Key.Should().Be("Key_F12"); + } + + [Test] + public void Parse_Should_Handle_Float_Deadzone_Formats() + { + const string xml = @" + + + + + + + + + + + + + + + + "; + + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls["RollAxisRaw"].Deadzone.Should().Be(0.0f); + controls["LateralThrustRaw"].Deadzone.Should().Be(0.05f); + controls["VerticalThrustRaw"].Deadzone.Should().Be(7.0f); + } + + [Test] + public void Parse_Should_Ignore_Value_Only_Elements_And_Only_Return_Controls_With_Bindings() + { + const string xml = @" + + nl-NL + + + + + + + + + "; + + var controls = BindingParser.Parse(xml).ToArray(); + + controls.Should().ContainSingle(c => c.Name == "YawLeftButton"); + controls.Select(c => c.Name).Should().NotContain("MouseXMode"); + controls.Select(c => c.Name).Should().NotContain("MouseSensitivity"); + controls.Select(c => c.Name).Should().NotContain("MouseGUI"); + } +} diff --git a/EliteAPI/bindings/Binding.cs b/EliteAPI/bindings/Binding.cs new file mode 100644 index 00000000..f0570736 --- /dev/null +++ b/EliteAPI/bindings/Binding.cs @@ -0,0 +1,18 @@ +namespace EliteAPI.Bindings; + +public readonly struct Binding : IBinding +{ + public string Device { get; init; } + + public string Key { get; init; } + + public IBinding[]? Modifiers { get; init; } + + public Binding(string device, string key, IBinding[]? modifiers = null) + { + Device = device; + Key = key; + Modifiers = modifiers; + + } +} diff --git a/EliteAPI/bindings/BindingParser.cs b/EliteAPI/bindings/BindingParser.cs new file mode 100644 index 00000000..4294f6ae --- /dev/null +++ b/EliteAPI/bindings/BindingParser.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Xml.Linq; +using Newtonsoft.Json; + +namespace EliteAPI.Bindings; + +public static class BindingParser +{ + public static IReadOnlyCollection Parse(string xml) + { + var doc = XDocument.Parse(xml); + var root = doc.Root ?? throw new InvalidOperationException("No root element in binds file."); + + var controls = root.Elements() + .Select(ParseElement) + .ToArray(); + + // Debug output – keep or remove as you like + foreach (var element in controls) + Console.WriteLine(JsonConvert.SerializeObject(element)); + + return controls; + } + + private static Control ParseElement(XElement element) + { + var primaryElement = GetChildCI(element, "Primary") ?? GetChildCI(element, "Binding"); + var secondaryElement = GetChildCI(element, "Secondary"); + + var deadzoneRaw = GetAttributeCI(GetChildCI(element, "Deadzone"), "Value"); + var invertedRaw = GetAttributeCI(GetChildCI(element, "Inverted"), "Value"); + var toggleRaw = GetAttributeCI(GetChildCI(element, "ToggleOn"), "Value"); + + return new Control + { + Name = element.Name.LocalName, + Primary = ParseBinding(primaryElement), + Secondary = ParseBinding(secondaryElement), + Deadzone = ParseFloat(deadzoneRaw), + IsToggle = ParseBool(toggleRaw), + IsInverted = ParseBool(invertedRaw) + }; + } + + /// + /// Parse a single binding element (Binding/Primary/Secondary), including Modifiers, + /// handling casing and "unbound" semantics. + /// + private static Binding? ParseBinding(XElement? element) + { + if (element is null) + return null; + + var device = GetAttributeCI(element, "Device") ?? string.Empty; + var key = GetAttributeCI(element, "Key") ?? string.Empty; + + // Treat unbound bindings (NoDevice / empty key) as null + if (IsUnbound(device, key)) + return null; + + // Parse child elements (case-insensitive) + var modifiers = element + .Elements() + .Where(e => string.Equals(e.Name.LocalName, "Modifier", StringComparison.OrdinalIgnoreCase)) + .Select(m => + { + var mDevice = GetAttributeCI(m, "Device") ?? string.Empty; + var mKey = GetAttributeCI(m, "Key") ?? string.Empty; + return (IBinding)new Binding(mDevice, mKey); + }) + .ToArray(); + + return new Binding(device, key, modifiers.Length == 0 ? null : modifiers); + } + + /// + /// Decide when a binding is "unbound" and should be treated as null. + /// + private static bool IsUnbound(string device, string key) + { + // Classic ED unbound pattern: {NoDevice} + empty key + if (string.Equals(device, "{NoDevice}", StringComparison.OrdinalIgnoreCase)) + return true; + + // No key at all = unbound (even if device is set) + if (string.IsNullOrWhiteSpace(key)) + return true; + + // Super defensive: both empty + if (string.IsNullOrWhiteSpace(device) && string.IsNullOrWhiteSpace(key)) + return true; + + return false; + } + + private static float? ParseFloat(string? raw) => + float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) + ? value + : null; + + private static bool? ParseBool(string? raw) => raw switch + { + "1" => true, + "0" => false, + null => null, + _ => null + }; + + private static XElement? GetChildCI(XElement parent, string name) => + parent.Elements() + .FirstOrDefault(e => string.Equals(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)); + + private static string? GetAttributeCI(XElement? element, string name) + { + if (element is null) + return null; + + return element.Attributes() + .FirstOrDefault(a => string.Equals(a.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)) + ?.Value; + } +} diff --git a/EliteAPI/bindings/Control.cs b/EliteAPI/bindings/Control.cs new file mode 100644 index 00000000..c63f5841 --- /dev/null +++ b/EliteAPI/bindings/Control.cs @@ -0,0 +1,16 @@ +namespace EliteAPI.Bindings; + +public readonly struct Control +{ + public string Name { get; init; } + + public Binding? Primary { get; init; } + + public Binding? Secondary { get; init; } + + public bool? IsToggle { get; init; } + + public bool? IsInverted { get; init; } + + public float? Deadzone { get; init; } +} diff --git a/EliteAPI/bindings/IBinding.cs b/EliteAPI/bindings/IBinding.cs new file mode 100644 index 00000000..ff69b17d --- /dev/null +++ b/EliteAPI/bindings/IBinding.cs @@ -0,0 +1,9 @@ +namespace EliteAPI.Bindings; + +public interface IBinding +{ + public string Device { get; init; } + + public string Key { get; init; } + +} From 875648fee85ab2d50969d2c3884da346f5b48be4 Mon Sep 17 00:00:00 2001 From: ThevinSilva Date: Sat, 29 Nov 2025 13:45:46 +0000 Subject: [PATCH 2/2] Formatted + Wrapped Main parse in Try Catch Block + Better Function Names --- Console/Program.cs | 19 +- EliteAPI.Tests/BindingTests.cs | 357 +++--- EliteAPI.Tests/test.binds | 1895 ++++++++++++++++++++++++++++ EliteAPI/bindings/Binding.cs | 18 +- EliteAPI/bindings/BindingParser.cs | 230 ++-- EliteAPI/bindings/IBinding.cs | 4 +- 6 files changed, 2218 insertions(+), 305 deletions(-) create mode 100644 EliteAPI.Tests/test.binds diff --git a/Console/Program.cs b/Console/Program.cs index 9c936454..0ddf109c 100644 --- a/Console/Program.cs +++ b/Console/Program.cs @@ -1,11 +1,18 @@ using EliteApi; using EliteAPI.Journals; using EliteAPI.Json; +using EliteAPI.Bindings; +using System.Xml.Linq; -var json = await File.ReadAllTextAsync("./EliteAPI.Tests/TestFiles/Status/StatusCombat.json"); -var paths = JournalUtils.ToPaths(json); +// var json = await File.ReadAllTextAsync("./EliteAPI.Tests/TestFiles/Status/StatusCombat.json"); +// var paths = JournalUtils.ToPaths(json); -foreach (var path in paths) -{ - Console.WriteLine($"{path.Path}: {path.Value} ({path.Type})"); -} +// foreach (var path in paths) +// { +// Console.WriteLine($"{path.Path}: {path.Value} ({path.Type})"); +// } + + + + +BindingParser.Parse(@"C:\Users\thevi\Documents\WORK_SPACE\EliteAPI\EliteAPI.Tests\Custom.4.2.binds"); diff --git a/EliteAPI.Tests/BindingTests.cs b/EliteAPI.Tests/BindingTests.cs index d115335d..bc6456bb 100644 --- a/EliteAPI.Tests/BindingTests.cs +++ b/EliteAPI.Tests/BindingTests.cs @@ -1,16 +1,25 @@ -using System.Linq; using EliteAPI.Bindings; using FluentAssertions; -using TUnit; namespace EliteAPI.Tests.Bindings; public class BindingParserTests { - [Test] - public void Parse_Should_Read_Simple_Primary_Keyboard_Bindings() - { - const string xml = @" + + [Test] + public void Parse_Sample_File() + { + string xml = File.ReadAllText(@"EliteAPI.Tests\test.binds"); + + var content = BindingParser.Parse(xml); + + content.Should().NotBeNull(); + } + + [Test] + public void Parse_Should_Read_Simple_Primary_Keyboard_Bindings() + { + const string xml = @" @@ -22,32 +31,32 @@ public void Parse_Should_Read_Simple_Primary_Keyboard_Bindings() "; - var controls = BindingParser - .Parse(xml) - .ToDictionary(c => c.Name); - - controls.Should().ContainKeys("YawLeftButton", "YawRightButton"); - - var yawLeft = controls["YawLeftButton"]; - yawLeft.Primary.HasValue.Should().BeTrue(); - yawLeft.Primary!.Value.Device.Should().Be("Keyboard"); - yawLeft.Primary!.Value.Key.Should().Be("Key_A"); - yawLeft.Secondary.HasValue.Should().BeFalse(); - yawLeft.IsToggle.Should().BeNull(); - yawLeft.IsInverted.Should().BeNull(); - yawLeft.Deadzone.Should().BeNull(); - - var yawRight = controls["YawRightButton"]; - yawRight.Primary.HasValue.Should().BeTrue(); - yawRight.Primary!.Value.Device.Should().Be("Keyboard"); - yawRight.Primary!.Value.Key.Should().Be("Key_D"); - yawRight.Secondary.HasValue.Should().BeFalse(); - } - - [Test] - public void Parse_Should_Treat_NoDevice_EmptyKey_As_Unbound() - { - const string xml = @" + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls.Should().ContainKeys("YawLeftButton", "YawRightButton"); + + var yawLeft = controls["YawLeftButton"]; + yawLeft.Primary.HasValue.Should().BeTrue(); + yawLeft.Primary!.Value.Device.Should().Be("Keyboard"); + yawLeft.Primary!.Value.Key.Should().Be("Key_A"); + yawLeft.Secondary.HasValue.Should().BeFalse(); + yawLeft.IsToggle.Should().BeNull(); + yawLeft.IsInverted.Should().BeNull(); + yawLeft.Deadzone.Should().BeNull(); + + var yawRight = controls["YawRightButton"]; + yawRight.Primary.HasValue.Should().BeTrue(); + yawRight.Primary!.Value.Device.Should().Be("Keyboard"); + yawRight.Primary!.Value.Key.Should().Be("Key_D"); + yawRight.Secondary.HasValue.Should().BeFalse(); + } + + [Test] + public void Parse_Should_Treat_NoDevice_EmptyKey_As_Unbound() + { + const string xml = @" @@ -60,26 +69,26 @@ public void Parse_Should_Treat_NoDevice_EmptyKey_As_Unbound() "; - var controls = BindingParser - .Parse(xml) - .ToDictionary(c => c.Name); - - controls.Should().ContainKeys("MouseReset", "RollLeftButton"); - - foreach (var control in controls.Values) - { - control.Primary.HasValue.Should().BeFalse(); - control.Secondary.HasValue.Should().BeFalse(); - control.IsToggle.Should().BeNull(); - control.IsInverted.Should().BeNull(); - control.Deadzone.Should().BeNull(); - } - } - - [Test] - public void Parse_Should_Map_Binding_Element_To_Primary_And_Read_Inverted_And_Deadzone() - { - const string xml = @" + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + controls.Should().ContainKeys("MouseReset", "RollLeftButton"); + + foreach (var control in controls.Values) + { + control.Primary.HasValue.Should().BeFalse(); + control.Secondary.HasValue.Should().BeFalse(); + control.IsToggle.Should().BeNull(); + control.IsInverted.Should().BeNull(); + control.Deadzone.Should().BeNull(); + } + } + + [Test] + public void Parse_Should_Map_Binding_Element_To_Primary_And_Read_Inverted_And_Deadzone() + { + const string xml = @" @@ -88,27 +97,27 @@ public void Parse_Should_Map_Binding_Element_To_Primary_And_Read_Inverted_And_De "; - var control = BindingParser - .Parse(xml) - .Single(c => c.Name == "YawAxisRaw"); + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "YawAxisRaw"); - control.Primary.HasValue.Should().BeTrue(); - var binding = control.Primary!.Value; + control.Primary.HasValue.Should().BeTrue(); + var binding = control.Primary!.Value; - binding.Device.Should().Be("Joystick"); - binding.Key.Should().Be("Joy_X"); - binding.Modifiers.Should().BeNull(); + binding.Device.Should().Be("Joystick"); + binding.Key.Should().Be("Joy_X"); + binding.Modifiers.Should().BeNull(); - control.Secondary.HasValue.Should().BeFalse(); - control.IsInverted.Should().BeTrue(); - control.IsToggle.Should().BeNull(); - control.Deadzone.Should().Be(0.25f); - } + control.Secondary.HasValue.Should().BeFalse(); + control.IsInverted.Should().BeTrue(); + control.IsToggle.Should().BeNull(); + control.Deadzone.Should().Be(0.25f); + } - [Test] - public void Parse_Should_Handle_All_ToggleOn_Variants() - { - const string xml = @" + [Test] + public void Parse_Should_Handle_All_ToggleOn_Variants() + { + const string xml = @" @@ -138,24 +147,24 @@ public void Parse_Should_Handle_All_ToggleOn_Variants() "; - var controls = BindingParser - .Parse(xml) - .ToDictionary(c => c.Name); + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); - controls["BlockMouseDecay"].IsToggle.Should().BeFalse(); - controls["ToggleFlightAssist"].IsToggle.Should().BeTrue(); + controls["BlockMouseDecay"].IsToggle.Should().BeFalse(); + controls["ToggleFlightAssist"].IsToggle.Should().BeTrue(); - // Presence of with no Value attribute – treat as true - controls["YawToRollButton"].IsToggle.Should().BeTrue(); + // Presence of with no Value attribute – treat as true + controls["YawToRollButton"].IsToggle.Should().BeTrue(); - // No element at all - controls["MouseReset"].IsToggle.Should().BeNull(); - } + // No element at all + controls["MouseReset"].IsToggle.Should().BeNull(); + } - [Test] - public void Parse_Should_Read_Modifiers_On_Primary_And_Secondary() - { - const string xml = @" + [Test] + public void Parse_Should_Read_Modifiers_On_Primary_And_Secondary() + { + const string xml = @" @@ -177,51 +186,51 @@ public void Parse_Should_Read_Modifiers_On_Primary_And_Secondary() "; - var controls = BindingParser - .Parse(xml) - .ToDictionary(c => c.Name); - - // Primary modifiers - var photo = controls["PhotoCameraToggle"]; - photo.Primary.HasValue.Should().BeTrue(); - var photoPrimary = photo.Primary!.Value; - - photoPrimary.Device.Should().Be("Keyboard"); - photoPrimary.Key.Should().Be("Key_Space"); - photoPrimary.Modifiers.Should().NotBeNull(); - photoPrimary.Modifiers!.Should().HaveCount(2); - - var firstPhotoMod = (Binding)photoPrimary.Modifiers![0]; - var secondPhotoMod = (Binding)photoPrimary.Modifiers![1]; - - firstPhotoMod.Device.Should().Be("Keyboard"); - firstPhotoMod.Key.Should().Be("Key_LeftControl"); - secondPhotoMod.Device.Should().Be("Keyboard"); - secondPhotoMod.Key.Should().Be("Key_LeftAlt"); - - // Secondary modifiers - var reverse = controls["ToggleReverseThrottleInput"]; - reverse.Secondary.HasValue.Should().BeTrue(); - var reverseSecondary = reverse.Secondary!.Value; - - reverseSecondary.Device.Should().Be("Keyboard"); - reverseSecondary.Key.Should().Be("Key_A"); - reverseSecondary.Modifiers.Should().NotBeNull(); - reverseSecondary.Modifiers!.Should().HaveCount(2); - - var firstReverseMod = (Binding)reverseSecondary.Modifiers![0]; - var secondReverseMod = (Binding)reverseSecondary.Modifiers![1]; - - firstReverseMod.Device.Should().Be("Keyboard"); - firstReverseMod.Key.Should().Be("Key_LeftAlt"); - secondReverseMod.Device.Should().Be("Keyboard"); - secondReverseMod.Key.Should().Be("Key_RightShift"); - } - - [Test] - public void Parse_Should_Ignore_Hold_Child_And_Still_Parse_Binding_And_Toggle() - { - const string xml = @" + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); + + // Primary modifiers + var photo = controls["PhotoCameraToggle"]; + photo.Primary.HasValue.Should().BeTrue(); + var photoPrimary = photo.Primary!.Value; + + photoPrimary.Device.Should().Be("Keyboard"); + photoPrimary.Key.Should().Be("Key_Space"); + photoPrimary.Modifiers.Should().NotBeNull(); + photoPrimary.Modifiers!.Should().HaveCount(2); + + var firstPhotoMod = (Binding)photoPrimary.Modifiers![0]; + var secondPhotoMod = (Binding)photoPrimary.Modifiers![1]; + + firstPhotoMod.Device.Should().Be("Keyboard"); + firstPhotoMod.Key.Should().Be("Key_LeftControl"); + secondPhotoMod.Device.Should().Be("Keyboard"); + secondPhotoMod.Key.Should().Be("Key_LeftAlt"); + + // Secondary modifiers + var reverse = controls["ToggleReverseThrottleInput"]; + reverse.Secondary.HasValue.Should().BeTrue(); + var reverseSecondary = reverse.Secondary!.Value; + + reverseSecondary.Device.Should().Be("Keyboard"); + reverseSecondary.Key.Should().Be("Key_A"); + reverseSecondary.Modifiers.Should().NotBeNull(); + reverseSecondary.Modifiers!.Should().HaveCount(2); + + var firstReverseMod = (Binding)reverseSecondary.Modifiers![0]; + var secondReverseMod = (Binding)reverseSecondary.Modifiers![1]; + + firstReverseMod.Device.Should().Be("Keyboard"); + firstReverseMod.Key.Should().Be("Key_LeftAlt"); + secondReverseMod.Device.Should().Be("Keyboard"); + secondReverseMod.Key.Should().Be("Key_RightShift"); + } + + [Test] + public void Parse_Should_Ignore_Hold_Child_And_Still_Parse_Binding_And_Toggle() + { + const string xml = @" @@ -232,27 +241,27 @@ public void Parse_Should_Ignore_Hold_Child_And_Still_Parse_Binding_And_Toggle() "; - var control = BindingParser - .Parse(xml) - .Single(c => c.Name == "HumanoidItemWheelButton"); + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "HumanoidItemWheelButton"); - control.Primary.HasValue.Should().BeTrue(); - var primary = control.Primary!.Value; + control.Primary.HasValue.Should().BeTrue(); + var primary = control.Primary!.Value; - primary.Device.Should().Be("Keyboard"); - primary.Key.Should().Be("Key_LeftAlt"); + primary.Device.Should().Be("Keyboard"); + primary.Key.Should().Be("Key_LeftAlt"); - // Hold isn't currently modelled on Binding – at minimum it must not break parsing - primary.Modifiers.Should().BeNull(); + // Hold isn't currently modelled on Binding – at minimum it must not break parsing + primary.Modifiers.Should().BeNull(); - control.IsToggle.Should().BeFalse(); // Value=""0"" - } + control.IsToggle.Should().BeFalse(); // Value=""0"" + } - [Test] - public void Parse_Should_Treat_Key_Attribute_Case_Insensitive() - { - // Based on ToggleVanityCamera in HCS, but with a meaningful secondary key - const string xml = @" + [Test] + public void Parse_Should_Treat_Key_Attribute_Case_Insensitive() + { + // Based on ToggleVanityCamera in HCS, but with a meaningful secondary key + const string xml = @" @@ -261,22 +270,22 @@ public void Parse_Should_Treat_Key_Attribute_Case_Insensitive() "; - var control = BindingParser - .Parse(xml) - .Single(c => c.Name == "ToggleVanityCamera"); + var control = BindingParser + .Parse(xml) + .Single(c => c.Name == "ToggleVanityCamera"); - control.Primary.HasValue.Should().BeTrue(); - control.Primary!.Value.Key.Should().Be("Key_RightShift"); + control.Primary.HasValue.Should().BeTrue(); + control.Primary!.Value.Key.Should().Be("Key_RightShift"); - control.Secondary.HasValue.Should().BeTrue(); - control.Secondary!.Value.Device.Should().Be("Keyboard"); - control.Secondary!.Value.Key.Should().Be("Key_F12"); - } + control.Secondary.HasValue.Should().BeTrue(); + control.Secondary!.Value.Device.Should().Be("Keyboard"); + control.Secondary!.Value.Key.Should().Be("Key_F12"); + } - [Test] - public void Parse_Should_Handle_Float_Deadzone_Formats() - { - const string xml = @" + [Test] + public void Parse_Should_Handle_Float_Deadzone_Formats() + { + const string xml = @" @@ -294,19 +303,19 @@ public void Parse_Should_Handle_Float_Deadzone_Formats() "; - var controls = BindingParser - .Parse(xml) - .ToDictionary(c => c.Name); + var controls = BindingParser + .Parse(xml) + .ToDictionary(c => c.Name); - controls["RollAxisRaw"].Deadzone.Should().Be(0.0f); - controls["LateralThrustRaw"].Deadzone.Should().Be(0.05f); - controls["VerticalThrustRaw"].Deadzone.Should().Be(7.0f); - } + controls["RollAxisRaw"].Deadzone.Should().Be(0.0f); + controls["LateralThrustRaw"].Deadzone.Should().Be(0.05f); + controls["VerticalThrustRaw"].Deadzone.Should().Be(7.0f); + } - [Test] - public void Parse_Should_Ignore_Value_Only_Elements_And_Only_Return_Controls_With_Bindings() - { - const string xml = @" + [Test] + public void Parse_Should_Ignore_Value_Only_Elements_And_Only_Return_Controls_With_Bindings() + { + const string xml = @" nl-NL @@ -319,11 +328,11 @@ public void Parse_Should_Ignore_Value_Only_Elements_And_Only_Return_Controls_Wit "; - var controls = BindingParser.Parse(xml).ToArray(); + var controls = BindingParser.Parse(xml).ToArray(); - controls.Should().ContainSingle(c => c.Name == "YawLeftButton"); - controls.Select(c => c.Name).Should().NotContain("MouseXMode"); - controls.Select(c => c.Name).Should().NotContain("MouseSensitivity"); - controls.Select(c => c.Name).Should().NotContain("MouseGUI"); - } + controls.Should().ContainSingle(c => c.Name == "YawLeftButton"); + controls.Select(c => c.Name).Should().NotContain("MouseXMode"); + controls.Select(c => c.Name).Should().NotContain("MouseSensitivity"); + controls.Select(c => c.Name).Should().NotContain("MouseGUI"); + } } diff --git a/EliteAPI.Tests/test.binds b/EliteAPI.Tests/test.binds new file mode 100644 index 00000000..97f307b9 --- /dev/null +++ b/EliteAPI.Tests/test.binds @@ -0,0 +1,1895 @@ + + + en-GB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EliteAPI/bindings/Binding.cs b/EliteAPI/bindings/Binding.cs index f0570736..34b4ca11 100644 --- a/EliteAPI/bindings/Binding.cs +++ b/EliteAPI/bindings/Binding.cs @@ -2,17 +2,17 @@ namespace EliteAPI.Bindings; public readonly struct Binding : IBinding { - public string Device { get; init; } + public string Device { get; init; } - public string Key { get; init; } + public string Key { get; init; } - public IBinding[]? Modifiers { get; init; } + public IBinding[]? Modifiers { get; init; } - public Binding(string device, string key, IBinding[]? modifiers = null) - { - Device = device; - Key = key; - Modifiers = modifiers; + public Binding(string device, string key, IBinding[]? modifiers = null) + { + Device = device; + Key = key; + Modifiers = modifiers; - } + } } diff --git a/EliteAPI/bindings/BindingParser.cs b/EliteAPI/bindings/BindingParser.cs index 4294f6ae..8b4459a7 100644 --- a/EliteAPI/bindings/BindingParser.cs +++ b/EliteAPI/bindings/BindingParser.cs @@ -3,123 +3,125 @@ using System.Globalization; using System.Linq; using System.Xml.Linq; -using Newtonsoft.Json; namespace EliteAPI.Bindings; public static class BindingParser { - public static IReadOnlyCollection Parse(string xml) - { - var doc = XDocument.Parse(xml); - var root = doc.Root ?? throw new InvalidOperationException("No root element in binds file."); - - var controls = root.Elements() - .Select(ParseElement) - .ToArray(); - - // Debug output – keep or remove as you like - foreach (var element in controls) - Console.WriteLine(JsonConvert.SerializeObject(element)); - - return controls; - } - - private static Control ParseElement(XElement element) - { - var primaryElement = GetChildCI(element, "Primary") ?? GetChildCI(element, "Binding"); - var secondaryElement = GetChildCI(element, "Secondary"); - - var deadzoneRaw = GetAttributeCI(GetChildCI(element, "Deadzone"), "Value"); - var invertedRaw = GetAttributeCI(GetChildCI(element, "Inverted"), "Value"); - var toggleRaw = GetAttributeCI(GetChildCI(element, "ToggleOn"), "Value"); - - return new Control - { - Name = element.Name.LocalName, - Primary = ParseBinding(primaryElement), - Secondary = ParseBinding(secondaryElement), - Deadzone = ParseFloat(deadzoneRaw), - IsToggle = ParseBool(toggleRaw), - IsInverted = ParseBool(invertedRaw) - }; - } - - /// - /// Parse a single binding element (Binding/Primary/Secondary), including Modifiers, - /// handling casing and "unbound" semantics. - /// - private static Binding? ParseBinding(XElement? element) - { - if (element is null) - return null; - - var device = GetAttributeCI(element, "Device") ?? string.Empty; - var key = GetAttributeCI(element, "Key") ?? string.Empty; - - // Treat unbound bindings (NoDevice / empty key) as null - if (IsUnbound(device, key)) - return null; - - // Parse child elements (case-insensitive) - var modifiers = element - .Elements() - .Where(e => string.Equals(e.Name.LocalName, "Modifier", StringComparison.OrdinalIgnoreCase)) - .Select(m => - { - var mDevice = GetAttributeCI(m, "Device") ?? string.Empty; - var mKey = GetAttributeCI(m, "Key") ?? string.Empty; - return (IBinding)new Binding(mDevice, mKey); - }) - .ToArray(); - - return new Binding(device, key, modifiers.Length == 0 ? null : modifiers); - } - - /// - /// Decide when a binding is "unbound" and should be treated as null. - /// - private static bool IsUnbound(string device, string key) - { - // Classic ED unbound pattern: {NoDevice} + empty key - if (string.Equals(device, "{NoDevice}", StringComparison.OrdinalIgnoreCase)) - return true; - - // No key at all = unbound (even if device is set) - if (string.IsNullOrWhiteSpace(key)) - return true; - - // Super defensive: both empty - if (string.IsNullOrWhiteSpace(device) && string.IsNullOrWhiteSpace(key)) - return true; - - return false; - } - - private static float? ParseFloat(string? raw) => - float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) - ? value - : null; - - private static bool? ParseBool(string? raw) => raw switch - { - "1" => true, - "0" => false, - null => null, - _ => null - }; - - private static XElement? GetChildCI(XElement parent, string name) => - parent.Elements() - .FirstOrDefault(e => string.Equals(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)); - - private static string? GetAttributeCI(XElement? element, string name) - { - if (element is null) - return null; - - return element.Attributes() - .FirstOrDefault(a => string.Equals(a.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)) - ?.Value; - } + public static IReadOnlyCollection Parse(string xml) + { + try + { + var doc = XDocument.Parse(xml); + var root = doc.Root; + + var controls = root.Elements() + .Select(ParseElement) + .ToArray(); + + return controls; + } + catch (Exception e) + { + return []; + } + } + + private static Control ParseElement(XElement element) + { + var primaryElement = GetChildElement(element, "Primary") ?? GetChildElement(element, "Binding"); + var secondaryElement = GetChildElement(element, "Secondary"); + + var deadzoneRaw = GetAttribute(GetChildElement(element, "Deadzone"), "Value"); + var invertedRaw = GetAttribute(GetChildElement(element, "Inverted"), "Value"); + var toggleRaw = GetAttribute(GetChildElement(element, "ToggleOn"), "Value"); + + return new Control + { + Name = element.Name.LocalName, + Primary = ParseBinding(primaryElement), + Secondary = ParseBinding(secondaryElement), + Deadzone = ParseFloat(deadzoneRaw), + IsToggle = ParseBool(toggleRaw), + IsInverted = ParseBool(invertedRaw) + }; + } + + /// + /// Parse a single binding element (Binding/Primary/Secondary), including Modifiers, + /// handling casing and "unbound" semantics. + /// + private static Binding? ParseBinding(XElement? element) + { + if (element is null) + return null; + + var device = GetAttribute(element, "Device") ?? string.Empty; + var key = GetAttribute(element, "Key") ?? string.Empty; + + // Treat unbound bindings (NoDevice / empty key) as null + if (IsUnbound(device, key)) + return null; + + // Parse child elements (case-insensitive) + var modifiers = element + .Elements() + .Where(e => string.Equals(e.Name.LocalName, "Modifier", StringComparison.OrdinalIgnoreCase)) + .Select(m => + { + var mDevice = GetAttribute(m, "Device") ?? string.Empty; + var mKey = GetAttribute(m, "Key") ?? string.Empty; + return (IBinding)new Binding(mDevice, mKey); + }) + .ToArray(); + + return new Binding(device, key, modifiers.Length == 0 ? null : modifiers); + } + + /// + /// Decide when a binding is "unbound" and should be treated as null. + /// + private static bool IsUnbound(string device, string key) + { + // Classic ED unbound pattern: {NoDevice} + empty key + if (string.Equals(device, "{NoDevice}", StringComparison.OrdinalIgnoreCase)) + return true; + + // No key at all = unbound (even if device is set) + if (string.IsNullOrWhiteSpace(key)) + return true; + + // Super defensive: both empty + if (string.IsNullOrWhiteSpace(device) && string.IsNullOrWhiteSpace(key)) + return true; + + return false; + } + + private static float? ParseFloat(string? raw) => + float.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) + ? value + : null; + + private static bool? ParseBool(string? raw) => raw switch + { + "1" => true, + "0" => false, + null => null, + _ => null + }; + + private static XElement? GetChildElement(XElement parent, string name) => + parent.Elements() + .FirstOrDefault(e => string.Equals(e.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)); + + private static string? GetAttribute(XElement? element, string name) + { + if (element is null) + return null; + + return element.Attributes() + .FirstOrDefault(a => string.Equals(a.Name.LocalName, name, StringComparison.OrdinalIgnoreCase)) + ?.Value; + } } diff --git a/EliteAPI/bindings/IBinding.cs b/EliteAPI/bindings/IBinding.cs index ff69b17d..d3fdadf1 100644 --- a/EliteAPI/bindings/IBinding.cs +++ b/EliteAPI/bindings/IBinding.cs @@ -2,8 +2,8 @@ namespace EliteAPI.Bindings; public interface IBinding { - public string Device { get; init; } + public string Device { get; init; } - public string Key { get; init; } + public string Key { get; init; } }