2022-05-29 23:32:31 +02:00
// <copyright file="bindED.cs" company="alterNERDtive">
// Copyright 2020– 2022 alterNERDtive.
//
// This file is part of bindED VoiceAttack plugin.
//
// bindED VoiceAttack plugin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// bindED VoiceAttack plugin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with bindED VoiceAttack plugin. If not, see <https://www.gnu.org/licenses/>.
// </copyright>
#nullable enable
2021-06-14 11:46:57 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Xml.Linq ;
namespace bindEDplugin
{
2022-05-29 23:32:31 +02:00
/// <summary>
/// This VoiceAttack plugin reads Elite Dangerous .binds files for keyboard
/// bindings and makes them available in VoiceAttack variables.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "historic, grandfathered in")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "historic, grandfathered in")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "historic, grandfathered in")]
2021-06-14 11:46:57 +02:00
public class bindEDPlugin
{
2022-05-31 19:03:54 +02:00
private static readonly Version VERSION = new ( "4.2.2" ) ;
2022-05-30 23:52:44 +02:00
2022-05-29 23:32:31 +02:00
private static readonly string BindingsDir = Path . Combine (
Environment . GetFolderPath (
2021-06-14 11:46:57 +02:00
Environment . SpecialFolder . LocalApplicationData ) ,
2022-05-29 23:32:31 +02:00
@"Frontier Developments\Elite Dangerous\Options\Bindings" ) ;
private static readonly Dictionary < string , int > FileEventCount = new ( ) ;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "just cause")]
private static dynamic? VA ;
private static string? pluginPath ;
private static FileSystemWatcher ? bindsWatcher ;
private static FileSystemWatcher ? mapWatcher ;
private static string? layout ;
private static Dictionary < string , int > ? keyMap ;
private static string? preset ;
private static Dictionary < string , List < string > > ? binds ;
2021-06-14 11:46:57 +02:00
private static FileSystemWatcher BindsWatcher
{
get
{
2022-05-29 23:32:31 +02:00
if ( bindsWatcher = = null )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
bindsWatcher = new FileSystemWatcher ( BindingsDir ) ;
bindsWatcher . NotifyFilter = NotifyFilters . LastWrite | NotifyFilters . FileName ;
bindsWatcher . Changed + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
bindsWatcher . Created + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
bindsWatcher . Renamed + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
return bindsWatcher ! ;
2021-06-14 11:46:57 +02:00
}
}
private static FileSystemWatcher MapWatcher
{
get
{
2022-05-29 23:32:31 +02:00
if ( mapWatcher = = null )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
mapWatcher = new FileSystemWatcher ( pluginPath ) ;
mapWatcher . NotifyFilter = NotifyFilters . LastWrite | NotifyFilters . FileName ;
mapWatcher . Changed + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
mapWatcher . Created + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
mapWatcher . Renamed + = ( source , eventArgs ) = > { FileChangedHandler ( eventArgs . Name ) ; } ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
return mapWatcher ! ;
2021-06-14 11:46:57 +02:00
}
}
private static string? Layout
{
2022-05-29 23:32:31 +02:00
get = > layout ? ? = VA ? . GetText ( "bindED.layout#" ) ? ? "en-us" ;
2021-06-14 11:46:57 +02:00
set
{
2022-05-29 23:32:31 +02:00
layout = value ;
2021-06-14 11:46:57 +02:00
KeyMap = null ;
}
}
private static Dictionary < string , int > ? KeyMap
{
2022-05-29 23:32:31 +02:00
get = > keyMap ? ? = LoadKeyMap ( Layout ! ) ;
set = > keyMap = value ;
2021-06-14 11:46:57 +02:00
}
private static string? Preset
{
2022-05-29 23:32:31 +02:00
get = > preset ? ? = DetectPreset ( ) ;
2021-06-14 11:46:57 +02:00
set
{
2022-05-29 23:32:31 +02:00
preset = value ;
2021-06-14 11:46:57 +02:00
Binds = null ;
}
}
private static Dictionary < string , List < string > > ? Binds
{
2022-05-29 23:32:31 +02:00
get = > binds ? ? = ReadBinds ( DetectBindsFile ( Preset ! ) ) ;
set = > binds = value ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
/// <summary>
/// The plugin’ s display name, as required by the VoiceAttack plugin API.
/// </summary>
/// <returns>The display name.</returns>
2021-06-14 11:46:57 +02:00
public static string VA_DisplayName ( ) = > $"bindED Plugin v{VERSION}-alterNERDtive" ;
2022-05-29 23:32:31 +02:00
/// <summary>
/// The plugin’ s description, as required by the VoiceAttack plugin API.
/// </summary>
/// <returns>The description.</returns>
2021-06-14 11:46:57 +02:00
public static string VA_DisplayInfo ( ) = > "bindED Plugin\r\n\r\n2016 VoiceAttack.com\r\n2020– 2021 alterNERDtive" ;
2022-05-29 23:32:31 +02:00
/// <summary>
/// The plugin’ s GUID, as required by the VoiceAttack plugin API.
/// </summary>
/// <returns>The GUID.</returns>
public static Guid VA_Id ( ) = > new ( "{524B4B9A-3965-4045-A39A-A239BF6E2838}" ) ;
/// <summary>
/// The Init method, as required by the VoiceAttack plugin API.
/// Runs when the plugin is initially loaded.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
2021-06-14 11:46:57 +02:00
public static void VA_Init1 ( dynamic vaProxy )
{
2022-05-29 23:32:31 +02:00
VA = vaProxy ;
VA . TextVariableChanged + = new Action < string , string , string , Guid ? > ( TextVariableChanged ) ;
pluginPath = Path . GetDirectoryName ( VA . PluginPath ( ) ) ;
2021-06-14 11:46:57 +02:00
2022-05-29 23:41:13 +02:00
VA . SetText ( "bindED.version" , VERSION . ToString ( ) ) ;
2022-05-29 23:32:31 +02:00
VA . SetText ( "bindED.fork" , "alterNERDtive" ) ;
2021-06-14 11:46:57 +02:00
try
{
LoadBinds ( Binds ) ;
}
catch ( Exception e )
{
LogError ( e . Message ) ;
}
finally
{
BindsWatcher . EnableRaisingEvents = true ;
MapWatcher . EnableRaisingEvents = true ;
}
}
2022-05-29 23:32:31 +02:00
/// <summary>
/// The Invoke method, as required by the VoiceAttack plugin API.
/// Runs whenever a plugin context is invoked.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
2021-06-14 11:46:57 +02:00
public static void VA_Invoke1 ( dynamic vaProxy )
{
2022-05-29 23:32:31 +02:00
VA = vaProxy ;
2021-06-14 11:46:57 +02:00
try
{
2022-05-29 23:32:31 +02:00
string context = VA . Context . ToLower ( ) ;
2021-06-14 09:34:42 +02:00
if ( context = = "diagnostics" )
{
LogInfo ( $"current keybord layout: {Layout}" ) ;
LogInfo ( $"current preset: {Preset}" ) ;
2022-05-29 23:32:31 +02:00
LogInfo ( $"detected binds file: {new FileInfo(DetectBindsFile(Preset!)).Name}" ) ;
2021-07-18 19:57:25 +02:00
LogInfo ( $"detected binds file (full path): {DetectBindsFile(Preset!)}" ) ;
2021-06-14 11:46:57 +02:00
}
else if ( context = = "listbinds" )
{
2022-05-29 23:32:31 +02:00
ListBinds ( Binds , VA . GetText ( "bindED.separator" ) ? ? "\r\n" ) ;
2021-06-14 11:46:57 +02:00
}
else if ( context = = "loadbinds" )
{
// force reset everything
Layout = null ;
Preset = null ;
2022-05-29 23:32:31 +02:00
if ( ! string . IsNullOrWhiteSpace ( VA . GetText ( "~bindsFile" ) ) )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
Binds = ReadBinds ( Path . Combine ( BindingsDir , VA . GetText ( "~bindsFile" ) ) ) ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
LoadBinds ( Binds ) ;
}
else if ( context = = "missingbinds" )
{
MissingBinds ( Binds ) ;
}
else
{
2022-05-29 23:32:31 +02:00
if ( string . IsNullOrWhiteSpace ( context ) )
2022-01-12 16:07:34 +01:00
{
LogError ( "Empty plugin context." ) ;
}
else
{
LogError ( $"Invalid plugin context '{context}'." ) ;
}
2022-05-29 23:32:31 +02:00
2022-01-12 16:08:05 +01:00
LogError ( "You generally do not need to invoke the plugin manually." ) ;
2021-06-14 11:46:57 +02:00
}
}
catch ( Exception e )
{
LogError ( e . Message ) ;
}
}
2022-05-29 23:32:31 +02:00
/// <summary>
/// The Exit method, as required by the VoiceAttack plugin API.
/// Runs when VoiceAttack is shut down.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "required by VoiceAttack plugin API")]
public static void VA_Exit1 ( dynamic vaProxy )
{
}
2021-06-14 11:46:57 +02:00
2022-05-29 23:32:31 +02:00
/// <summary>
/// The StopCommand method, as required by the VoiceAttack plugin API.
/// Runs whenever all commands are stopped using the “Stop All Commands”
/// button or action.
/// </summary>
public static void VA_StopCommand ( )
{
}
2021-06-14 11:46:57 +02:00
2022-05-29 23:32:31 +02:00
private static void TextVariableChanged ( string name , string from , string to , Guid ? internalID = null )
2021-06-14 11:46:57 +02:00
{
if ( name = = "bindED.layout#" )
{
LogInfo ( $"Keyboard layout changed to '{to}', reloading …" ) ;
Layout = to ;
try
{
LoadBinds ( Binds ) ;
}
catch ( Exception e )
{
LogError ( e . Message ) ;
}
}
}
private static void LogError ( string message )
{
2022-05-29 23:32:31 +02:00
VA ! . WriteToLog ( $"ERROR | bindED: {message}" , "red" ) ;
2021-06-14 11:46:57 +02:00
}
private static void LogInfo ( string message )
{
2022-05-29 23:32:31 +02:00
VA ! . WriteToLog ( $"INFO | bindED: {message}" , "blue" ) ;
2021-06-14 11:46:57 +02:00
}
private static void LogWarn ( string message )
{
2022-05-29 23:32:31 +02:00
VA ! . WriteToLog ( $"WARN | bindED: {message}" , "yellow" ) ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
private static void ListBinds ( Dictionary < string , List < string > > ? binds , string separator )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
VA ! . SetText ( "~bindED.bindsList" , string . Join ( separator , binds ! . Keys ) ) ;
2021-06-14 11:46:57 +02:00
LogInfo ( "List of Elite binds saved to TXT variable '~bindED.bindsList'." ) ;
}
private static void LoadBinds ( Dictionary < string , List < string > > ? binds )
{
foreach ( KeyValuePair < string , List < string > > bind in binds ! )
{
string value = string . Empty ;
bool valid = true ;
if ( bind . Value . Count = = 0 )
{
2022-05-29 23:32:31 +02:00
// LogInfo($"No keyboard bind for '{bind.Key}' found, skipping …");
2021-06-14 11:46:57 +02:00
}
else
{
foreach ( string key in bind . Value )
{
if ( KeyMap ! . ContainsKey ( key ) )
{
value + = $"[{KeyMap[key]}]" ;
}
else
{
valid = false ;
LogError ( $"No valid key code for '{key}' found, skipping bind for '{bind.Key}' …" ) ;
}
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
if ( valid )
{
2022-05-29 23:32:31 +02:00
VA ! . SetText ( bind . Key , value ) ;
2021-06-14 11:46:57 +02:00
}
}
}
2022-05-29 23:32:31 +02:00
LogInfo ( $"Elite binds '{(string.IsNullOrWhiteSpace(VA!.GetText(" ~ bindsFile ")) ? Preset : VA!.GetText(" ~ bindsFile "))}' for layout '{Layout}' loaded successfully." ) ;
2021-06-14 11:46:57 +02:00
}
private static void MissingBinds ( Dictionary < string , List < string > > ? binds )
{
2022-05-29 23:32:31 +02:00
List < string > missing = new ( 256 ) ;
2021-06-14 11:46:57 +02:00
foreach ( KeyValuePair < string , List < string > > bind in binds ! )
{
if ( bind . Value . Count = = 0 )
{
missing . Add ( bind . Key ) ;
}
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
if ( missing . Count > 0 )
{
2022-05-29 23:32:31 +02:00
VA ! . SetText ( "~bindED.missingBinds" , string . Join ( "\r\n" , missing ) ) ;
VA ! . SetBoolean ( "~bindED.missingBinds" , true ) ;
2021-06-14 11:46:57 +02:00
LogInfo ( "List of missing Elite binds saved to TXT variable '~bindED.missingBinds'." ) ;
}
else
{
LogInfo ( $"No missing keyboard binds found." ) ;
}
}
2022-05-29 23:32:31 +02:00
private static Dictionary < string , int > LoadKeyMap ( string layout )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
string mapFile = Path . Combine ( pluginPath , $"EDMap-{layout.ToLower()}.txt" ) ;
2021-06-14 11:46:57 +02:00
if ( ! File . Exists ( mapFile ) )
{
throw new FileNotFoundException ( $"No map file for layout '{layout}' found." ) ;
}
2022-05-29 23:32:31 +02:00
Dictionary < string , int > map = new ( 256 ) ;
foreach ( string line in File . ReadAllLines ( mapFile , System . Text . Encoding . UTF8 ) )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
string [ ] arItem = line . Split ( ";" . ToCharArray ( ) , 2 , StringSplitOptions . RemoveEmptyEntries ) ;
if ( ( arItem . Count ( ) = = 2 ) & & ( ! string . IsNullOrWhiteSpace ( arItem [ 0 ] ) ) & & ( ! map . ContainsKey ( arItem [ 0 ] ) ) )
2021-06-14 11:46:57 +02:00
{
ushort iKey ;
if ( ushort . TryParse ( arItem [ 1 ] , out iKey ) )
{
if ( iKey > 0 & & iKey < 256 )
2022-05-29 23:32:31 +02:00
{
2021-06-14 11:46:57 +02:00
map . Add ( arItem [ 0 ] . Trim ( ) , iKey ) ;
2022-05-29 23:32:31 +02:00
}
2021-06-14 11:46:57 +02:00
}
}
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
if ( map . Count = = 0 )
{
throw new Exception ( $"Map file for {layout} does not contain any elements." ) ;
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
return map ;
}
private static string DetectPreset ( )
{
2022-05-29 23:32:31 +02:00
string startFile = Path . Combine ( BindingsDir , "StartPreset.4.start" ) ;
2021-06-14 11:46:57 +02:00
if ( ! File . Exists ( startFile ) )
{
2022-05-29 23:32:31 +02:00
startFile = Path . Combine ( BindingsDir , "StartPreset.start" ) ;
2021-12-24 17:48:28 +01:00
if ( ! File . Exists ( startFile ) )
{
throw new FileNotFoundException ( "No 'StartPreset.start' file found. Please run Elite: Dangerous at least once, then restart VoiceAttack." ) ;
}
2021-06-14 11:46:57 +02:00
}
2021-07-04 20:59:18 +02:00
IEnumerable < string > presets = File . ReadAllLines ( startFile ) . Distinct ( ) ;
if ( presets . Count ( ) > 1 )
{
2022-05-29 23:32:31 +02:00
LogError ( $"You have selected multiple control presets ('{string.Join(" ' , ' ", presets)}'). "
+ $"Only binds from '{presets.First()}' will be used. Please refer to the documentation for more information." ) ;
2021-07-04 20:59:18 +02:00
}
return presets . First ( ) ;
2021-06-14 11:46:57 +02:00
}
2021-07-18 19:57:25 +02:00
private static string DetectBindsFile ( string preset )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
DirectoryInfo dirInfo = new ( BindingsDir ) ;
2021-06-14 11:46:57 +02:00
FileInfo [ ] bindFiles = dirInfo . GetFiles ( )
2021-07-18 19:56:36 +02:00
. Where ( i = > Regex . Match ( i . Name , $@"^{Regex.Escape(preset)}\.[34]\.0\.binds$" ) . Success )
2021-06-14 11:46:57 +02:00
. OrderByDescending ( p = > p . Name ) . ToArray ( ) ;
if ( bindFiles . Count ( ) = = 0 )
{
bindFiles = dirInfo . GetFiles ( $"{preset}.binds" ) ;
if ( bindFiles . Count ( ) = = 0 )
{
throw new FileNotFoundException ( $"No bindings file found for preset '{preset}'. If this is a default preset, please change anything in Elite’ s controls options." ) ;
}
}
return bindFiles [ 0 ] . FullName ;
}
private static Dictionary < string , List < string > > ReadBinds ( string file )
{
XElement rootElement ;
rootElement = XElement . Load ( file ) ;
2022-05-29 23:32:31 +02:00
Dictionary < string , List < string > > binds = new ( 512 ) ;
2021-06-14 11:46:57 +02:00
if ( rootElement ! = null )
{
foreach ( XElement c in rootElement . Elements ( ) . Where ( i = > i . Elements ( ) . Count ( ) > 0 ) )
{
2022-05-29 23:32:31 +02:00
List < string > keys = new ( ) ;
2021-06-14 11:46:57 +02:00
foreach ( var element in c . Elements ( ) . Where ( i = > i . HasAttributes ) )
{
if ( element . Name = = "Primary" )
{
2022-05-29 23:32:31 +02:00
if ( element . Attribute ( "Device" ) . Value = = "Keyboard"
& & ! string . IsNullOrWhiteSpace ( element . Attribute ( "Key" ) . Value ) & & element . Attribute ( "Key" ) . Value . StartsWith ( "Key_" ) )
2021-06-14 11:46:57 +02:00
{
foreach ( var modifier in element . Elements ( ) . Where ( i = > i . Name . LocalName = = "Modifier" ) )
{
keys . Add ( modifier . Attribute ( "Key" ) . Value ) ;
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
keys . Add ( element . Attribute ( "Key" ) . Value ) ;
}
}
2022-05-29 23:32:31 +02:00
if ( keys . Count = = 0 & & element . Name = = "Secondary" )
{ // nothing found in primary... look in secondary
if ( element . Attribute ( "Device" ) . Value = = "Keyboard"
& & ! string . IsNullOrWhiteSpace ( element . Attribute ( "Key" ) . Value ) & & element . Attribute ( "Key" ) . Value . StartsWith ( "Key_" ) )
2021-06-14 11:46:57 +02:00
{
foreach ( var modifier in element . Elements ( ) . Where ( i = > i . Name . LocalName = = "Modifier" ) )
{
keys . Add ( modifier . Attribute ( "Key" ) . Value ) ;
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
keys . Add ( element . Attribute ( "Key" ) . Value ) ;
}
}
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
binds . Add ( $"ed{c.Name.LocalName}" , keys ) ;
}
}
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
return binds ;
}
private static void FileChangedHandler ( string name )
{
// so apparently these events all fire twice … let’ s make sure we only handle it once.
2022-05-29 23:32:31 +02:00
if ( FileEventCount . ContainsKey ( name ) )
2021-06-14 11:46:57 +02:00
{
2022-05-29 23:32:31 +02:00
FileEventCount [ name ] + = 1 ;
2021-06-14 11:46:57 +02:00
}
else
{
2022-05-29 23:32:31 +02:00
FileEventCount . Add ( name , 1 ) ;
2021-06-14 11:46:57 +02:00
}
2022-05-29 23:32:31 +02:00
if ( FileEventCount [ name ] % 2 = = 0 )
2021-06-14 11:46:57 +02:00
{
try
{
// let’ s make semi-sure that the file isn’ t locked …
// FIXXME: solve this properly
Thread . Sleep ( 500 ) ;
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
// Going by name only is a bit naïve given we’ re watching 2
// separate directories, but hey … worst case if something
// is doing unintended things is unnecessarily reloading the
// binds.
if ( name = = $"EDMap-{Layout!.ToLower()}.txt" )
{
LogInfo ( $"Key map for layout '{Layout}' has changed, reloading …" ) ;
KeyMap = null ;
LoadBinds ( Binds ) ;
}
else if ( name = = "StartPreset.start" )
{
LogInfo ( "Controls preset has changed, reloading …" ) ;
Preset = null ;
LoadBinds ( Binds ) ;
}
else if ( Regex . Match ( name , $@"{Preset}(\.[34]\.0)?\.binds$" ) . Success )
{
LogInfo ( $"Bindings file '{name}' has changed, reloading …" ) ;
Binds = null ;
LoadBinds ( Binds ) ;
2022-05-29 23:32:31 +02:00
2021-06-14 11:46:57 +02:00
// copy Odyssey -> Horizons
2022-05-29 23:32:31 +02:00
if ( name = = $"{Preset}.4.0.binds" & & ! VA ! . GetBoolean ( "bindED.disableHorizonsSync#" ) )
2021-06-14 11:46:57 +02:00
{
File . WriteAllText (
2022-05-29 23:32:31 +02:00
Path . Combine ( BindingsDir , $"{Preset}.3.0.binds" ) ,
File . ReadAllText ( Path . Combine ( BindingsDir , name ) )
. Replace ( "MajorVersion=\"4\" MinorVersion=\"0\">" , "MajorVersion=\"3\" MinorVersion=\"0\">" ) ) ;
2021-06-14 11:46:57 +02:00
}
}
}
catch ( Exception e )
{
LogError ( e . Message ) ;
}
}
}
}
}