// // Copyright 2022 alterNERDtive. // // This file is part of YAVAPF. // // YAVAPF 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. // // YAVAPF 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 YAVAPF. If not, see <https://www.gnu.org/licenses/>. // using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using VoiceAttack; namespace alterNERDtive.Yavapf { /// /// Framework class for implementing a VoiceAttack plugin. /// public class VoiceAttackPlugin { private VoiceAttackLog? log; private VoiceAttackInitProxyClass? vaProxy; /// /// Invoked when VoiceAttack initializes the plugin. /// protected event Action? InitActions; /// /// Invoked when VoiceAttack closes. /// protected event Action? ExitActions; /// /// Invoked when VoiceAttack stops all commands. /// protected event Action? StopActions; /// /// Gets or sets the Actions to be run when the plugin is invoked from a /// VoiceAttack command. Only Actions with a matching “Context” /// attribute will be invoked. /// protected HandlerList> Contexts { get; set; } = new (); /// /// Gets or sets the Actions to be run when a /// variable changed. /// protected HandlerList> BoolChangedHandlers { get; set; } = new (); /// /// Gets or sets the Actions to be run when a /// variable changed. /// protected HandlerList> DateTimeChangedHandlers { get; set; } = new (); /// /// Gets or sets the Actions to be run when a /// variable changed. /// protected HandlerList> DecimalChangedHandlers { get; set; } = new (); /// /// Gets or sets the Actions to be run when a /// variable changed. /// protected HandlerList> IntChangedHandlers { get; set; } = new (); /// /// Gets or sets the Actions to be run when a /// variable changed. /// protected HandlerList> StringChangedHandlers { get; set; } = new (); /// /// Gets the currently stored VoiceAttackInitProxyClass object which is /// used to interface with VoiceAttack. /// /// You will usually want to use the provided methods and Properties /// instead. /// protected VoiceAttackInitProxyClass Proxy { get => this.vaProxy!; } /// /// Gets or sets the name of the plugin. /// protected string? Name { get; set; } /// /// Gets or sets the current version of the plugin. /// protected string? Version { get; set; } /// /// Gets or sets the description of the plugin. /// protected string? Info { get; set; } /// /// Gets or sets the GUID of the plugin. /// protected string? Guid { get; set; } /// /// Gets the instance the plugin uses to /// log to the VoiceAttack event log. /// /// You can use this to log your own messages. /// protected VoiceAttackLog Log { get => this.log ??= new VoiceAttackLog(this.Proxy, this.Name!); } /// /// Gets the value of a variable from VoiceAttack. /// /// Valid varible types are , , /// , and . /// /// The type of the variable. /// The name of the variable. /// The value of the variable. Can be null. /// Thrown when the variable is of an invalid type. protected T? Get(string name) { if (name.StartsWith("~")) { this.Log.Warn( $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); } return this.Proxy.Get(name); } /// /// Sets a variable for use in VoiceAttack. /// /// Valid varible types are , , /// , and . /// /// The type of the variable. /// The name of the variable. /// The value of the variable. Can not be null. /// Thrown when the variable is of an invalid type. protected void Set(string name, T? value) { if (name.StartsWith("~")) { this.Log.Warn( $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); } this.Proxy.Set(name, value); } /// /// Unsets a variable for use in VoiceAttack (= sets it to null). /// /// Valid varible types are , , /// , and . /// /// The type of the variable. /// The name of the variable. /// Thrown when the variable is of an invalid type. protected void Unset(string name) { if (name.StartsWith("~")) { this.Log.Warn( $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); } this.Proxy.Unset(name); } /// /// The plugin’s display name, as required by the VoiceAttack plugin API. /// /// The display name. protected string VaDisplayName() => $"{this.Name} v{this.Version}"; /// /// The plugin’s description, as required by the VoiceAttack plugin API. /// /// The description. protected string VaDisplayInfo() => this.Info!; /// /// The plugin’s GUID, as required by the VoiceAttack plugin API. /// /// The GUID. protected Guid VaId() => new (this.Guid); /// /// The Init method, as required by the VoiceAttack plugin API. /// Runs when the plugin is initially loaded. /// /// The VoiceAttack proxy object. protected void VaInit1(VoiceAttackInitProxyClass vaProxy) { this.vaProxy = vaProxy; this.Set($"{this.Name}.version", this.Version); this.Log.Debug($"Initializing v{this.Version} …"); this.vaProxy.BooleanVariableChanged += this.BooleanVariableChanged; this.vaProxy.DateVariableChanged += this.DateVariableChanged; this.vaProxy.DecimalVariableChanged += this.DecimalVariableChanged; this.vaProxy.IntegerVariableChanged += this.IntegerVariableChanged; this.vaProxy.TextVariableChanged += this.TextVariableChanged; this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.InitActions += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.ExitActions += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.StopActions += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.Contexts += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.BoolChangedHandlers += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.DateTimeChangedHandlers += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.DecimalChangedHandlers += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.IntChangedHandlers += (Action)m.CreateDelegate(typeof(Action))); this.GetType().GetMethods().Where(m => m.GetCustomAttributes().Any()).ToList().ForEach( m => this.StringChangedHandlers += (Action)m.CreateDelegate(typeof(Action))); this.Log.Debug("Running Init handlers …"); this.InitActions?.Invoke(vaProxy); this.Log.Debug("Finished running Init handlers."); this.Set($"{this.Name}.initialized", true); this.Log.Debug("Initialized."); } /// /// The Invoke method, as required by the VoiceAttack plugin API. /// Runs whenever a plugin context is invoked. /// /// The VoiceAttack proxy object. protected void VaInvoke1(VoiceAttackInvokeProxyClass vaProxy) { this.vaProxy = vaProxy; string context = vaProxy.Context.ToLower(); if (context.StartsWith("log.")) { try { string message = this.Get("~message") ?? throw new ArgumentNullException("~message"); switch (context) { case "log.error": this.Log.Error(message); break; case "log.warn": this.Log.Warn(message); break; case "log.notice": this.Log.Notice(message); break; case "log.info": this.Log.Info(message); break; case "log.debug": this.Log.Debug(message); break; default: throw new ArgumentException("invalid context", "context"); } } catch (ArgumentNullException e) { this.Log.Error($"Missing parameter '{e.ParamName}' for context '{context}'"); } catch (ArgumentException e) when (e.ParamName == "context") { this.Log.Error($"Invalid plugin context '{vaProxy.Context}'."); } catch (Exception e) { this.Log.Error($"Unhandled exception while executing plugin context '{context}': {e.Message}"); } } else { List> actions = this.Contexts.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == context || (attr.Name.StartsWith("^") && Regex.Match(context, attr.Name).Success)) .Any()).ToList(); if (actions.Any()) { foreach (Action action in actions) { try { action.Invoke(vaProxy); } catch (ArgumentNullException e) { this.Log.Error($"Missing parameter '{e.ParamName}' for context '{context}'"); } catch (ArgumentException e) when (e.ParamName == "context") { this.Log.Error($"Invalid plugin context '{vaProxy.Context}'."); } catch (Exception e) { this.Log.Error($"Unhandled exception while executing plugin context '{context}': {e.Message}"); } } } else { this.Log.Error($"Invalid plugin context '{vaProxy.Context}'."); } } } /// /// The Exit method, as required by the VoiceAttack plugin API. /// Runs when VoiceAttack is shut down. /// /// The VoiceAttack proxy object. protected void VaExit1(VoiceAttackProxyClass vaProxy) { this.ExitActions?.Invoke(vaProxy); } /// /// The StopCommand method, as required by the VoiceAttack plugin API. /// Runs whenever all commands are stopped using the “Stop All Commands” /// button or action. /// protected void VaStopCommand() { this.StopActions?.Invoke(); } /// /// Invoked when a variable changed. /// /// The name of the variable. /// The old value of the variable. /// The new value of the variable. /// The internal GUID of the variable. private void BooleanVariableChanged(string name, bool? from, bool? to, Guid? internalID = null) { foreach (Action action in this.BoolChangedHandlers.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == name || (attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success)) .Any()).ToList()) { try { action.Invoke(name, from, to); } catch (Exception e) { this.Log.Error($"Unhandled exception while handling changed bool variable '{name}': {e.Message}"); } } } /// /// Invoked when a variable changed. /// /// The name of the variable. /// The old value of the variable. /// The new value of the variable. /// The internal GUID of the variable. private void DateVariableChanged(string name, DateTime? from, DateTime? to, Guid? internalID = null) { foreach (Action action in this.DateTimeChangedHandlers.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == name || (attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success)) .Any()).ToList()) { try { action.Invoke(name, from, to); } catch (Exception e) { this.Log.Error($"Unhandled exception while handling changed DateTime variable '{name}': {e.Message}"); } } } /// /// Invoked when a variable changed. /// /// The name of the variable. /// The old value of the variable. /// The new value of the variable. /// The internal GUID of the variable. private void DecimalVariableChanged(string name, decimal? from, decimal? to, Guid? internalID = null) { foreach (Action action in this.DecimalChangedHandlers.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == name || (attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success)) .Any()).ToList()) { try { action.Invoke(name, from, to); } catch (Exception e) { this.Log.Error($"Unhandled exception while handling changed decimal variable '{name}': {e.Message}"); } } } /// /// Invoked when a variable changed. /// /// The name of the variable. /// The old value of the variable. /// The new value of the variable. /// The internal GUID of the variable. private void IntegerVariableChanged(string name, int? from, int? to, Guid? internalID = null) { foreach (Action action in this.IntChangedHandlers.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == name || (attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success)) .Any()).ToList()) { try { action.Invoke(name, from, to); } catch (Exception e) { this.Log.Error($"Unhandled exception while handling changed int variable '{name}': {e.Message}"); } } } /// /// Invoked when a variable changed. /// /// The name of the variable. /// The old value of the variable. /// The new value of the variable. /// The internal GUID of the variable. private void TextVariableChanged(string name, string? from, string? to, Guid? internalID = null) { if (name == $"{this.Name}.loglevel#") { try { this.Log.SetLogLevel(to); } catch (ArgumentException) { this.Log.Error($"Error setting log level: '{to!}' is not a valid log level."); } } foreach (Action action in this.StringChangedHandlers.Where( action => action.Method.GetCustomAttributes().Where( attr => attr.Name == name || (attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success)) .Any()).ToList()) { try { action.Invoke(name, from, to); } catch (Exception e) { this.Log.Error($"Unhandled exception while handling changed string variable '{name}': {e.Message}"); } } } /// /// A list of event handlers (Actions). Basically just a list that /// implements the + and - operators because they look nice. /// /// The type of the list. protected class HandlerList : List { /// /// Adds a handler to the list. /// /// The list to add to. /// The handler to add. /// The sum of both. public static HandlerList operator +(HandlerList handlers, T item) { handlers.Add(item); return handlers; } /// /// Removes a handler from the list. /// /// The list to remove from. /// The handler to remove. /// The list without the handler. public static HandlerList operator -(HandlerList handlers, T item) { handlers.Remove(item); return handlers; } } /// /// Denotes a handler for . /// [AttributeUsage(AttributeTargets.Method)] protected class InitAttribute : Attribute { } /// /// Denotes a handler for . /// [AttributeUsage(AttributeTargets.Method)] protected class ExitAttribute : Attribute { } /// /// Denotes a handler for . /// [AttributeUsage(AttributeTargets.Method)] protected class StopCommandAttribute : Attribute { } /// /// Denotes a handler for a plugin contexts. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class ContextAttribute : Attribute { /// /// Initializes a new instance of the /// class. /// /// The name of or regex for the context. public ContextAttribute(string name) { this.Name = name.ToLower(); } /// /// Initializes a new instance of the /// class that will be invoked for all contexts. /// public ContextAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the context. /// public string Name { get; } } /// /// Denotes a handler for changed variables. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class BoolAttribute : Attribute { /// /// Initializes a new instance of the /// class. /// /// The name of or regex for the variable. public BoolAttribute(string name) { this.Name = name; } /// /// Initializes a new instance of the /// class that will be invoked for all variables. /// public BoolAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the variable name. /// public string Name { get; } } /// /// Denotes a handler for changed variables. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class DateTimeAttribute : Attribute { /// /// Initializes a new instance of the class. /// /// The name of or regex for the variable. public DateTimeAttribute(string name) { this.Name = name; } /// /// Initializes a new instance of the class that will be invoked for all /// variables. /// public DateTimeAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the variable. /// public string Name { get; } } /// /// Denotes a handler for changed variables. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class DecimalAttribute : Attribute { /// /// Initializes a new instance of the /// class. /// /// The name of or regex for the variable. public DecimalAttribute(string name) { this.Name = name; } /// /// Initializes a new instance of the /// class that will be invoked for all variables. /// public DecimalAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the variable. /// public string Name { get; } } /// /// Denotes a handler for changed variables. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class IntAttribute : Attribute { /// /// Initializes a new instance of the /// class. /// /// The name of or regex for the variable. public IntAttribute(string name) { this.Name = name; } /// /// Initializes a new instance of the /// class that will be invoked for all variables. /// public IntAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the variable. /// public string Name { get; } } /// /// Denotes a handler for changed variables. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] protected class StringAttribute : Attribute { /// /// Initializes a new instance of the /// class. /// /// The name of or regex for the variable. public StringAttribute(string name) { this.Name = name; } /// /// Initializes a new instance of the /// class that is invoked for all variables. /// public StringAttribute() { this.Name = "^.*"; } /// /// Gets the name of or regex for the variable. /// public string Name { get; } } } }