2020-10-21 01:13:40 +02:00
#nullable enable
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.IO.Pipes ;
2020-12-02 19:08:46 +01:00
using System.Linq ;
2020-10-21 01:13:40 +02:00
namespace alterNERDtive.util
{
2020-12-02 19:08:46 +01:00
public class Configuration
{
private readonly dynamic VA ;
private readonly string ID ;
private readonly VoiceAttackLog Log ;
2020-12-02 20:28:09 +01:00
private readonly VoiceAttackCommands Commands ;
2020-12-02 19:08:46 +01:00
private static readonly Dictionary < string , OptDict < string , Option > > Defaults = new Dictionary < string , OptDict < string , Option > >
{
{
"alterNERDtive-base" ,
new OptDict < string , Option > {
{ new Option ( "eddi.quietMode" , true , voiceTrigger : "eddi quiet mode" , description : "Whether or not to make EDDI shut up." ) } ,
{ new Option ( "keyPressDuration" , ( decimal ) 0.01 , voiceTrigger : "key press duration" , description : "The time keys will be held down for." ) } ,
2020-12-07 20:25:24 +01:00
{ new Option ( "delays.quitToDesktop" , ( decimal ) 10.0 , voiceTrigger : "quit to desktop delay" ) } ,
2020-12-07 21:45:17 +01:00
{ new Option ( "elite.pasteKey" , "v" , voiceTrigger : "elite paste key" , description : "The key used to paste in conjunction with CTRL. The key that would be 'V' on QWERTY." ) } ,
{ new Option ( "log.logLevel" , "NOTICE" , voiceTrigger : "log level" , validValues : new List < dynamic > { "ERROR" , "WARN" , "NOTICE" , "INFO" , "DEBUG" } ,
description : @"The level of detail for logging to the VoiceAttack log.\nValid levels are ""ERROR"", ""WARN"", ""NOTICE"", ""INFO"" and ""DEBUG"".\nDefault: ""NOTICE""." ) } ,
2020-12-02 19:08:46 +01:00
}
} ,
{
"EliteAttack" ,
new OptDict < string , Option > {
2020-12-07 20:25:24 +01:00
{ new Option ( "announceEdsmSystemStatus" , true , voiceTrigger : "announce edsm system status" ) } ,
{ new Option ( "announceMappingCandidates" , true , voiceTrigger : "announce mapping candidates" ) } ,
{ new Option ( "announceOutdatedStationData" , true , voiceTrigger : "announce outdated station data" ) } ,
{ new Option ( "announceR2RMappingCandidates" , true , voiceTrigger : "announce road to riches mapping candidates" ) } ,
{ new Option ( "autoRestock" , true , voiceTrigger : "auto restock" ) } ,
//{ new Option("enableAutoUpdateCheck", true, voiceTrigger: "auto update check") },
{ new Option ( "flightAssistOff" , false , voiceTrigger : "flight assist off" ) } ,
{ new Option ( "hyperspaceDethrottle" , true , voiceTrigger : "hyper space dethrottle" ) } ,
{ new Option ( "outdatedStationThreshold" , ( int ) 365 , voiceTrigger : "outdated station threshold" ) } ,
2020-12-02 19:08:46 +01:00
}
} ,
{
"RatAttack" ,
new OptDict < string , Option > {
2020-12-03 02:05:16 +01:00
{ new Option ( "autoCloseCase" , false , voiceTrigger : "auto close case" ) } ,
{ new Option ( "announceNearestCMDR" , false , voiceTrigger : "announce nearest commander" ) } ,
{ new Option ( "announcePlatform" , false , voiceTrigger : "announce platform" ) } ,
{ new Option ( "CMDRs" , "" , voiceTrigger : "commanders" ) } ,
{ new Option ( "confirmCalls" , true , voiceTrigger : "confirm calls" ) } ,
{ new Option ( "onDuty" , true , voiceTrigger : "on duty" ) } ,
2020-12-07 21:45:17 +01:00
{ new Option ( "platforms" , "PC" , voiceTrigger : "platforms" ) } ,
2020-12-02 19:08:46 +01:00
}
} ,
{
"SpanshAttack" ,
new OptDict < string , Option > {
2020-12-07 18:09:26 +01:00
{ new Option ( "announceJumpsLeft" , ";1;3;5;10;15;20;30;50;75;100;" , voiceTrigger : "announce jumps left" ) } ,
{ new Option ( "announceWaypoints" , true , voiceTrigger : "announce waypoints" ) } ,
{ new Option ( "autoJumpAfterScooping" , true , voiceTrigger : "auto jump after scooping" ) } ,
{ new Option ( "autoPlot" , true , voiceTrigger : "auto plot" ) } ,
{ new Option ( "clearOnShutdown" , true , voiceTrigger : "clear on shutdown" ) } ,
{ new Option ( "copyWaypointToClipboard" , false , voiceTrigger : "copy waypoint to clipboard" ) } ,
{ new Option ( "defaultToLadenRange" , false , voiceTrigger : "default to laden range" ) } ,
2020-12-07 21:45:17 +01:00
{ new Option ( "timeTrip" , false , voiceTrigger : "time trip" ) } ,
2020-12-02 19:08:46 +01:00
}
} ,
{
"StreamAttack" ,
new OptDict < string , Option > {
2020-12-03 13:49:10 +01:00
{ new Option ( "outputDir" , @"%appdata%\StreamAttack\" , voiceTrigger : "StreamAttack output directory" ) }
2020-12-02 19:08:46 +01:00
}
}
} ;
2020-12-07 21:45:17 +01:00
public class Option
2020-12-02 19:08:46 +01:00
{
public readonly string Name ;
public readonly dynamic DefaultValue ;
2020-12-07 21:45:17 +01:00
public readonly List < dynamic > ? ValidValues ;
2020-12-02 19:08:46 +01:00
public readonly string VoiceTrigger ;
public string TtsDescription { get = > ttsDescription ? ? VoiceTrigger ; }
private readonly string? ttsDescription ;
public string Description { get = > description ? ? "No description available." ; }
public readonly string? description ;
2020-12-07 21:45:17 +01:00
public Option ( string name , dynamic defaultValue , string voiceTrigger , List < dynamic > ? validValues = null , string? ttsDescription = null , string? description = null )
= > ( Name , DefaultValue , VoiceTrigger , ValidValues , this . ttsDescription , this . description ) = ( name , defaultValue , voiceTrigger , validValues , ttsDescription , description ) ;
2020-12-02 19:08:46 +01:00
public static implicit operator ( string , Option ) ( Option o ) = > ( o . Name , o ) ;
public static explicit operator bool ( Option o ) = > o . DefaultValue ;
public static explicit operator DateTime ( Option o ) = > o . DefaultValue ;
public static explicit operator decimal ( Option o ) = > o . DefaultValue ;
public static explicit operator int ( Option o ) = > o . DefaultValue ;
public static explicit operator short ( Option o ) = > o . DefaultValue ;
public static explicit operator string ( Option o ) = > o . DefaultValue ;
public override string ToString ( ) = > DefaultValue . ToString ( ) ;
}
private class OptDict < TKey , TValue > : Dictionary < TKey , TValue >
{
public OptDict ( ) : base ( ) { }
public OptDict ( int capacity ) : base ( capacity ) { }
public void Add ( ( TKey , TValue ) tuple )
{
base . Add ( tuple . Item1 , tuple . Item2 ) ;
}
}
2020-12-02 20:28:09 +01:00
public Configuration ( dynamic vaProxy , VoiceAttackLog log , VoiceAttackCommands commands , string id ) = > ( VA , Log , Commands , ID ) = ( vaProxy , log , commands , id ) ;
2020-12-02 19:08:46 +01:00
public dynamic GetDefault ( string name )
{
return GetDefault ( ID , name ) ;
}
public static dynamic GetDefault ( string id , string name )
{
return Defaults [ id ] [ name ] ;
}
2020-12-07 21:45:17 +01:00
public Option GetOption ( string name )
{
return GetOption ( ID , name ) ;
}
public Option GetOption ( string id , string name )
{
return Defaults [ id ] [ name ] ;
}
2020-12-02 19:08:46 +01:00
public void SetVoiceTriggers ( System . Type type )
{
List < string > triggers = new List < string > ( ) ;
foreach ( Dictionary < string , Option > options in Defaults . Values )
{
foreach ( Option option in options . Values )
{
if ( option . DefaultValue . GetType ( ) = = type )
{
if ( triggers . Contains ( option . VoiceTrigger ) )
{
throw new ArgumentException ( $"Voice trigger '{option.VoiceTrigger}' is not unique, aborting …" ) ;
}
triggers . Add ( option . VoiceTrigger ) ;
}
}
}
if ( triggers . Count > 0 )
{
string triggerString = string . Join ( ";" , triggers ) ;
VA . SetText ( $"alterNERDtive-base.triggers.{type.Name}" , triggerString ) ;
Log . Debug ( $"Voice triggers for {type.Name}: '{triggerString}'" ) ;
}
else
{
// make sure we don’ t accidentally have weird things happening with empty config voice triggers
string triggerString = $"tenuiadafesslejüsljlejutlesuivle{type.Name}" ;
VA . SetText ( $"alterNERDtive-base.triggers.{type.Name}" , triggerString ) ;
Log . Debug ( $"No voice triggers found for {type.Name}" ) ;
}
}
public void SetVariablesForTrigger ( dynamic vaProxy , string trigger )
{
_ = trigger ? ? throw new ArgumentNullException ( "trigger" ) ;
foreach ( KeyValuePair < string , OptDict < string , Option > > options in Defaults )
{
try
{
2020-12-03 16:02:40 +01:00
Option option = options . Value . First ( item = > item . Value . VoiceTrigger . ToLower ( ) = = trigger ) . Value ;
2020-12-02 19:08:46 +01:00
vaProxy . SetText ( "~name" , $"{options.Key}.{option.Name}" ) ;
vaProxy . SetText ( "~ttsDescription" , option . TtsDescription ) ;
vaProxy . SetText ( "~description" , option . Description ) ;
break ;
}
catch ( InvalidOperationException ) { }
}
}
public bool HasDefault ( string name )
{
return HasDefault ( ID , name ) ;
}
public static bool HasDefault ( string id , string name )
{
return Defaults [ id ] . ContainsKey ( name ) ;
}
2020-12-02 20:28:09 +01:00
public void LoadFromProfile ( )
{
foreach ( KeyValuePair < string , OptDict < string , Option > > options in Defaults )
{
LoadFromProfile ( options . Key ) ;
}
}
public void LoadFromProfile ( string id )
{
foreach ( Option option in Defaults [ id ] . Values )
{
dynamic value = option . DefaultValue ;
string name = $"{id}.{option.Name}" ;
string type ;
if ( value is bool )
{
type = "boolean" ;
}
else if ( value is DateTime )
{
type = "date" ;
}
else if ( value is decimal )
{
type = "decimal" ;
}
else if ( value is int )
{
type = "int" ;
}
else if ( value is short )
{
type = "smallint" ;
}
else if ( value is string )
{
type = "text" ;
}
else
{
throw new InvalidDataException ( $"Invalid data type for option '{name}': '{value}'" ) ;
}
Log . Debug ( $"Loading value for option '{name}' from profile …" ) ;
Commands . Run ( "alterNERDtive-base.loadVariableFromProfile" , parameters : new dynamic [ ] { new string [ ] { $"{name}#" , type } } ) ;
}
}
2020-12-02 19:08:46 +01:00
public dynamic? ApplyDefault ( string name )
{
return ApplyDefault ( ID , name ) ;
}
public dynamic? ApplyDefault ( string id , string name )
{
if ( ! HasDefault ( id , name ) )
{
Log . Warn ( $"No default configuration value found for '{id}.{name}'" ) ;
return null ;
}
dynamic value = Defaults [ id ] [ name ] . DefaultValue ;
Log . Debug ( $"Loading default configuration value, '{id}.{name}': '{value}' …" ) ;
string variable = $"{id}.{name}#" ;
if ( value is bool )
{
VA . SetBoolean ( variable , value ) ;
}
else if ( value is DateTime )
{
VA . SetDate ( variable , value ) ;
}
else if ( value is decimal )
{
VA . SetDecimal ( variable , value ) ;
}
else if ( value is int )
{
VA . SetInt ( variable , value ) ;
}
else if ( value is short )
{
VA . SetSmallInt ( variable , value ) ;
}
else if ( value is string )
{
VA . SetText ( variable , value ) ;
}
else
{
throw new InvalidDataException ( $"Invalid data type for option '{id}.{name}': '{value}'" ) ;
}
return value ;
}
public void ApplyDefaults ( )
{
ApplyDefaults ( ID ) ;
}
public void ApplyAllDefaults ( )
{
foreach ( string id in Defaults . Keys )
{
ApplyDefaults ( id ) ;
}
}
public void ApplyDefaults ( string id )
{
foreach ( string key in Defaults [ id ] . Keys )
{
ApplyDefault ( id , key ) ;
}
}
2020-12-07 16:29:07 +01:00
public void DumpConfig ( )
{
foreach ( string id in Defaults . Keys )
{
DumpConfig ( id ) ;
}
}
public void DumpConfig ( string id )
{
Log . Notice ( $"===== {id} configuration: =====" ) ;
foreach ( string name in Defaults [ id ] . Keys )
{
DumpConfig ( id , name ) ;
}
}
public void DumpConfig ( string id , string name )
{
Option option = Defaults [ id ] [ name ] ;
string variable = $"{id}.{option.Name}#" ;
dynamic defaultValue = option . DefaultValue ;
dynamic value ;
if ( defaultValue is bool )
{
value = VA . GetBoolean ( variable ) ;
}
else if ( defaultValue is DateTime )
{
value = VA . GetDate ( variable ) ;
}
else if ( defaultValue is decimal )
{
value = VA . GetDecimal ( variable ) ;
}
else if ( defaultValue is int )
{
value = VA . GetInt ( variable ) ;
}
else if ( defaultValue is short )
{
value = VA . GetSmallInt ( variable ) ;
}
else if ( defaultValue is string )
{
value = VA . GetText ( variable ) ;
}
else
{
throw new InvalidDataException ( $"Invalid data type for option '{id}.{name}': '{defaultValue}'" ) ;
}
Log . Notice ( $"{variable} = {value}{(value == defaultValue? " ( default ) " : " ")}" ) ;
}
2020-12-02 19:08:46 +01:00
}
2020-10-21 01:13:40 +02:00
public class PythonProxy
{
public static Process SetupPythonScript ( string path , string arguments )
{
Process p = new Process ( ) ;
p . StartInfo . FileName = path ;
p . StartInfo . Arguments = arguments ;
p . StartInfo . WindowStyle = ProcessWindowStyle . Hidden ;
p . StartInfo . RedirectStandardOutput = true ;
2020-10-24 16:28:24 +02:00
p . StartInfo . RedirectStandardError = true ;
2020-10-21 01:13:40 +02:00
p . StartInfo . UseShellExecute = false ;
p . StartInfo . CreateNoWindow = true ;
return p ;
}
}
public enum LogLevel
{
ERROR ,
WARN ,
NOTICE ,
INFO ,
DEBUG
}
public class VoiceAttackCommands
{
private readonly dynamic VA ;
private readonly VoiceAttackLog Log ;
public VoiceAttackCommands ( dynamic vaProxy , VoiceAttackLog log ) = > ( VA , Log ) = ( vaProxy , log ) ;
public void Run ( string command , bool logMissing = true , bool wait = false , bool subcommand = false , Action < Guid ? > ? callback = null , dynamic [ ] ? parameters = null )
{
_ = command ? ? throw new ArgumentNullException ( "command" ) ;
if ( VA . Command . Exists ( command ) )
{
Log . Debug ( $"Parsing arguments for command '{command}' …" ) ;
string [ ] ? strings = null ;
int [ ] ? integers = null ;
decimal [ ] ? decimals = null ;
bool [ ] ? booleans = null ;
DateTime [ ] ? dates = null ; // this might not work!
if ( parameters ! = null )
{
foreach ( var values in parameters )
{
if ( values . GetType ( ) = = typeof ( string [ ] ) )
strings = values as string [ ] ;
else if ( values . GetType ( ) = = typeof ( int [ ] ) )
integers = values as int [ ] ;
else if ( values . GetType ( ) = = typeof ( decimal [ ] ) )
decimals = values as decimal [ ] ;
else if ( values . GetType ( ) = = typeof ( bool [ ] ) )
booleans = values as bool [ ] ;
else if ( values . GetType ( ) = = typeof ( DateTime [ ] ) )
dates = values as DateTime [ ] ;
}
}
Log . Debug ( $"Running command '{command}' …" ) ;
VA . Command . Execute (
CommandPhrase : command ,
WaitForReturn : wait ,
AsSubcommand : subcommand ,
CompletedAction : callback ,
2020-12-02 19:08:46 +01:00
PassedText : strings = = null ? null : $@"""{String.Join<string>(@""" ; "" ", strings)}" "" ,
2020-10-21 01:13:40 +02:00
PassedIntegers : integers = = null ? null : String . Join < int > ( ";" , integers ) ,
PassedDecimals : decimals = = null ? null : String . Join < decimal > ( ";" , decimals ) ,
PassedBooleans : booleans = = null ? null : String . Join < bool > ( ";" , booleans ) ,
PassedDates : dates = = null ? null : String . Join < DateTime > ( ";" , dates )
) ;
}
else
{
if ( logMissing )
Log . Warn ( $"Tried running missing command '{command}'." ) ;
}
}
public void RunAll ( IEnumerable < string > prefixes , string command , bool logMissing = true , bool wait = false , bool subcommand = false , Action < Guid ? > ? callback = null , dynamic [ ] ? parameters = null )
{
foreach ( string prefix in prefixes )
Run ( $"{prefix}.{command}" , logMissing , wait , subcommand , callback , parameters ) ;
}
public void TriggerEvent ( string name , bool logMissing = true , bool wait = false , bool subcommand = false , Action < Guid ? > ? callback = null , dynamic [ ] ? parameters = null )
{
Run ( $"(({name}))" , logMissing , wait , subcommand , callback , parameters ) ;
}
public void TriggerEventAll ( IEnumerable < string > prefixes , string name , bool logMissing = true , bool wait = false , bool subcommand = false , Action < Guid ? > ? callback = null , dynamic [ ] ? parameters = null )
{
foreach ( string prefix in prefixes )
Run ( $"(({prefix}.{name}))" , logMissing , wait , subcommand , callback , parameters ) ;
}
}
public class VoiceAttackLog
2020-10-25 10:13:14 +01:00
{
2020-10-21 01:13:40 +02:00
private readonly dynamic VA ;
private readonly string ID ;
private static readonly string [ ] LogColour = { "red" , "yellow" , "green" , "blue" , "gray" } ;
2020-10-25 10:13:14 +01:00
public LogLevel ? CurrentLogLevel
{
2020-10-21 01:13:40 +02:00
get = > currentLogLevel ? ? LogLevel . NOTICE ;
set
2020-10-25 10:13:14 +01:00
{
2020-10-21 01:13:40 +02:00
currentLogLevel = value ;
Notice ( $"Log level set to {value ?? LogLevel.NOTICE}." ) ;
}
}
public void SetCurrentLogLevel ( string level )
{
if ( level = = null )
CurrentLogLevel = null ;
else
CurrentLogLevel = ( LogLevel ) Enum . Parse ( typeof ( LogLevel ) , level . ToUpper ( ) ) ;
}
private static LogLevel ? currentLogLevel ;
2020-10-25 10:13:14 +01:00
public VoiceAttackLog ( dynamic vaProxy , string id ) = > ( VA , ID ) = ( vaProxy , id ) ;
2020-10-21 01:13:40 +02:00
public void Log ( string message , LogLevel level = LogLevel . INFO )
{
Log ( ID , message , level ) ;
}
public void Log ( string sender , string message , LogLevel level = LogLevel . INFO )
{
_ = sender ? ? throw new ArgumentNullException ( "sender" ) ;
_ = message ? ? throw new ArgumentNullException ( "message" ) ;
if ( level < = CurrentLogLevel )
VA . WriteToLog ( $"{level} | {sender}: {message}" , LogColour [ ( int ) level ] ) ;
}
public void Error ( string message ) = > Log ( message , LogLevel . ERROR ) ;
public void Warn ( string message ) = > Log ( message , LogLevel . WARN ) ;
public void Notice ( string message ) = > Log ( message , LogLevel . NOTICE ) ;
public void Info ( string message ) = > Log ( message , LogLevel . INFO ) ;
public void Debug ( string message ) = > Log ( message , LogLevel . DEBUG ) ;
}
public interface IPipable
{
public void ParseString ( string serialization ) ;
public string ToString ( ) ;
}
public class PipeServer < Thing > where Thing : IPipable , new ( )
{
private readonly string PipeName ;
private readonly SignalHandler Handler ;
private readonly VoiceAttackLog Log ;
private bool Running = false ;
2020-10-25 10:13:14 +01:00
private NamedPipeServerStream ? Server ;
2020-10-21 01:13:40 +02:00
public PipeServer ( VoiceAttackLog log , string name , SignalHandler handler )
= > ( Log , PipeName , Handler ) = ( log , name , handler ) ;
public delegate void SignalHandler ( Thing thing ) ;
public PipeServer < Thing > Run ( )
{
2020-12-02 19:08:46 +01:00
Log . Debug ( $"Starting '{PipeName}' pipe …" ) ;
2020-10-21 01:13:40 +02:00
if ( ! Running )
{
Running = true ;
2020-10-25 10:13:14 +01:00
WaitForConnection ( ) ;
2020-10-21 01:13:40 +02:00
}
return this ;
}
public PipeServer < Thing > Stop ( )
{
2020-12-02 19:08:46 +01:00
Log . Debug ( $"Stopping '{PipeName}' pipe …" ) ;
2020-10-21 01:13:40 +02:00
if ( Running )
{
Running = false ;
2020-10-25 10:13:14 +01:00
Server ! . Close ( ) ;
2020-10-21 01:13:40 +02:00
}
return this ;
}
2020-10-25 10:13:14 +01:00
private void WaitForConnection ( )
2020-10-21 01:13:40 +02:00
{
2020-10-25 10:13:14 +01:00
try
2020-10-21 01:13:40 +02:00
{
2020-10-25 10:13:14 +01:00
Server = new NamedPipeServerStream ( PipeName , PipeDirection . In , NamedPipeServerStream . MaxAllowedServerInstances , PipeTransmissionMode . Message , PipeOptions . Asynchronous ) ;
Server . BeginWaitForConnection ( OnConnect , Server ) ;
}
catch ( Exception e )
{
Log . Error ( $"Error setting up pipe: {e.Message}" ) ;
}
}
private void OnConnect ( IAsyncResult ar )
{
NamedPipeServerStream server = ( NamedPipeServerStream ) ar . AsyncState ;
try
{
server . EndWaitForConnection ( ar ) ;
WaitForConnection ( ) ;
using StreamReader reader = new StreamReader ( server ) ;
Thing thing = new Thing ( ) ;
thing . ParseString ( reader . ReadToEnd ( ) ) ;
Handler ( thing ) ;
}
catch ( ObjectDisposedException )
{
2020-12-02 19:08:46 +01:00
Log . Debug ( $"'{PipeName}' pipe has been closed." ) ;
2020-10-25 10:13:14 +01:00
}
catch ( Exception e )
{
Log . Error ( $"Error reading pipe: {e.Message}" ) ;
2020-10-21 01:13:40 +02:00
}
}
}
}