Skip to content

Commit 0e75615

Browse files
committed
✨ player settings
semver: minor
1 parent b858e73 commit 0e75615

8 files changed

Lines changed: 384 additions & 2 deletions

File tree

EliteAPI.Tests/EliteAPI.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
<None Update="TestFiles\Audio\Custom.4.3.audio">
3333
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
3434
</None>
35+
<None Update="TestFiles\Player\Custom.4.3.misc">
36+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
37+
</None>
3538
</ItemGroup>
3639

3740
</Project>

EliteAPI.Tests/PlayerTests.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using EliteAPI.Options.Player;
2+
using FluentAssertions;
3+
4+
namespace EliteAPI.Tests.Player;
5+
6+
public class PlayerParserTests
7+
{
8+
[Fact]
9+
public void Parse_Sample_File()
10+
{
11+
string xml = File.ReadAllText("TestFiles/Player/Custom.4.3.misc");
12+
13+
var content = PlayerParser.Parse(xml);
14+
15+
content.Should().NotBeNull();
16+
content.Should().NotBeEmpty();
17+
}
18+
19+
[Fact]
20+
public void Parse_Should_Read_String_And_Numeric_Values_As_Raw_Strings()
21+
{
22+
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8"" ?>
23+
<Root PresetName=""Custom"" MajorVersion=""4"" MinorVersion=""3"">
24+
<GunsightMode Value=""Bindings_TraditionalGunsights"" />
25+
<DashboardGUIBrightness Value=""0.69000000"" />
26+
<ClockEnabled Value=""1"" />
27+
</Root>";
28+
29+
var settings = PlayerParser.Parse(xml).ToDictionary(s => s.Name);
30+
31+
settings["GunsightMode"].Value.Should().Be("Bindings_TraditionalGunsights");
32+
settings["DashboardGUIBrightness"].Value.Should().Be("0.69000000");
33+
settings["ClockEnabled"].Value.Should().Be("1");
34+
}
35+
36+
[Fact]
37+
public void Parse_Should_Skip_Elements_Without_Value_Attribute()
38+
{
39+
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8"" ?>
40+
<Root>
41+
<ClockEnabled Value=""1"" />
42+
<VisibilityFlags />
43+
<QuickItems></QuickItems>
44+
<Language Value=""English"" />
45+
</Root>";
46+
47+
var settings = PlayerParser.Parse(xml).ToArray();
48+
49+
settings.Should().HaveCount(2);
50+
settings.Select(s => s.Name).Should().BeEquivalentTo(new[] { "ClockEnabled", "Language" });
51+
}
52+
53+
[Fact]
54+
public void Parse_Should_Return_Empty_For_Malformed_Xml()
55+
{
56+
var settings = PlayerParser.Parse("not valid xml <<<");
57+
58+
settings.Should().NotBeNull();
59+
settings.Should().BeEmpty();
60+
}
61+
62+
[Fact]
63+
public void Parse_Real_Preset_Contains_Known_Settings()
64+
{
65+
string xml = File.ReadAllText("TestFiles/Player/Custom.4.3.misc");
66+
67+
var settings = PlayerParser.Parse(xml).ToDictionary(s => s.Name);
68+
69+
settings.Should().ContainKey("Language");
70+
settings.Should().ContainKey("ClockEnabled");
71+
settings.Should().ContainKey("GunsightMode");
72+
}
73+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<Root PresetName="Custom" MajorVersion="4" MinorVersion="3">
3+
<GunsightMode Value="Bindings_TraditionalGunsights" />
4+
<DashboardGUIBrightness Value="0.69000000" />
5+
<ReportCrimesAgainstMe Value="1" />
6+
<Language Value="English" />
7+
<LanguageOverrideActive Value="0" />
8+
<SensorScaleType Value="Logarithmic" />
9+
<PilotIsFemale Value="0" />
10+
<PreFlightChecksActive Value="0" />
11+
<DisableUiCameraLockOn Value="0" />
12+
<OrbitLinesEnabled Value="0" />
13+
<ClockEnabled Value="1" />
14+
<Dethrottle Value="1" />
15+
<AutoDockEnabled Value="1" />
16+
<AutoLaunchEnabled Value="1" />
17+
<AutoLandEnabled Value="1" />
18+
<PreferredWeaponMode Value="2" />
19+
<HidePresence Value="0" />
20+
<OutfittingSortingPreference Value="3" />
21+
<FirstTimeMultiCrew Value="1" />
22+
<FirstTimeMultiCrewFireCon Value="1" />
23+
<MultiCrewFighterConEnabled Value="1" />
24+
<MultiCrewFireConLimited Value="0" />
25+
<MultiCrewVehicleConEnabled Value="1" />
26+
<WingBeaconEnabled Value="1" />
27+
<WingBoardingEnabled Value="1" />
28+
<DisableIdleHandAnimations Value="0" />
29+
<AutoCruisethrottleRelevant Value="0" />
30+
<HideLocationIcons Value="0" />
31+
<StarNames Value="1" />
32+
<RegionNames Value="1" />
33+
<TradeRoutes Value="1" />
34+
<Constellations Value="0" />
35+
<ShowGrid Value="1" />
36+
<NavigationMarkers Value="1" />
37+
<routePlottingMode Value="1" />
38+
<routePlottingVirtualCargoMass Value="0" />
39+
<routePlottingRealCargoMass Value="0" />
40+
<NebulaNames Value="1" />
41+
<RealisticView Value="0" />
42+
<ColourVisIndex Value="2" />
43+
<SizeVisIndex Value="0" />
44+
<FriendMarkers Value="1" />
45+
<WingMarkers Value="1" />
46+
<MissionMarkers Value="1" />
47+
<CommunityGoalMarkers Value="1" />
48+
<ShipMarkers Value="1" />
49+
<EngineerMarkers Value="1" />
50+
<PoliticsMode Value="0" />
51+
<PoliticsTerritoryType Value="0" />
52+
<PoliticsTerritoryModePowersFilter Value="-1" />
53+
<PoliticsTerritoryModeStatesFilter Value="-1" />
54+
<PoliticsTerritoryActivityFilterPower Value="-1" />
55+
<PoliticsTerritoryActivityFilterAction Value="0" />
56+
<PoliticsTerritoryActivityFilterFilterMinActivity Value="0" />
57+
<BookmarkMarkers Value="1" />
58+
<TypeDMarkers Value="0" />
59+
<TypeRMarkers Value="0" />
60+
<NavJetConeBoost Value="0" />
61+
<NavigationPreviousRoute Value="1" />
62+
<CommodityIndex Value="0" />
63+
<CommodityPriceMode Value="0" />
64+
<TradeCategory Value="3" />
65+
<ServicesOption Value="0" />
66+
<SquadronBookmarkMarkers Value="0" />
67+
<GalacticZonesPlane Value="1" />
68+
<GalacticZoneNames Value="1" />
69+
<StarterZoneMarkers Value="1" />
70+
<FleetCarrierMarkers Value="1" />
71+
<ConflictZoneMarkers Value="1" />
72+
<DryDockMarkers Value="0" />
73+
<TypeUMarkers Value="1" />
74+
<TypeAMarkers Value="0" />
75+
<MaelstromMarkers Value="1" />
76+
<RescueMegashipMarkers Value="1" />
77+
<ThargoidFrontlineMarkers Value="1" />
78+
<PowerActivityMarkers Value="1" />
79+
<ShowControls Value="1" />
80+
<ShowSystemConnectors Value="1" />
81+
<PoliticsTerritoryActivityFilterMinActivity Value="0.00000000" />
82+
<RouteStartSystem Value="3657399571162" />
83+
<RouteDestinationSystem Value="0" />
84+
<RouteDestinationBody Value="0" />
85+
<RouteDestinationMarketID Value="18446744073709551615" />
86+
<RouteDestinationBodysiteID Value="18446744073709551615" />
87+
<RouteDestinationIsCluster Value="0" />
88+
<RouteDestinationIsSurfaceSettlement Value="0" />
89+
<VisibilityFlags />
90+
<VisibilityValues />
91+
<TextChannelOptions>
92+
<VisibleChannels>
93+
<CHAT_notifications Value="2" />
94+
<COMMS_voice_messages Value="2" />
95+
<CHAT_system Value="1" />
96+
<CHAT_group_wing_and_multicrew Value="2" />
97+
<COMMS_direct_messages Value="2" />
98+
<CHAT_group_local Value="2" />
99+
</VisibleChannels>
100+
</TextChannelOptions>
101+
<VoiceChatPanelLocation Value="TopCentre" />
102+
<ImportExportPriceMode Value="GalacticAverage" />
103+
<ImportExportSystemAddress Value="0" />
104+
<ImportExportMarketID Value="18446744073709551615" />
105+
<ImportExportShowImportPrices Value="1" />
106+
<HelpPopupViewedFSS Value="1" />
107+
<HelpPopupViewedSAA Value="0" />
108+
<HelpPopupViewedAXMap Value="0" />
109+
<HelpPopupViewedPowerplayMap Value="0" />
110+
<HelpPopupViewedPowerplayHub Value="0" />
111+
<HelpPopupViewedPowerplayPledge Value="0" />
112+
<HelpPopupViewedColonisationContact Value="0" />
113+
<HelpPopupViewedColonisationContactMap Value="0" />
114+
<HelpPopupViewedColonisationSystemMap Value="0" />
115+
<HelpPopupViewedColonisationPlacementCamera Value="0" />
116+
<HelpPopupViewedSquadronNonMemberHome Value="0" />
117+
<HelpPopupViewedSquadronMemberHome Value="0" />
118+
<HelpPopupViewedSquadronRoster Value="0" />
119+
<HelpPopupViewedSquadronApplicationManagement Value="0" />
120+
<HelpPopupViewedSquadronPlayerApplications Value="0" />
121+
<HelpPopupViewedSquadronRanksAndPermissions Value="0" />
122+
<HelpPopupViewedSquadronManagement Value="0" />
123+
<HelpPopupViewedSquadronBank Value="0" />
124+
<HelpPopupViewedSquadronLeaderboard Value="0" />
125+
<CurrentHUDMode Value="Normal" />
126+
<MarketFilter_inCargo Value="1" />
127+
<MarketFilter_requiredForMission Value="1" />
128+
<MarketFilter_highDemand Value="1" />
129+
<MarketFilter_rareGoods Value="1" />
130+
<MarketFilter_commodTypeFlags Value="4294967295" />
131+
<MarketPopupExp_Buy Value="1" />
132+
<MarketPopupExp_Sell Value="1" />
133+
<QuickItems>
134+
<Slot0 Value="3590857428" />
135+
<Slot1 Value="2972124150" />
136+
<Slot2 Value="3821282906" />
137+
<Slot3 Value="1717417831" />
138+
<Slot5 Value="3644429595" />
139+
<Slot6 Value="1371712943" />
140+
<Slot7 Value="2520689613" />
141+
</QuickItems>
142+
<AgeCertification Value="2" />
143+
<ShowMissionAcceptWarning Value="1" />
144+
<ShowDescriptionsInFactionsPanel Value="1" />
145+
<PowerplaySystemFilterGroups>
146+
<Group0 Value="4294967295" />
147+
<Group1 Value="4294967295" />
148+
<Group2 Value="4294967295" />
149+
</PowerplaySystemFilterGroups>
150+
<PowerplaySystemSortField Value="0" />
151+
</Root>

EliteAPI/EliteDangerousApi.cs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using EliteAPI.Events;
66
using EliteAPI.Options.Audio;
77
using EliteAPI.Options.Bindings;
8+
using EliteAPI.Options.Player;
89
using EliteAPI.Journals;
910
using EliteAPI.Json;
1011
using EliteAPI.Utils;
@@ -19,6 +20,7 @@ public class EliteDangerousApi
1920
private readonly List<FileWatcher> _statusWatchers;
2021
private readonly FileWatcher _bindingsPresetsWatcher;
2122
private readonly FileWatcher _audioPresetsWatcher;
23+
private readonly FileWatcher _playerPresetsWatcher;
2224
private readonly StatusTracker _statusTracker = new();
2325

2426
private readonly List<Action<FileInfo>> _journalChangedHandlers = [];
@@ -33,18 +35,19 @@ public class EliteDangerousApi
3335
private readonly Dictionary<string, List<Action<(string eventName, string json)>>> _untypedEventHandlers = new(StringComparer.OrdinalIgnoreCase);
3436
private readonly List<Action<IReadOnlyCollection<Control>>> _bindingsHandlers = [];
3537
private readonly List<Action<IReadOnlyCollection<AudioSetting>>> _audioHandlers = [];
38+
private readonly List<Action<IReadOnlyCollection<PlayerSetting>>> _playerHandlers = [];
3639

3740
public Version Version => typeof(EliteDangerousApi).Assembly.GetName().Version!;
3841

39-
public EliteDangerousApi() : this(JournalUtils.GetJournalsDirectory(), BindingsUtils.GetBindingsDirectory(), AudioUtils.GetAudioDirectory())
42+
public EliteDangerousApi() : this(JournalUtils.GetJournalsDirectory(), BindingsUtils.GetBindingsDirectory(), AudioUtils.GetAudioDirectory(), PlayerUtils.GetPlayerDirectory())
4043
{
4144
}
4245

4346
/// <summary>
4447
/// Creates a new instance of the API with custom directories.
4548
/// Pass null for directories to skip file watcher initialization (useful for testing).
4649
/// </summary>
47-
public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? bindingsDirectory, DirectoryInfo? audioDirectory = null)
50+
public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? bindingsDirectory, DirectoryInfo? audioDirectory = null, DirectoryInfo? playerDirectory = null)
4851
{
4952
if (journalDirectory != null)
5053
{
@@ -89,6 +92,15 @@ public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? binding
8992
{
9093
_audioPresetsWatcher = null!;
9194
}
95+
96+
if (playerDirectory != null)
97+
{
98+
_playerPresetsWatcher = FileWatcher.Create(playerDirectory, "*.misc", FileWatchMode.EntireFile);
99+
}
100+
else
101+
{
102+
_playerPresetsWatcher = null!;
103+
}
92104
}
93105

94106
public void Start()
@@ -125,6 +137,12 @@ public void Start()
125137
_audioPresetsWatcher.OnContentChanged(HandleAudioContent);
126138
HandleAudioContent(_audioPresetsWatcher.StartWatching());
127139
}
140+
141+
if (_playerPresetsWatcher != null)
142+
{
143+
_playerPresetsWatcher.OnContentChanged(HandlePlayerContent);
144+
HandlePlayerContent(_playerPresetsWatcher.StartWatching());
145+
}
128146
}
129147

130148
/// <summary>
@@ -151,6 +169,14 @@ public void OnAudioSettingsChanged(Action<IReadOnlyCollection<AudioSetting>> han
151169
_audioHandlers.Add(handler);
152170
}
153171

172+
/// <summary>
173+
/// Listens for when player settings have changed
174+
/// </summary>
175+
public void OnPlayerSettingsChanged(Action<IReadOnlyCollection<PlayerSetting>> handler)
176+
{
177+
_playerHandlers.Add(handler);
178+
}
179+
154180
/// <summary>
155181
/// Listens for a specific event of <typeparamref name="TEvent"/>.
156182
/// </summary>
@@ -354,4 +380,19 @@ private void HandleAudioContent(string xml)
354380
foreach (var handler in _audioHandlers)
355381
SafeInvoke.Invoke("handling audio settings change", handler, settings);
356382
}
383+
384+
private void HandlePlayerContent(string xml)
385+
{
386+
if (string.IsNullOrWhiteSpace(xml))
387+
return;
388+
389+
var settings = PlayerParser.Parse(xml);
390+
391+
Log.Info($"Loaded {settings.Count} player settings");
392+
foreach (var setting in settings)
393+
Log.Debug($" {setting.ToDebugString()}");
394+
395+
foreach (var handler in _playerHandlers)
396+
SafeInvoke.Invoke("handling player settings change", handler, settings);
397+
}
357398
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Xml.Linq;
5+
6+
namespace EliteAPI.Options.Player;
7+
8+
public static class PlayerParser
9+
{
10+
public static IReadOnlyCollection<PlayerSetting> Parse(string xml)
11+
{
12+
try
13+
{
14+
var doc = XDocument.Parse(xml);
15+
var root = doc.Root;
16+
17+
if (root is null)
18+
return [];
19+
20+
var settings = root.Elements()
21+
.Select(ParseElement)
22+
.Where(s => s.HasValue)
23+
.Select(s => s!.Value)
24+
.ToArray();
25+
26+
return settings;
27+
}
28+
catch
29+
{
30+
return [];
31+
}
32+
}
33+
34+
private static PlayerSetting? ParseElement(XElement element)
35+
{
36+
var value = GetAttribute(element, "Value");
37+
if (value is null)
38+
return null;
39+
40+
return new PlayerSetting
41+
{
42+
Name = element.Name.LocalName,
43+
Value = value
44+
};
45+
}
46+
47+
private static string? GetAttribute(XElement? element, string name)
48+
{
49+
if (element is null)
50+
return null;
51+
52+
return element.Attributes()
53+
.FirstOrDefault(a => string.Equals(a.Name.LocalName, name, StringComparison.OrdinalIgnoreCase))
54+
?.Value;
55+
}
56+
}

0 commit comments

Comments
 (0)