//
// 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; }
}
}
}