x
•using System.Xml;
using System.Xml.Serialization;
using DoUrVerseLib.Device;
using DoUrVerseLib.Errors;
using DoUrVerseLib.Config;
using System.Reflection;
using System.Xml.Schema;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using System;
using static System.Collections.Specialized.BitVector32;
using System.IO;
//TODO: Remove this cyclic reference (viewmodels should be in this DoUrVerseLib
namespace DoUrVerseLib.SCXML;
public class ProfileChangedEventArgs : EventArgs
{
public string Setting;
public object Value;
public ProfileChangedEventArgs(string setting, object value)
{
Setting = setting;
Value = value;
}
}
public enum Operation { Unknown, Saved, Loaded };
public class ProfileEventArgs
{
bool success = false;
public bool Success { get { return success; } }
public string Filename { get; private set; }
public Operation Operation { get; private set; }
public ProfileEventArgs(bool success, Operation operation, string filename)
{
this.success = success;
Filename = filename;
Operation = operation;
}
}
public delegate void ProfileEventHandler(object sender, ProfileEventArgs args);
public delegate void OnProfileChangedEventHandler(object sender, ProfileChangedEventArgs args);
public class Profile : ICloneable
{
XmlDocument root;
public const string DEFAULT_PROFILE_NAME = "controls copy";
public static Profile Instance { get; private set; } = new();
static Profile? _defaultProfile;
public static Profile None = new(null);
public static event ProfileEventHandler? ProfileLoaded;
public static event ProfileEventHandler? ProfileSaved;
public static event OnProfileChangedEventHandler? ProfileChanged;
public bool IsDefaultProfile { get; set; } = false;
public bool IsChanged { get; private set; } = false;
string profileName = string.Empty;
public string ProfileName {
get => profileName;
set
{
profileName = value;
if (ProfileChanged != null && this == Instance)
{
IsChanged = true;
IsDefaultProfile = false;
ProfileChanged(this,new ProfileChangedEventArgs(nameof(ProfileName), value));
}
}
}
List<DeviceInstance> instances { get; set; } = new();
List<DeviceOptions> deviceoptions { get; set;} = new();
List<ActionMap> actionMaps { get; set; } = new();
List<DeviceInstance> foundDevices { get; set; } = new();
List<DeviceInstance> missingDevices { get; set; } = new();
List<IDevice> unknownDevices { get; set; } = new();
List<DeviceInstance> eligibleDevices = new();
public string WritePath { get; private set; }
public Profile() {}
private Profile(object _)
{
}
public static void Read(string path)
{
var ext = Path.GetExtension(path);
if (ext != ".xml")
{
throw new FileLoadException("Supplied file was not a XML file");
}
if (!File.Exists(path))
{
throw new FileNotFoundException(path);
}
using (Stream stream = File.Open(path, FileMode.Open))
{
Read(stream, path);
}
}
/// <summary>
/// Sets profile to be the default profile
/// </summary>
public static void LoadDefault()
{
if (_defaultProfile != null)
{
Instance = (Profile)_defaultProfile.Clone();
Instance.ProfileName = DEFAULT_PROFILE_NAME;
Instance.IsDefaultProfile = true;
ProfileLoaded(Instance, new ProfileEventArgs(true, Operation.Loaded, string.Empty));
}
}
/// <summary>
/// Read a default profile to be compared with player profiles
/// </summary>
/// <param name="stream"></param>
public static void ReadDefault(Stream stream)
{
_defaultProfile = read(stream);
foreach (var map in _defaultProfile.actionMaps)
foreach (var act in map.Actions)
foreach (var rb in act.Rebinds)
rb.IsDefaultKeybind = true;
//_defaultProfile.addDefaultKeybinds();
}
/// <summary>
/// Reads an exported Star Citizen advanced controls profile
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public static void Read(Stream stream, string path)
{
try {
Instance = read(stream);
Instance.addDefaultKeybinds();
Instance.IsChanged = false;
ProfileLoaded(Instance, new ProfileEventArgs(true, Operation.Loaded, path));
}
catch (Exception ex)
{
NotifyUser.Error("Unable to load new profile", ex);
}
}
static Profile read(Stream stream)
{
XmlDocument doc = new();
doc.Load(stream);
validate(doc);
var profile = new Profile();
profile.root = doc;
profile.extract();
return profile;
}
void addDefaultKeybinds()
{
if (_defaultProfile == null)
{
return;
}
int added = 0;
foreach (var actionMap in _defaultProfile.actionMaps)
{
if (Instance.AddActionMapIfMissing(actionMap))
{
continue;
}
foreach (var action in actionMap.Actions)
{
if (Instance.AddActionIfMissing(actionMap.Name, action))
{
continue;
}
foreach (var rebind in action.Rebinds)
{
rebind.IsDefaultKeybind = true;
if (Instance.AddRebindIfMissing(actionMap.Name, action.Name, rebind))
{
added++;
}
}
}
}
}
bool checkIfDefaultKeybind(string actionMap, string action, Rebind rebind)
{
return rebind.Value == GetDefaultSubstitute(actionMap, action, rebind)?.Value;
}
static void validate(XmlDocument doc)
{
if (Config.Config.Instance.Settings.EnableExperimentalXsdValidation)
{
var assembly = Assembly.GetExecutingAssembly();
string resourceName = assembly.GetManifestResourceNames()
.Single(str => str.EndsWith("export-layout-319.xsd"));
using (Stream? stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new FileNotFoundException(resourceName);
}
XmlReader reader = XmlReader.Create(stream);
doc.Schemas.Add(null, reader);
try
{
doc.Validate(null);
}
catch (XmlSchemaValidationException ex)
{
throw new InvalidStarCitizenLayout(ex.Message);
}
}
}
/*if (node == null)
throw new InvalidStarCitizenLayout();*/
}
public XmlDocument GetDoc()
{
XmlDocument newDoc = new();
XmlNode r = root.SelectSingleNode("/ActionMaps");
XmlNode copy = newDoc.ImportNode(r, true);
newDoc.AppendChild(copy);
return newDoc;
}
public static void SetWritePath(string path)
{
//Todo check if path is valid
if (Directory.Exists(Path.GetDirectoryName(path)) == false)
{
return;
}
string filename = Path.GetFullPath(path).Trim();
if (string.IsNullOrEmpty(filename))
{
return;
}
string? extension = Path.GetExtension(filename);
if (extension != ".xml")
{
return;
}
Instance.WritePath = path;
Instance.IsDefaultProfile = false;
if (ProfileChanged != null)
{
ProfileChanged(Instance, new(nameof(WritePath),path));
}
}
public static bool HasWritePath()
{
return !string.IsNullOrEmpty(Instance.WritePath);
}
public static string GetWritePath()
{
return Instance.WritePath;
}
public static void Write(XmlDocument root)
{
XmlSerializer ser = new XmlSerializer(typeof(XmlDocument));
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.OmitXmlDeclaration = true;
using (XmlWriter writer = XmlWriter.Create(Instance.WritePath, settings))
{
ser.Serialize(writer, root);
}
}
public List<DeviceInstance> GetDeviceInstances()
{
List<DeviceInstance> list = new();
foreach (var di in instances)
{
list.Add(di);
}
return list;
}
public List<ActionMap> GetActionMaps()
{
List<ActionMap> list = new();
foreach (var am in actionMaps)
{
list.Add(am);
}
return list;
}
public string GetActionPrefix(Guid hardwareId)
{
DeviceInstance device;
string prefix = "";
foreach (var instance in instances)
{
if (instance.HardwareId == hardwareId)
{
device = instance;
switch (instance.Type)
{
case "joystick": prefix = $"js{instance.Instance}_"; break;
case "keyboard": prefix = $"kb{instance.Instance}_"; break;
case "mouse": prefix = $"mo{instance.Instance}_"; break;
}
break;
}
}
if (prefix == "")
{
throw new Exception("Unable to match the binding device with a device from the star citizen xml export. Binding action for {hardwareId}");
}
return prefix;
}
/// <summary>
/// Takes a custom rebind and finds a replacement from default
/// useful when a rebind is removed to see if a default should
/// be placed in its stead.
/// </summary>
/// <param name="rebind"></param>
public static Rebind? GetDefaultSubstitute(string actionMap, string action, Rebind rebind)
{
if (_defaultProfile == null) return null;
foreach (ActionMap defaultMap in _defaultProfile.actionMaps)
{
if (defaultMap.Name == actionMap)
{
foreach (var defaultAction in defaultMap.Actions)
{
if (defaultAction.Name == action)
{
foreach (Rebind defaultRebind in defaultAction.Rebinds)
{
if (defaultRebind.Prefix.Equals(rebind.Prefix))
{
return defaultRebind;
}
}
}
}
}
}
return null;
}
public bool AddActionMapIfMissing(ActionMap actionMap)
{
bool foundActionMap = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap.Name)
{
foundActionMap = true;
}
}
if (!foundActionMap)
{
var clone = (ActionMap)actionMap.Clone();
foreach (var cloned_action in clone.Actions)
{
foreach (var cloned_rebind in cloned_action.Rebinds)
{
cloned_rebind.IsDefaultKeybind = true;
}
}
actionMaps.Add(clone);
return true;
}
return false;
}
public bool AddActionIfMissing(string actionMap, Action action)
{
bool foundAction = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap)
{
foreach (var act in map.Actions)
{
if (action.Name == act.Name)
{
foundAction= true;
}
}
if (!foundAction)
{
var clone = (Action)action.Clone();
foreach (var cloned_rebind in clone.Rebinds)
{
cloned_rebind.IsDefaultKeybind = true;
}
map.Actions.Add(clone);
return true;
}
}
}
return false;
}
public bool AddRebindIfMissing(string actionMap, string action, Rebind rebind)
{
string prefix = rebind.Input.Substring(0, rebind.Input.IndexOf("_") + 1);
bool success = false;
bool foundActionMap = false;
bool foundAction = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap)
{
foundActionMap = true;
foreach (var a in map.Actions)
{
if (a.Name == action)
{
foundAction = true;
bool rebindExists = false;
foreach (var r in a.Rebinds)
{
string cmp = r.Input.Substring(0, r.Input.IndexOf('_') + 1);
if (cmp.Equals(prefix))
{
rebindExists = true;
success = true;
break;
}
}
if (!rebindExists)
{
a.Rebinds.Add(rebind);
success = true;
}
}
}
if (foundAction) break;
}
if (foundActionMap) break;
}
return success;
}
public Rebind CreateRebind(string actionMap, string action, DeviceInput input)
{
Rebind rebind = new();
string setting = input.Setting;
rebind.Input = setting;
// string prefix = setting.Substring(0, setting.IndexOf("_")+1);
bool success = false;
bool foundActionMap = false;
bool foundAction = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap)
{
foundActionMap = true;
foreach (var a in map.Actions)
{
if (a.Name == action)
{
foundAction = true;
/*bool rebindExists = false;
foreach (var r in a.Rebinds)
{
string cmp = r.Input.Substring(0, r.Input.IndexOf('_') + 1);
if (cmp.Equals(prefix))
{
rebindExists = true;
success = true;
break;
}
}
if (!rebindExists)
{
//a.Rebinds.Add(rebind);
success = true;
}*/
}
if (foundAction) break;
}
}
if (foundAction) break;
}
if (success)
{
/*IsChanged = true;
if (ProfileChanged != null)
{
ProfileChanged(this, new ProfileChangedEventArgs(nameof(actionMap), new()));
}*/
}
if (!foundActionMap)
{
throw new Exception($"Unable to bind action under the action map '{actionMap}'. The action map does not exist.");
}
else if (!foundAction)
{
throw new Exception("Unable to bind action '{action}'. The action does not exist under the action map {actionMap}.");
}
else if (!foundAction)
{
throw new Exception("Could not bind the input '{input}' to '{actionMap}.{action}'. The device prefix {prefix} was not recoginized. Try making an export from Star Citizen with products you are trying to bind with.");
}
else
{
return rebind;
}
}
/*public Rebind Rebind(string actionMap, string action, DeviceInstance device, string input)
{
string prefix = "";
switch (device.Type)
{
case "joystick": prefix = $"js{device.Instance}_"; break;
case "keyboard": prefix = $"kb{device.Instance}_"; break;
case "mouse": prefix = $"mo{device.Instance}_"; break;
case "gamepad": prefix = $"gp{device.Instance}_"; break;
}
if (prefix == "")
{
throw new Exception("Unable to match the binding device with a device from the star citizen xml export. Binding action for {hardwareId}");
}
Rebind rebind = new();
rebind.Input = prefix + input;
bool success = false;
bool foundActionMap = false;
bool foundAction = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap)
{
foundActionMap = true;
foreach (var a in map.Actions)
{
if (a.Name == action)
{
foundAction = true;
bool rebindExists = false;
foreach (var r in a.Rebinds)
{
string cmp = r.Input.Substring(0, r.Input.IndexOf('_') + 1);
if (cmp.Equals(prefix))
{
rebindExists = true;
//r.Input = rebind.Input;
success = true;
break;
}
}
if (!rebindExists)
{
a.Rebinds.Add(rebind);
success = true;
}
}
}
}
}
if (!foundActionMap)
{
throw new Exception($"Unable to bind action under the action map '{actionMap}'. The action map does not exist.");
}
else if (!foundAction)
{
throw new Exception("Unable to bind action '{action}'. The action does not exist under the action map {actionMap}.");
}
else if (!success)
{
throw new Exception("Could not bind the input '{input}' to '{actionMap}.{action}'. The device prefix {prefix} was not recoginized. Try making an export from Star Citizen with products you are trying to bind with.");
}
else
{
return rebind;
}
}*/
public void addMissingAction(string actionMap, string action)
{
if (string.IsNullOrEmpty(actionMap))
{
Debug.WriteLine("Cannot add missing action: actionmap is empty");
return;
}
else if (string.IsNullOrEmpty(action))
{
Debug.WriteLine("Cannot add missing action: action is empty");
return;
}
ActionMap? am = null;
bool foundAction = false;
foreach (var map in actionMaps)
{
if (map.Name == actionMap)
{
am = map;
foreach (var a in map.Actions)
{
if (a.Name == action)
{
foundAction = true;
}
}
}
}
if (am == null)
{
am = new ActionMap();
am.Name = actionMap;
actionMaps.Add(am);
}
if (!foundAction)
{
Action a = new Action();
a.Name = action;
a.Rebinds.Add(new Rebind());
am.Actions.Add(a);
}
}
void extractProfileName()
{
var node = root.SelectSingleNode("/ActionMaps");
foreach (XmlAttribute attr in node.Attributes)
{
if (attr.Name == "profileName")
{
ProfileName = attr.Value;
}
}
}
void extractDeviceOptions()
{
var nodes = root.SelectNodes("/ActionMaps/deviceoptions");
DeviceOptions instance;
foreach (XmlNode node in nodes)
{
instance = DeviceOptions.FromNode(node);
deviceoptions.Add(instance);
}
}
void extractInstances()
{
var nodes = root.SelectNodes("/ActionMaps/options");
DeviceInstance instance;
foreach (XmlNode node in nodes)
{
instance = DeviceInstance.FromNode(node);
instances.Add(instance);
}
}
void extractActionMaps()
{
var nodes = root.SelectNodes("/ActionMaps/actionmap");
ActionMap instance;
foreach (XmlNode node in nodes)
{
instance = ActionMap.FromNode(node);
actionMaps.Add(instance);
}
}
//TODO: Reimplement this
void verifyInstances()
{
//DeviceDetector detector = new();
//List<IDevice> joysticks = detector.AllDevices;
//Sometimes Virpil Joystick switches hardware guid so they nolonger match the one in the keybinding configuration
// Therefore this runs through each joystick and compares the product name and updates the Star citizen configuration
//TODO: Fix this!
/*foreach (Joystick joystick in joysticks)
{
foreach (DeviceInstance device in instances)
{
if (device.Type == "joystick"
&& device.Product == joystick.Information.ProductName
&& device.HardwareId != joystick.Information.ProductGuid)
{
// 03-04-2023: Commented out because HardwareId setter is now private to avoid tinkering
//device.HardwareId = joystick.Information.ProductGuid;
}
}
}
foundDevices = new();
missingDevices = new();
unknownDevices = new();
foreach (Joystick joystick in joysticks)
{
var found = false;
foreach (DeviceInstance device in instances)
{
if (device.Type == "joystick"
&& device.Product == joystick.Information.ProductName)
{
found = true;
foundDevices.Add(device);
}
}
if (!found)
{
unknownDevices.Add(joystick);
}
}
//If another device was found than those listed in the profile configuration
// we add it to the user interface. That way the user can bind actions with it.
foreach (var d in unknownDevices)
{
var instance = DeviceInstance.FromDevice(d);
//instance.HardwareId = d.HardwareId;
instance.Product = d.Name;
instances.Add(instance);
}
foreach (DeviceInstance device in instances)
{
if (foundDevices.Contains(device) == false)
{
missingDevices.Add(device);
}
}*/
}
void updateConfigurationHardwareId()
{
}
void extract()
{
extractProfileName();
extractDeviceOptions();
extractInstances();
extractActionMaps();
//Verifies the devices found inside the configuration
// If the devices wasn't found - alert the user.
//TODO: Post-migration - verify instances
//verifyInstances();
//TODO: Post-migration - detect devices
detectEligibleDevices();
recognizeDeviceInstances();
}
Dictionary<IDevice,DeviceInstance> deviceToInstanceMap = new();
void recognizeDeviceInstances()
{
foreach (var instance in instances) {
var hardwareId = instance.HardwareId.ToString();
var product = hardwareId.Substring(0,4);
var vendor = hardwareId.Substring(4,4);
var instanceNumber = 0;
foreach (var device in DeviceDetector.GetDetectedDevices())
{
if (device.ProductId == product && device.VendorId == vendor && int.TryParse(instance.Instance, out instanceNumber)) {
deviceToInstanceMap.Add(device, instance);
DeviceDetector.UpdateDeviceInstance(device, instanceNumber);
}
}
}
}
public DeviceInstance? GetDeviceInstanceFromDevice(IDevice device)
{
if (deviceToInstanceMap.ContainsKey(device)) {
return deviceToInstanceMap[device];
}
//Get the device based on the order they're detected by DirectX
// see https://robertsspaceindustries.com/spectrum/community/SC/forum/50259/thread/input-config-file-behaviour/4608064
int instance = 0;
foreach (IDevice dev in DeviceDetector.GetDetectedDevices())
{
if (dev.Type == device.Type)
{
instance++;
}
if (dev.Name == device.Name)
{
DeviceInstance devInstance = DeviceInstance.FromDevice(dev);
devInstance.Instance = instance.ToString();
return devInstance;
}
}
return null;
}
void detectEligibleDevices()
{
IDevice[] devices = DeviceDetector.GetDetectedDevices();
foreach (var instance in instances)
{
foreach (IDevice device in devices)
{
DeviceInstance joystickDevice = DeviceInstance.FromDevice(device);
if (joystickDevice != null && joystickDevice.Equals(instance))
{
instance.Detected = true;
eligibleDevices.Add(instance);
}
}
}
}
public List<DeviceInstance> GetEligibleDevices()
{
List<DeviceInstance> list = new();
foreach (var device in eligibleDevices)
{
list.Add(device);
}
return list;
}
public void SaveProfileXml(string profileName, List<DeviceInstance> deviceInstances, Dictionary<string, List<Action>> groups)
{
XmlDocument newDoc = Profile.Instance.GetDoc();
var root = newDoc.SelectSingleNode("/ActionMaps");
//Apply new profile name
foreach (XmlAttribute attr in root.Attributes)
{
if (attr.Name == "profileName")
{
attr.Value = profileName;
}
}
var customisationUIHeader = newDoc.SelectSingleNode("/ActionMaps/CustomisationUIHeader");
foreach (XmlAttribute attr in customisationUIHeader.Attributes)
{
if (attr.Name == "label")
{
attr.Value = profileName;
}
}
//Update device instances
var deviceInstancesOrdered = deviceInstances.OrderByDescending((disp) => disp.Instance);
var devices = newDoc.SelectSingleNode("/ActionMaps/CustomisationUIHeader/devices");
if (devices != null) {
while(devices.ChildNodes.Count > 0)
{
devices.RemoveChild(devices.ChildNodes[0]);
}
}
var nodes = newDoc.SelectNodes("/ActionMaps/options");
if (nodes != null)
{
foreach (XmlNode node in nodes)
{
root.RemoveChild(node);
}
}
foreach (var deviceInstance in deviceInstancesOrdered)
{
//create joystick, mouse and keyboard nodes
if (devices != null)
{
var deviceNode = newDoc.CreateElement(deviceInstance.Type);
deviceNode.SetAttribute("instance", deviceInstance.Instance);
devices.PrependChild(deviceNode);
}
Options opt = new Options();
opt.instance = deviceInstance.Instance;
opt.type = deviceInstance.Type;
opt.Product = $"{deviceInstance.Product} {{{deviceInstance.HardwareId.ToString().ToUpper()}}}";
root.InsertAfter(opt.ToXml(newDoc), customisationUIHeader);
}
if (deviceoptions.Count > 0)
{
foreach (var dev in deviceoptions)
{
//root.InsertAfter(dev.ToXml(newDoc), customisationUIHeader);
}
}
//Update bindings
nodes = newDoc.SelectNodes("/ActionMaps/actionmap");
foreach (XmlNode node in nodes)
{
root.RemoveChild(node);
}
var actionmaps = newDoc.SelectSingleNode("/ActionMaps");
foreach (KeyValuePair<string, List<Action>> kv in groups)
{
var groupNode = actionmaps?.OwnerDocument?.CreateElement("actionmap");
var groupName = groupNode?.OwnerDocument.CreateAttribute("name");
if (groupNode == null || groupName == null) continue;
groupName.InnerText = kv.Key;
groupNode.Attributes.Append(groupName);
foreach (var action in kv.Value)
{
var actionNode = newDoc.CreateElement("action");
var actionName = newDoc.CreateAttribute("name");
actionName.InnerText = action.Name;
actionNode.Attributes.Append(actionName);
foreach (var rebind in action.Rebinds)
{
if (rebind.IsDefaultKeybind)
continue; //No reason to save default rebinds
var rebindNode = newDoc.CreateElement("rebind");
var rebindInput = newDoc.CreateAttribute("input");
rebindInput.InnerText = rebind.Input;
if (!string.IsNullOrWhiteSpace(rebind.ActivationMode))
{
var rebindActivationMode = newDoc.CreateAttribute("activationMode");
rebindActivationMode.InnerText = "double_tap";
rebindNode.Attributes.Append(rebindActivationMode);
}
rebindNode.Attributes.Append(rebindInput);
actionNode.AppendChild(rebindNode);
}
groupNode.AppendChild(actionNode);
}
actionmaps?.AppendChild(groupNode);
}
Write(newDoc);
if (ProfileSaved != null)
{
ProfileSaved(this, new(true, Operation.Saved, WritePath));
}
}
//Not used
public void SaveProfileXml()
{
XmlDocument newDoc = Profile.Instance.GetDoc();
var root = newDoc.SelectSingleNode("/ActionMaps");
//Update bindings
var nodes = newDoc.SelectNodes("/ActionMaps/actionmap");
foreach (XmlNode node in nodes)
{
root.RemoveChild(node);
}
foreach (var map in actionMaps)
{
var groupNode = newDoc.CreateElement("actionmap");
var groupName = groupNode?.OwnerDocument.CreateAttribute("name");
groupName.InnerText = map.Name;
groupNode.Attributes.Append(groupName);
foreach (var action in map.Actions)
{
var actionNode = newDoc.CreateElement("action");
var actionName = newDoc.CreateAttribute("name");
actionName.InnerText = action.Name;
actionNode.Attributes.Append(actionName);
foreach (var rebind in action.Rebinds)
{
if (rebind.IsDefaultKeybind)
continue;
var rebindNode = newDoc.CreateElement("rebind");
var rebindInput = newDoc.CreateAttribute("input");
rebindInput.InnerText = rebind.Input;
rebindNode.Attributes.Append(rebindInput);
actionNode.AppendChild(rebindNode);
}
groupNode.AppendChild(actionNode);
}
root?.AppendChild(groupNode);
}
Write(newDoc);
}
public object Clone()
{
Profile clone = new()
{
ProfileName = $"{ProfileName} copy",
root = new XmlDocument(),
//These three properties doesn't have to be cloned
instances = instances,
deviceoptions = deviceoptions,
eligibleDevices = eligibleDevices,
};
clone.root.InnerXml = root.InnerXml;
foreach (ICloneable map in actionMaps)
{
clone.actionMaps.Add((ActionMap) map.Clone());
}
return clone;
}
}