Skip to content

Commit

Permalink
merge default hardware caps with button maps loaded from JSON file si…
Browse files Browse the repository at this point in the history
…nce now the JSON file will not have the complete list of buttons since only the modified ones are serialized
  • Loading branch information
joekolodz committed Sep 19, 2021
1 parent 4b04bae commit d7ffcba
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 58 deletions.
101 changes: 84 additions & 17 deletions src/CustomJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public override bool CanConvert(Type objectType)
{
return objectType == typeof(HOTASButton) ||
objectType == typeof(HOTASAxis) ||
objectType == typeof(ButtonAction) ||
objectType == typeof(IHOTASDevice);
}

Expand Down Expand Up @@ -52,6 +53,21 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
return buttonMap;
}

if (objectType == typeof(ButtonAction))
{
var action = new ButtonAction();
try
{
serializer.Populate(reader, action);
}
catch (Exception e)
{
Logging.Log.Error(e, "Failed to deserialize a ButtonAction");
throw;
}
return action;
}

var dic = new Dictionary<int, ObservableCollection<IHotasBaseMap>>();

var jsonDictionary = JObject.Load(reader);
Expand Down Expand Up @@ -96,49 +112,100 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
return dic;
}


//Don't serialize buttons that don't have any maps
//Don't serialize buttons that don't have any maps or custom changes
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is ButtonAction action)
{
SerializeButtonAction(writer, serializer, action);
return;
}

if (value is HOTASButton button)
{
if ((string.Compare(button.MapName, JoystickOffsetValues.GetName(button.MapId)) == 0 ||
string.Compare(button.MapName, JoystickOffsetValues.GetPOVName(button.MapId)) == 0) &&
(button.ActionCatalogItem == null || button.ActionCatalogItem.Actions == null || button.ActionCatalogItem.Actions.Count == 0)) return;

SerializeButton(writer, serializer, typeof(HOTASButton), button);
if (button.Type == HOTASButton.ButtonType.Button && string.CompareOrdinal(button.MapName, JoystickOffsetValues.GetName(button.MapId)) != 0 ||
button.Type == HOTASButton.ButtonType.POV && string.CompareOrdinal(button.MapName, JoystickOffsetValues.GetPOVName(button.MapId)) != 0 ||
button.ActionCatalogItem?.Actions?.Count > 0 ||
button.IsShift)
{
SerializeButton(writer, serializer, button);
}
return;
}

if (value is HOTASAxis axis)
{
if ((axis.ButtonMap == null || axis.ButtonMap.Count == 0) &&
(axis.ReverseButtonMap == null || axis.ReverseButtonMap.Count == 0)) return;

SerializeButton(writer, serializer, typeof(HOTASAxis), axis);
SerializeAxis(writer, serializer, axis);
return;
}

if ((!(value is Dictionary<int, ObservableCollection<IHotasBaseMap>> profiles))) return;

if (!(value is Dictionary<int, ObservableCollection<IHotasBaseMap>>)) return;

serializer.Serialize(writer, value);
}

private static void SerializeButton(JsonWriter writer, JsonSerializer serializer, Type t, IHotasBaseMap axis)
private static void SerializeButton(JsonWriter writer, JsonSerializer serializer, HOTASButton button)
{
var propList = t.GetProperties();
var propList = typeof(HOTASButton).GetProperties();
writer.WriteStartObject();
foreach (var prop in propList)
{
if (prop.GetCustomAttributes(true).Any(x => x is JsonIgnoreAttribute))
{
continue;
}
if (prop.GetCustomAttributes(true).Any(x => x is JsonIgnoreAttribute)) continue;

var value = prop.GetValue(button);

if (prop.Name == nameof(button.ShiftModePage) && (int)prop.GetValue(button) == 0) continue;
if (prop.Name == nameof(button.IsShift) && (bool)prop.GetValue(button) == false) continue;

writer.WritePropertyName(prop.Name);
serializer.Serialize(writer, prop.GetValue(axis));
serializer.Serialize(writer, value);
}
writer.WriteEndObject();
}

private static void SerializeAxis(JsonWriter writer, JsonSerializer serializer, HOTASAxis axis)
{
var propList = typeof(HOTASAxis).GetProperties();
writer.WriteStartObject();
foreach (var prop in propList)
{
if (prop.GetCustomAttributes(true).Any(x => x is JsonIgnoreAttribute)) continue;

var value = prop.GetValue(axis);

if (prop.Name == nameof(axis.IsDirectional) && (bool)prop.GetValue(axis) == true) continue;
if (prop.Name == nameof(axis.IsMultiAction) && (bool)prop.GetValue(axis) == false) continue;
if (prop.Name == nameof(axis.SoundFileName) && string.IsNullOrEmpty((string)prop.GetValue(axis))) continue;
if (prop.Name == nameof(axis.SoundVolume) && Math.Abs((double)prop.GetValue(axis) - 1.0d) < 0.01) continue;

writer.WritePropertyName(prop.Name);
serializer.Serialize(writer, value);
}
writer.WriteEndObject();
}

private static void SerializeButtonAction(JsonWriter writer, JsonSerializer serializer, ButtonAction action)
{
var propList = typeof(ButtonAction).GetProperties();
writer.WriteStartObject();
foreach (var prop in propList)
{
if (prop.GetCustomAttributes(true).Any(x => x is JsonIgnoreAttribute)) continue;

var value = prop.GetValue(action);

if (prop.Name == nameof(action.IsKeyUp) && (bool)prop.GetValue(action) == false) continue;
if (prop.Name == nameof(action.IsExtended) && (bool)prop.GetValue(action) == false) continue;
if (prop.Name == nameof(action.TimeInMilliseconds) && (int)prop.GetValue(action) == 0) continue;

writer.WritePropertyName(prop.Name);
serializer.Serialize(writer, value);
}
writer.WriteEndObject();
}
}
}

4 changes: 2 additions & 2 deletions src/DataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static ObservableCollection<IHOTASDevice> GetDeviceList()
new HOTASDevice(di, new JoystickFactory(), productGuid,stickGuid, "Joe's Test Stick", new HOTASQueue())
};

devices[0].SetButtonMap(new ObservableCollection<IHotasBaseMap>()
devices[0].ApplyButtonMap(new ObservableCollection<IHotasBaseMap>()
{
new HOTASButton()
{
Expand All @@ -45,7 +45,7 @@ public static ObservableCollection<IHOTASDevice> GetDeviceList()
new HOTASAxis() {MapName = "Axis 2", Type = HOTASButton.ButtonType.AxisRadial, MapId = 55, IsDirectional = true}
});

devices[1].SetButtonMap(new ObservableCollection<IHotasBaseMap>()
devices[1].ApplyButtonMap(new ObservableCollection<IHotasBaseMap>()
{
new HOTASAxis() { MapName = "Axis 1", Type = HOTASButton.ButtonType.AxisLinear, MapId = 42, IsDirectional = true },
new HOTASAxis() { MapName = "Axis 2", Type = HOTASButton.ButtonType.AxisRadial, MapId = 43, IsDirectional = false },
Expand Down
1 change: 0 additions & 1 deletion src/Models/FileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ private static void BaseSave(string fileName, IHOTASCollection deviceList)
try
{
_fileIo.WriteAllText(fileName, JsonConvert.SerializeObject(deviceList, Formatting.Indented, new CustomJsonConverter()));

}
catch (Exception e)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Models/HOTASAxis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class HOTASAxis : IHotasBaseMap
public bool IsDirectional { get; set; } = true;
public bool IsMultiAction { get; set; } = false;
public string SoundFileName { get; set; }
public double SoundVolume { get; set; }
public double SoundVolume { get; set; } = 1.0d;
public ObservableCollection<Segment> Segments { get; set; }

[JsonIgnore]
Expand All @@ -31,14 +31,14 @@ public class HOTASAxis : IHotasBaseMap
private int _lastAvg;
private readonly int[] _previousValues;

[JsonIgnore]
public AxisDirection Direction { get; private set; }

public HOTASAxis()
{
Segments = new ObservableCollection<Segment>();
ButtonMap = new ObservableCollection<HOTASButton>();
ReverseButtonMap = new ObservableCollection<HOTASButton>();
SoundVolume = 1.0d;
_previousValues = new int[_arraySize];
}

Expand Down
25 changes: 16 additions & 9 deletions src/Models/HOTASCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,18 @@ public void ReplaceDevice(IHOTASDevice newDevice)

private void RebuildMapForNewDevice(IHOTASDevice device, IHOTASDevice newDevice)
{
newDevice.SetButtonMap(device.ButtonMap.ToObservableCollection());
newDevice.ApplyButtonMap(device.ButtonMap.ToObservableCollection());
newDevice.SetModeProfile(device.ModeProfiles);
Devices.Add(newDevice);
}

public void Start()
{
LoadAllDevices();
if (!App.IsDebug)
{
Devices = RefreshMissingDevices();
}

ListenToAllDevices();
}

Expand Down Expand Up @@ -128,13 +132,12 @@ public void ClearButtonMap()
}
}

/// <summary>
/// Get all devices attached and load their custom button map profiles
/// </summary>
private void LoadAllDevices()
public void ApplyButtonMapToAllProfiles()
{
if (App.IsDebug) return;
Devices = GetHOTASDevices();
foreach (var d in Devices)
{
d.OverlayAllProfilesToDevice();
}
}

public void ListenToAllDevices()
Expand Down Expand Up @@ -270,7 +273,11 @@ public void ClearUnassignedActions()
}
}

public ObservableCollection<IHOTASDevice> GetHOTASDevices()
/// <summary>
/// Rescan all devices from DirectInput. Any devices scanned that are not already in the Device list will be added
/// </summary>
/// <returns></returns>
public ObservableCollection<IHOTASDevice> RefreshMissingDevices()
{
var rescannedDevices = _directInput.GetDevices(DeviceClass.GameControl, DeviceEnumerationFlags.AttachedOnly);

Expand Down
48 changes: 38 additions & 10 deletions src/Models/HOTASDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void SetModeProfile(Dictionary<int, ObservableCollection<IHotasBaseMap>>
{
ModeProfiles = profile;
if (ModeProfiles.Count < 1) return;
SetButtonMap(profile[ModeProfiles.Keys.Min()].ToObservableCollection());
ApplyButtonMap(profile[ModeProfiles.Keys.Min()].ToObservableCollection());
}

//todo pass in the mode profile key for the profile you want to template from
Expand Down Expand Up @@ -233,14 +233,47 @@ private void RemoveQueueHandlers()
_hotasQueue.LostConnectionToDevice -= OnLostConnectionToDevice;
}

public void SetButtonMap(ObservableCollection<IHotasBaseMap> existingButtonMap)
public void OverlayAllProfilesToDevice()
{
var mergedModeProfiles = new Dictionary<int, ObservableCollection<IHotasBaseMap>>();
var deviceButtons = new ObservableCollection<IHotasBaseMap>();
SeedButtonMapProfileFromDeviceCapabilities(deviceButtons);

foreach (var p in ModeProfiles)
{
if (p.Value.Count == 0) continue;
var d = deviceButtons.ToObservableCollection();//make a copy since we could have more than 1 profile and we don't need to rescan caps
mergedModeProfiles.Add(p.Key, MergeMaps(d, p.Value));
}

ModeProfiles = mergedModeProfiles;
}

private ObservableCollection<IHotasBaseMap> MergeMaps(ObservableCollection<IHotasBaseMap> sourceMap, ObservableCollection<IHotasBaseMap> destinationMap)
{
var merged = new ObservableCollection<IHotasBaseMap>();
foreach (var source in sourceMap)
{
var i = destinationMap.FirstOrDefault(b => b.MapId == source.MapId);
merged.Add(i ?? source);
}
return merged;
}


/// <summary>
/// Re-seed the device before overlaying the given button map
/// </summary>
/// <param name="existingButtonMap"></param>
public void ApplyButtonMap(ObservableCollection<IHotasBaseMap> existingButtonMap)
{
if (IsDeviceLoaded)
{
//rebuild the button map in this manner, because additional buttons may be recognized at a later time
//for instance, the virpil side cars can be linked to an existing device which makes that original device look like it has more buttons.
//Reset the button map back to default hardware capabilities and then overlay the given button map on top
//We rebuild the button map in this manner, because additional hardware buttons may be recognized at a later time as well as the fact that the map loaded from the JSON file only has modified buttons
//For instance, the virpil side cars can be linked to an existing device which makes that original device look like it has more buttons.
//existingButtonMap is assumed to have less button entries in this scenario. So we want to copy the data from existingButtonMap for any buttons that match between the two lists
//any buttons that don't match means that the device has more buttons than are in the existingButtonMap list and so there is nothing to copy
//Any buttons that don't match means that the device has more buttons than are in the existingButtonMap list and so there is nothing to copy
//If the device has fewer buttons than exist on the existingButtonMap, then those buttons are not copied over and lost
ButtonMap = new ObservableCollection<IHotasBaseMap>();
SeedButtonMapProfileFromDeviceCapabilities(ButtonMap);
Expand Down Expand Up @@ -279,11 +312,6 @@ public void ForceButtonPress(JoystickOffset offset, bool isDown)
private void LoadCapabilitiesMapping()
{
LoadCapabilities();
SeedButtonMapProfileFromDeviceCapabilities();
}

private void SeedButtonMapProfileFromDeviceCapabilities()
{
SeedButtonMapProfileFromDeviceCapabilities(ButtonMap);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Models/IHOTASCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ public interface IHOTASCollection
void Start();
void Stop();
void ClearButtonMap();
void ApplyButtonMapToAllProfiles();
void ListenToAllDevices();
void ListenToDevice(IHOTASDevice device);
int SetupNewModeProfile();
void CopyModeProfileFromTemplate(int templateModeSource, int destinationMode);
void ForceButtonPress(IHOTASDevice device, JoystickOffset offset, bool isDown);
IHOTASDevice GetDevice(Guid instanceId);
void ClearUnassignedActions();
ObservableCollection<IHOTASDevice> GetHOTASDevices();
ObservableCollection<IHOTASDevice> RefreshMissingDevices();

/// <summary>
/// Activate the profile for the given Mode
Expand Down
3 changes: 2 additions & 1 deletion src/Models/IHOTASDevice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public interface IHOTASDevice
void CopyModeProfileFromTemplate(int templateModeSource, int destinationMode);
//void ReAcquireJoystick();
void ListenAsync();
void SetButtonMap(ObservableCollection<IHotasBaseMap> existingButtonMap);
void OverlayAllProfilesToDevice();
void ApplyButtonMap(ObservableCollection<IHotasBaseMap> existingButtonMap);
void SetMode(int mode);
void ForceButtonPress(JoystickOffset offset, bool isDown);
void Stop();
Expand Down
4 changes: 3 additions & 1 deletion src/ViewModels/HOTASCollectionViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ private void DeviceList_AxisChanged(object sender, AxisChangedEventArgs e)

private void RefreshDeviceList()
{
var newDevices = _deviceList.GetHOTASDevices();
var newDevices = _deviceList.RefreshMissingDevices();

//if the device has a mapping already loaded, then assign this device to that mapping
foreach (var deviceViewModel in Devices)
Expand Down Expand Up @@ -516,6 +516,8 @@ private void LoadHotas(IHOTASCollection loadedDeviceList)
AddHandlers();
ProfileSetFileName = _fileSystem.LastSavedFileName;

//since the JSON file may have less button maps than are on the device, we need to overlay them individually instead of replacing the whole collection
_deviceList.ApplyButtonMapToAllProfiles();
_deviceList.AutoSetMode();
_deviceList.ListenToAllDevices();

Expand Down
6 changes: 3 additions & 3 deletions tests/DeviceViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public void clear_button_map()
};
list.Add(testButton);

hotasDevice.SetButtonMap(list);
hotasDevice.ApplyButtonMap(list);

deviceVm.RebuildMap();
Assert.NotEmpty(testButton.ActionCatalogItem.Actions);
Expand All @@ -298,7 +298,7 @@ public void rebuild_map_no_param()
list.Add(new HOTASButton() { MapId = 48, Type = HOTASButton.ButtonType.Button, ActionCatalogItem = new ActionCatalogItem() { Actions = new ObservableCollection<ButtonAction>() { new ButtonAction() } }, ActionName = "first" });
list.Add(new HOTASButton() { MapId = 49, Type = HOTASButton.ButtonType.Button, ActionCatalogItem = new ActionCatalogItem() { Actions = new ObservableCollection<ButtonAction>() { new ButtonAction() } }, ActionName = "second" });

hotasDevice.SetButtonMap(list);
hotasDevice.ApplyButtonMap(list);
deviceVm.ButtonMap.Add(new ButtonMapViewModel() { ButtonId = 1 });
Assert.Single(deviceVm.ButtonMap);

Expand Down Expand Up @@ -391,7 +391,7 @@ public void replace_device()

var list = new ObservableCollection<IHotasBaseMap>();
list.Add(new HOTASButton() { MapId = 48, Type = HOTASButton.ButtonType.Button, ActionCatalogItem = new ActionCatalogItem() { Actions = new ObservableCollection<ButtonAction>() { new ButtonAction() } }, ActionName = "first" });
expectedHotasDevice.SetButtonMap(list);
expectedHotasDevice.ApplyButtonMap(list);

Assert.Equal(originalHotasDevice.Name, deviceVm.Name);
Assert.Equal(originalHotasDevice.DeviceId, deviceVm.InstanceId);
Expand Down
Loading

0 comments on commit d7ffcba

Please sign in to comment.