documentation

plus some refactoring / added code on the way …
This commit is contained in:
alterNERDtive 2022-07-11 01:46:42 +02:00
parent 0e477058cc
commit 5b6db94bce
21 changed files with 971 additions and 92 deletions

View file

@ -19,9 +19,10 @@
using System;
using alterNERDtive.Yavapf;
using VoiceAttack;
namespace alterNERDtive.Yavapf.Example
namespace alterNERDtive.Example
{
/// <summary>
/// This is an example for a VoiceAttack plugin using YAVAPF.
@ -163,7 +164,7 @@ namespace alterNERDtive.Yavapf.Example
/// An example handler for VA_StopCommand. If your plugin needs to
/// execute anything when all commands are stopped this is the place.
/// </summary>
[Stop]
[StopCommand]
public static void Stop()
{
Plugin.Log.Notice("This is the example Stop handler method.");
@ -197,8 +198,8 @@ namespace alterNERDtive.Yavapf.Example
/// contexts that begin with “foo” or contain “bar”.
/// </summary>
/// <param name="vaProxy">The current VoiceAttack proxy object.</param>
[Context("^foo.*")]
[Context("^.*bar.*")]
[Context(@"^foo.*")]
[Context(@"^.*bar.*")]
public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy)
{
Plugin.Log.Notice(
@ -212,9 +213,8 @@ namespace alterNERDtive.Yavapf.Example
/// <param name="name">The name of the variable.</param>
/// <param name="from">The old value of the variable.</param>
/// <param name="to">The new value of the variable.</param>
/// <param name="internalID">The GUID of the variable.</param>
[Bool("isDay#")]
public static void DayChanged(string name, bool? from, bool? to, Guid? internalID)
public static void DayChanged(string name, bool? from, bool? to)
{
Plugin.Log.Notice($"This is the example handler for changed bool variables. It is now {(to ?? false ? "day" : "night")}.");
}
@ -226,11 +226,14 @@ namespace alterNERDtive.Yavapf.Example
/// <param name="name">The name of the variable.</param>
/// <param name="from">The old value of the variable.</param>
/// <param name="to">The new value of the variable.</param>
/// <param name="internalID">The GUID of the variable.</param>
[String]
public static void StringChanged(string name, string? from, string? to, Guid? internalID)
public static void StringChanged(string name, string? from, string? to)
{
Plugin.Log.Notice($"This is the example handler for changed string variables. '{name}' changed from '{from ?? "Not Set"}' to '{to ?? "Not Set"}'.");
// exclude log level changes
if (name != $"{Plugin.Name}.loglevel#")
{
Plugin.Log.Notice($"This is the example handler for changed string variables. '{name}' changed from '{from ?? "Not Set"}' to '{to ?? "Not Set"}'.");
}
}
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<RootNamespace>alterNERDtive.Yavapf.Example</RootNamespace>
<RootNamespace>alterNERDtive.Example</RootNamespace>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@ -20,6 +20,7 @@
<Reference Include="VoiceAttack">
<HintPath>C:\Program Files\VoiceAttack\VoiceAttack.exe</HintPath>
<Private>False</Private>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
</ItemGroup>

View file

@ -23,4 +23,4 @@
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Its my name, you prick :)", Scope = "namespace", Target = "~N:alterNERDtive.Yavapf.Example")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Its my name, you prick :)", Scope = "namespace", Target = "~N:alterNERDtive.Example")]

View file

@ -0,0 +1,125 @@
// <copyright file="MinimumViablePlugin.cs" company="alterNERDtive">
// 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 &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
using System;
using alterNERDtive.Yavapf;
namespace alterNERDtive.Example
{
/// <summary>
/// This is an example for a VoiceAttack plugin using YAVAPF.
///
/// You can use this class and this project as base for your own implementation.
/// </summary>
public class MinimumViablePlugin : VoiceAttackPlugin
{
private static readonly MinimumViablePlugin Plugin;
/// <summary>
/// Initializes static members of the <see cref="MinimumViablePlugin"/> class.
///
/// Since VoiceAttacks plugin API requires a bunch of static methods
/// instead of instantiating a plugin class, the “Constructor” here also
/// needs to be static. It is executed right before a static method is
/// used for the first time, which would usually be when VoiceAttack
/// calls the <see cref="VA_Init1(dynamic)"/> method.
/// </summary>
static MinimumViablePlugin()
{
// You can generate a GUID in Visual Studio under “Tools” → “Create
// GUID”. Choose “Registry Format”.
Plugin = new ()
{
Name = "Minimum Viable Plugin",
Version = "0.0.1",
Info = "This is a description",
Guid = "{2E5CDD74-0E05-4745-A791-76E8C5AABBC3}",
};
}
/// <summary>
/// The plugins display name, as required by the VoiceAttack plugin
/// API.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <returns>The display name.</returns>
public static string VA_DisplayName() => Plugin.VaDisplayName();
/// <summary>
/// The plugins description, as required by the VoiceAttack plugin API.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <returns>The description.</returns>
public static string VA_DisplayInfo() => Plugin.VaDisplayInfo();
/// <summary>
/// The plugins GUID, as required by the VoiceAttack plugin API.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <returns>The GUID.</returns>
public static Guid VA_Id() => Plugin.VaId();
/// <summary>
/// The Init method, as required by the VoiceAttack plugin API.
/// Runs when the plugin is initially loaded.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
public static void VA_Init1(dynamic vaProxy) => Plugin.VaInit1(vaProxy);
/// <summary>
/// The Invoke method, as required by the VoiceAttack plugin API.
/// Runs whenever a plugin context is invoked.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
public static void VA_Invoke1(dynamic vaProxy) => Plugin.VaInvoke1(vaProxy);
/// <summary>
/// The Exit method, as required by the VoiceAttack plugin API.
/// Runs when VoiceAttack is shut down.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
/// <param name="vaProxy">The VoiceAttack proxy object.</param>
public static void VA_Exit1(dynamic vaProxy) => Plugin.VaExit1(vaProxy);
/// <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.
///
/// Since it is required to be static, it must be defined in your plugin
/// class for VoiceAttack to pick it up as a plugin.
/// </summary>
public static void VA_StopCommand() => Plugin.VaStopCommand();
}
}

View file

@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace alterNERDtive.Yavapf.Example.Properties {
namespace alterNERDtive.Example.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]

View file

@ -16,6 +16,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
LICENSE = LICENSE
mkdocs.yml = mkdocs.yml
README.md = README.md
requirements.txt = requirements.txt
stylecop.json = stylecop.json
EndProjectSection
EndProject
@ -42,13 +43,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{3FE46F37-0
docs\commands.md = docs\commands.md
docs\contexts.md = docs\contexts.md
docs\events.md = docs\events.md
docs\extra.css = docs\extra.css
docs\gettingstarted.md = docs\gettingstarted.md
docs\faq.md = docs\faq.md
docs\index.md = docs\index.md
docs\troubleshooting.md = docs\troubleshooting.md
docs\logging.md = docs\logging.md
docs\variables.md = docs\variables.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoiceAttack-Framework-Test", "VoiceAttack-Framework-Test\VoiceAttack-Framework-Test.csproj", "{D49B2D9A-1014-4554-977A-E57E9972D196}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VoiceAttack-Framework-Test", "VoiceAttack-Framework-Test\VoiceAttack-Framework-Test.csproj", "{D49B2D9A-1014-4554-977A-E57E9972D196}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -4,7 +4,7 @@
<TargetFrameworks>net48</TargetFrameworks>
<RootNamespace>alterNERDtive.Yavapf</RootNamespace>
<PackageId>alterNERDtive.YAVAPF</PackageId>
<Version>0.0.1</Version>
<Version>0.1.0</Version>
<Company>alterNERDtive</Company>
<Authors>alterNERDtive</Authors>
<Description>YAVAPF is yet another VoiceAttack plugin framework.</Description>
@ -15,7 +15,7 @@
<PackageIconUrl />
<RepositoryUrl>https://github.com/alterNERDtive/YAVAPF</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>VoiceAttack</PackageTags>
<PackageTags>VoiceAttack;plugin;framework</PackageTags>
<NeutralLanguage>en</NeutralLanguage>
<Nullable>enable</Nullable>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
@ -44,6 +44,7 @@
<Reference Include="VoiceAttack">
<HintPath>C:\Program Files\VoiceAttack\VoiceAttack.exe</HintPath>
<Private>False</Private>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
</ItemGroup>

View file

@ -54,8 +54,12 @@ namespace alterNERDtive.Yavapf
get => logLevel ?? Yavapf.LogLevel.NOTICE;
set
{
logLevel = value;
this.Notice($"Log level set to {value ?? Yavapf.LogLevel.NOTICE}.");
if (value != logLevel)
{
logLevel = value;
this.vaProxy.SetText($"{this.id}.loglevel#", value.ToString().ToLower());
this.Notice($"Log level set to {value ?? Yavapf.LogLevel.NOTICE}.");
}
}
}
@ -65,7 +69,9 @@ namespace alterNERDtive.Yavapf
/// Valid values are ERROR, WARN, NOTICE, INFO and DEBUG.
/// </summary>
/// <param name="level">The new log level.</param>
public void SetLogLevel(string level)
/// <exception cref="ArgumentException">Thrown when <paramref
/// name="level"/>is not a valid log level.</exception>
public void SetLogLevel(string? level)
{
if (level == null)
{
@ -77,25 +83,6 @@ namespace alterNERDtive.Yavapf
}
}
/// <summary>
/// Logs a given message with the specified log level.
///
/// If the current log level is higher than the messages log level it
/// will not be logged.
/// </summary>
/// <param name="message">The message to be logged.</param>
/// <param name="level">The desired log level.</param>
/// <exception cref="ArgumentNullException">Thrown when the message is null.</exception>
public void Log(string message, LogLevel level = Yavapf.LogLevel.INFO)
{
_ = message ?? throw new ArgumentNullException("message");
if (level <= this.LogLevel)
{
this.vaProxy.WriteToLog($"{level} | {this.id}: {message}", LogColour[(int)level]);
}
}
/// <summary>
/// Logs a given message with the ERROR log level.
/// </summary>
@ -135,6 +122,25 @@ namespace alterNERDtive.Yavapf
/// </summary>
/// <param name="message">The message to be logged.</param>
public void Debug(string message) => this.Log(message, Yavapf.LogLevel.DEBUG);
/// <summary>
/// Logs a given message with the specified log level.
///
/// If the current log level is higher than the messages log level it
/// will not be logged.
/// </summary>
/// <param name="message">The message to be logged.</param>
/// <param name="level">The desired log level.</param>
/// <exception cref="ArgumentNullException">Thrown when the message is null.</exception>
private void Log(string message, LogLevel level = Yavapf.LogLevel.INFO)
{
_ = message ?? throw new ArgumentNullException("message");
if (level <= this.LogLevel)
{
this.vaProxy.WriteToLog($"{level} | {this.id}: {message}", LogColour[(int)level]);
}
}
}
/// <summary>

View file

@ -62,31 +62,31 @@ namespace alterNERDtive.Yavapf
/// Gets or sets the Actions to be run when a <see cref="bool"/>
/// variable changed.
/// </summary>
protected HandlerList<Action<string, bool?, bool?, Guid?>> BoolChangedHandlers { get; set; } = new ();
protected HandlerList<Action<string, bool?, bool?>> BoolChangedHandlers { get; set; } = new ();
/// <summary>
/// Gets or sets the Actions to be run when a <see cref="DateTime"/>
/// variable changed.
/// </summary>
protected HandlerList<Action<string, DateTime?, DateTime?, Guid?>> DateTimeChangedHandlers { get; set; } = new ();
protected HandlerList<Action<string, DateTime?, DateTime?>> DateTimeChangedHandlers { get; set; } = new ();
/// <summary>
/// Gets or sets the Actions to be run when a <see cref="decimal"/>
/// variable changed.
/// </summary>
protected HandlerList<Action<string, decimal?, decimal?, Guid?>> DecimalChangedHandlers { get; set; } = new ();
protected HandlerList<Action<string, decimal?, decimal?>> DecimalChangedHandlers { get; set; } = new ();
/// <summary>
/// Gets or sets the Actions to be run when a <see cref="int"/>
/// variable changed.
/// </summary>
protected HandlerList<Action<string, int?, int?, Guid?>> IntChangedHandlers { get; set; } = new ();
protected HandlerList<Action<string, int?, int?>> IntChangedHandlers { get; set; } = new ();
/// <summary>
/// Gets or sets the Actions to be run when a <see cref="string"/>
/// variable changed.
/// </summary>
protected HandlerList<Action<string, string?, string?, Guid?>> StringChangedHandlers { get; set; } = new ();
protected HandlerList<Action<string, string?, string?>> StringChangedHandlers { get; set; } = new ();
/// <summary>
/// Gets the currently stored VoiceAttackInitProxyClass object which is
@ -271,26 +271,26 @@ namespace alterNERDtive.Yavapf
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<ExitAttribute>().Any()).ToList().ForEach(
m => this.ExitActions += (Action<VoiceAttackProxyClass>)m.CreateDelegate(typeof(Action<VoiceAttackProxyClass>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<StopAttribute>().Any()).ToList().ForEach(
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<StopCommandAttribute>().Any()).ToList().ForEach(
m => this.StopActions += (Action)m.CreateDelegate(typeof(Action)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<ContextAttribute>().Any()).ToList().ForEach(
m => this.Contexts += (Action<VoiceAttackInvokeProxyClass>)m.CreateDelegate(typeof(Action<VoiceAttackInvokeProxyClass>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<BoolAttribute>().Any()).ToList().ForEach(
m => this.BoolChangedHandlers += (Action<string, bool?, bool?, Guid?>)m.CreateDelegate(typeof(Action<string, bool?, bool?, Guid?>)));
m => this.BoolChangedHandlers += (Action<string, bool?, bool?>)m.CreateDelegate(typeof(Action<string, bool?, bool?>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<DateTimeAttribute>().Any()).ToList().ForEach(
m => this.DateTimeChangedHandlers += (Action<string, DateTime?, DateTime?, Guid?>)m.CreateDelegate(typeof(Action<string, DateTime?, DateTime?, Guid?>)));
m => this.DateTimeChangedHandlers += (Action<string, DateTime?, DateTime?>)m.CreateDelegate(typeof(Action<string, DateTime?, DateTime?>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<DecimalAttribute>().Any()).ToList().ForEach(
m => this.DecimalChangedHandlers += (Action<string, decimal?, decimal?, Guid?>)m.CreateDelegate(typeof(Action<string, decimal?, decimal?, Guid?>)));
m => this.DecimalChangedHandlers += (Action<string, decimal?, decimal?>)m.CreateDelegate(typeof(Action<string, decimal?, decimal?>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<IntAttribute>().Any()).ToList().ForEach(
m => this.IntChangedHandlers += (Action<string, int?, int?, Guid?>)m.CreateDelegate(typeof(Action<string, int?, int?, Guid?>)));
m => this.IntChangedHandlers += (Action<string, int?, int?>)m.CreateDelegate(typeof(Action<string, int?, int?>)));
this.GetType().GetMethods().Where(m => m.GetCustomAttributes<StringAttribute>().Any()).ToList().ForEach(
m => this.StringChangedHandlers += (Action<string, string?, string?, Guid?>)m.CreateDelegate(typeof(Action<string, string?, string?, Guid?>)));
m => this.StringChangedHandlers += (Action<string, string?, string?>)m.CreateDelegate(typeof(Action<string, string?, string?>)));
this.Log.Debug("Running Init handlers …");
this.InitActions?.Invoke(vaProxy);
@ -311,33 +311,79 @@ namespace alterNERDtive.Yavapf
string context = vaProxy.Context.ToLower();
List<Action<VoiceAttackInvokeProxyClass>> actions = this.Contexts.Where(
action => action.Method.GetCustomAttributes<ContextAttribute>().Where(
attr => attr.Name == context ||
(attr.Name.StartsWith("^") && Regex.Match(context, attr.Name).Success))
.Any()).ToList();
if (actions.Any())
if (context.StartsWith("log."))
{
foreach (Action<VoiceAttackInvokeProxyClass> action in actions)
try
{
try
string message = this.Get<string>("~message") ?? throw new ArgumentNullException("~message");
switch (context)
{
action.Invoke(vaProxy);
}
catch (ArgumentNullException e)
{
this.Log.Error($"Missing parameter '{e.ParamName}' for context '{context}'");
}
catch (Exception e)
{
this.Log.Error($"Unhandled exception while executing plugin context '{context}': {e.Message}");
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
{
this.Log.Error($"Invalid plugin context '{vaProxy.Context}'.");
List<Action<VoiceAttackInvokeProxyClass>> actions = this.Contexts.Where(
action => action.Method.GetCustomAttributes<ContextAttribute>().Where(
attr => attr.Name == context ||
(attr.Name.StartsWith("^") && Regex.Match(context, attr.Name).Success))
.Any()).ToList();
if (actions.Any())
{
foreach (Action<VoiceAttackInvokeProxyClass> 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}'.");
}
}
}
@ -370,7 +416,7 @@ namespace alterNERDtive.Yavapf
/// <param name="internalID">The internal GUID of the variable.</param>
private void BooleanVariableChanged(string name, bool? from, bool? to, Guid? internalID = null)
{
foreach (Action<string, bool?, bool?, Guid?> action in this.BoolChangedHandlers.Where(
foreach (Action<string, bool?, bool?> action in this.BoolChangedHandlers.Where(
action => action.Method.GetCustomAttributes<BoolAttribute>().Where(
attr => attr.Name == name ||
(attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success))
@ -378,7 +424,7 @@ namespace alterNERDtive.Yavapf
{
try
{
action.Invoke(name, from, to, internalID);
action.Invoke(name, from, to);
}
catch (Exception e)
{
@ -396,7 +442,7 @@ namespace alterNERDtive.Yavapf
/// <param name="internalID">The internal GUID of the variable.</param>
private void DateVariableChanged(string name, DateTime? from, DateTime? to, Guid? internalID = null)
{
foreach (Action<string, DateTime?, DateTime?, Guid?> action in this.DateTimeChangedHandlers.Where(
foreach (Action<string, DateTime?, DateTime?> action in this.DateTimeChangedHandlers.Where(
action => action.Method.GetCustomAttributes<DateTimeAttribute>().Where(
attr => attr.Name == name ||
(attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success))
@ -404,7 +450,7 @@ namespace alterNERDtive.Yavapf
{
try
{
action.Invoke(name, from, to, internalID);
action.Invoke(name, from, to);
}
catch (Exception e)
{
@ -422,7 +468,7 @@ namespace alterNERDtive.Yavapf
/// <param name="internalID">The internal GUID of the variable.</param>
private void DecimalVariableChanged(string name, decimal? from, decimal? to, Guid? internalID = null)
{
foreach (Action<string, decimal?, decimal?, Guid?> action in this.DecimalChangedHandlers.Where(
foreach (Action<string, decimal?, decimal?> action in this.DecimalChangedHandlers.Where(
action => action.Method.GetCustomAttributes<DecimalAttribute>().Where(
attr => attr.Name == name ||
(attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success))
@ -430,7 +476,7 @@ namespace alterNERDtive.Yavapf
{
try
{
action.Invoke(name, from, to, internalID);
action.Invoke(name, from, to);
}
catch (Exception e)
{
@ -448,7 +494,7 @@ namespace alterNERDtive.Yavapf
/// <param name="internalID">The internal GUID of the variable.</param>
private void IntegerVariableChanged(string name, int? from, int? to, Guid? internalID = null)
{
foreach (Action<string, int?, int?, Guid?> action in this.IntChangedHandlers.Where(
foreach (Action<string, int?, int?> action in this.IntChangedHandlers.Where(
action => action.Method.GetCustomAttributes<IntAttribute>().Where(
attr => attr.Name == name ||
(attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success))
@ -456,7 +502,7 @@ namespace alterNERDtive.Yavapf
{
try
{
action.Invoke(name, from, to, internalID);
action.Invoke(name, from, to);
}
catch (Exception e)
{
@ -474,7 +520,19 @@ namespace alterNERDtive.Yavapf
/// <param name="internalID">The internal GUID of the variable.</param>
private void TextVariableChanged(string name, string? from, string? to, Guid? internalID = null)
{
foreach (Action<string, string?, string?, Guid?> action in this.StringChangedHandlers.Where(
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<string, string?, string?> action in this.StringChangedHandlers.Where(
action => action.Method.GetCustomAttributes<StringAttribute>().Where(
attr => attr.Name == name ||
(attr.Name.StartsWith("^") && Regex.Match(name, attr.Name).Success))
@ -482,7 +540,7 @@ namespace alterNERDtive.Yavapf
{
try
{
action.Invoke(name, from, to, internalID);
action.Invoke(name, from, to);
}
catch (Exception e)
{
@ -545,7 +603,7 @@ namespace alterNERDtive.Yavapf
/// Denotes a handler for <see cref="VaStopCommand()"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
protected class StopAttribute : Attribute
protected class StopCommandAttribute : Attribute
{
}
@ -562,7 +620,7 @@ namespace alterNERDtive.Yavapf
/// <param name="name">The name of or regex for the context.</param>
public ContextAttribute(string name)
{
this.Name = name;
this.Name = name.ToLower();
}
/// <summary>

View file

@ -0,0 +1,3 @@
# Executing VoiceAttack Commands
Not implemented yet.

View file

@ -0,0 +1,171 @@
# Defining Plugin Contexts
Plugin contexts are defined similarly to [event handlers](events.md).
They are `public static` methods of your plugin class that must have a
`ContextAttribute` and must accept a `VoiceAttackInvokeProxyClass` parameter.
`ContextAttribute` has a single property `Name`. `Name` can either be the name
of a plugin context or a regular expression defining all plugin contexts it
should be associated with. `Name` is set through an optional parameter of the
attribute constructor; if it is omitted, the method will be executed for any
plugin context.
A method can have multiple `ContextAttribute`s. It will be executed if any of
them matches the context of a plugin invocation. That also means that you can
have several methods that handle the same plugin context; as with [event
handlers](events.md), a specific order of execution cannot be guaranteed.
If your method handles multiple contexts, the context it was invoked with can be
found in the `Context` property of its `VoiceAttackInvokeProxyClass` parameter.
**Note**: The `log.*` context is reserved [for
logging](logging.md#from-a-voiceattack-command) and cannot be used for your
plugin.
## Named Plugin Contexts
For singular context names, add a `ContextAttribute` for each name. Context
names are to be lower case by convention.
This should be the default way to handle plugin contexts, and multiple contexts
handled by the same method should be alternate names for the same functionality.
Separate functionality, separate handler method(s).
```csharp
[Context("test")]
[Context("test context")]
[Context("alternate context name")]
public static void TestContext(VoiceAttackInvokeProxyClass vaProxy) {
[…]
}
```
## Regular Expression Plugin Contexts
For contexts defined by regular expressions, the `Name` property must start with
a `^` to be recognized as a regular expression. Incidentally that means you have
to define your regular expression to match from the beginning of the context
string.
The main use case for regular expression contexts is grouping contexts that
logically belong together or behave in very similar ways. For example you could
have a single `^edsm\..*` context in an Elite Dangerous related plugin that
handles anything related to querying [EDSM](https://edsm.net).
As with [catchall contexts](#catchall-plugin-contexts), you should probably have
some kind of way to differentiate between contexts. For any contexts that match
the regular expression(s) but are not valid contexts for your plugin, `throw` an
`ArgumentException` with “context” as the parameter name. The exception message
can be anything, it will not be used.
Oh, and of course you can combine named and regular expression contexts. This
example features some different regular expressions and corresponding
conditionals:
```csharp
[Context(@"^foo.*")]
[Context(@"^.*bar.*")]
[Context(@"^.*baz")]
[Context("some name")]
public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy) {
string context = vaProxy.Context;
if (context.StartsWith("foo")) {
[…]
}
else if (context.Contains("bar")) {
[…]
}
else if (context.EndsWith("baz")) {
[…]
}
else if (context == "some name")) {
[…]
}
else {
throw new ArgumentException("", "context");
}
}
```
This example is more focused and closer to how regular expression contexts are
intended to be used in practice:
```csharp
[Context(@"^edsm\..*")]
public static void EdsmContext(VoiceAttackInvokeProxyClass vaProxy) {
switch(vaProxy.Context)
{
case "edsm.findsystem":
[…]
break;
case "edsm.findcommander":
[…]
break;
case "edsm.trafficreport":
[…]
break;
default:
throw new ArgumentException("", "context");;
}
}
```
## “Catchall” Plugin Contexts
To have a method invoked on any plugin invocation regardless of context, add a
`ContextAttribute` and omit the `Name`.
**This is not recommended** and has similar issues to using the bare VoiceAttack
plugin API. It is mostly provided for backwards compatibility; you can easily
convert your old `VA_Invoke1(dynamic)` method to a catchall plugin context and
then modify from there.
As with [regular expression contexts](#regular-expression-plugin-contexts), you
should probably have some kind of way to differentiate between contexts. For any
contexts that are not valid contexts for your plugin, `throw` an
`ArgumentException` with “context” as the parameter name. The exception message,
again, doesnt matter.
```csharp
[Context]
public static void CatchallContext(VoiceAttackInvokeProxyClass vaProxy) {
switch (vaProxy.Context)
{
case "some context":
[…]
break;
case "some other context":
[…]
break;
default:
throw new ArgumentException("", "context");;
}
}
```
## Context Parameters
VoiceAttack plugin contexts by design do not have any parameters. If you need
data passed from a VoiceAttack command to the plugin when a context is invoked,
you will have to set a variable in your VoiceAttack command and then retrieve
said variable from the context handler method.
In general it is recommended to provide context parameters as command scoped
variables (`~` prefix) in order not to interfere with other commands / plugin
invocations and their data.
This example accesses the `~test` text variable from plugin code:
```csharp
string? testParameter = Plugin.Get<string>("~test");
```
In case a parameter is missing that is _required_ for your context `throw` an
`ArgumentNullException` with the variable name as the parameter name:
```csharp
string testParameter = Plugin.Get<string>("~test") ?? throw new ArgumentNullException("~test");
```
[More about variables](variables.md).

View file

@ -0,0 +1,61 @@
# Handling VoiceAttack Events
In order to handle VoiceAttacks `Init`, `Exit` and `StopCommand` events, you
will have to define corresponding event handlers. [The `Invoke` event is handled
separately](contexts.md).
Generally speaking, event handlers in YAVAPF are `public static` methods of your
plugin class that must have certain attributes associated to them and must have
the correct method signature.
An event can have as many handlers as you require. Do note that a specific order
of execution cannot be guaranteed.
## Init
`Init` handlers are invoked when VoiceAttack inintializes your plugin. That
happens exactly once at application startup. Use these to setup your plugin for
use.
`Init` handlers must accept a single `VoiceAttackInitProxyClass` parameter and
must have an `InitAttribute`. `InitAttribute` does not have any properties.
```csharp
[Init]
public static void MyInitHandler(VoiceAttackInitProxyClass vaProxy) {
[…]
}
```
## Exit
`Exit` handlers are invoked when VoiceAttack closes. That happens exactly once
at application shutdown. Use these to gracefully tear down anything your plugin
has to tear down.
`Exit` handlers must accept a single `VoiceAttackProxyClass` parameter and must
have an `ExitAttribute`. `ExitAttribute` does not have any properties.
```csharp
[Exit]
public static void MyExitHandler(VoiceAttackProxyClass vaProxy) {
[…]
}
```
## StopCommand
`StopCommand` handlers are invoked whenever VoiceAttack stops all commands. That
happens e.g. when a “Stop all commands” command action is executed or when the
“Stop Commands” button on the main interface is pressed. If your plugin has to
respond to that, use these.
`StopCommand` handlers must have no parameters and must have a
`StopCommandAttribute`. `StopCommandAttribute` does not have any properties.
```csharp
[StopCommand]
public static void MyStopCommandHandler() {
[…]
}
```

5
docs/extra.css Normal file
View file

@ -0,0 +1,5 @@
div.section > p,
div.section > ol,
div.section > ul {
text-align: justify
}

3
docs/faq.md Normal file
View file

@ -0,0 +1,3 @@
# Frequently Asked Questions
There doesnt seem to be anything here. Maybe you should ask one :)

View file

@ -0,0 +1,193 @@
# Getting Started
First off, you can see [the Example plugin
project](https://github.com/alterNERDtive/YAVAPF/tree/release/ExamplePlugin) on
Github for reference.
Second off, this documentation assumes that you have at least cross read the
section about plugins [in the VoiceAttack
manual](https://voiceattack.com/VoiceAttackHelp.pdf). If any terminology is new
to you, it is probably introduced there. Unlike said manual though this will
provide step by step instructions to get your plugin set up.
## Creating a Visual Studio Project
I am going to assume for this part of the documentation that you are using
Visual Studio 2022 or later (_not_ Visual Studio Code!) as your development
environment. [The Community Edition is free for unlimited time personal
use](https://visualstudio.microsoft.com/vs/compare/).
VoiceAttack is a .Net Framework 4.8 application. Plugins targeting .Net 5+ or
.Net Core will not work. I still recommend creating a .Net project instead of a
.Net Framework project, then changing the “Target Framework” to .Net Framework
4.8. This allows you to use the full `dotnet` tool chain, which makes e.g. using
Github Actions to build / release your project much less painful. Trust me, Ive
done it both ways.
So, create a new “Class Library” project, then use a text editor to change the
“TargetFrameworks” property to `net48`. While youre there you might also want
to change the “LanguageVerison” to `10`. Most new features are backwards
compatible with .Net Framework. The compiler will assist you with errors for
those that are not.
## Adding YAVAPF as a Dependency
This one is the simple part, just install `alterNERDtive.YAVAPF` through NuGet.
Done.
Alternatively you can add it manually by cloning
`github.com/alterNERDtive/YAVAPF.git` as a git submodule and referencing
`VoiceAttack-Framework\VoiceAttack-Framework.csproj` as a project reference.
But seriously, use NuGet. I havent taught myself how to release NuGet packages
just for you to ignore it!
## Adding VoiceAttack as a Dependency
This is a little more involved. In order to use the actual proxy classes from
VoiceAttack instead of the “official” crutch of `dynamic` types, you will need
to add an assembly reference to `VoiceAttack.exe`.
Right click → “Add” → “Assembly Reference…” → “Browse” → browse to the
VoiceAttack installation folder → select `VoiceAttack.exe` → hit “Add” → make
sure it is ticked in the list → hit “OK”.
Now, we want to _reference_ `VoiceAttack.exe`, but we dont want to _include_ it
when compiling the plugin. So select “VoiceAttack” in “Dependencies” →
“Assemblies” and make sure that both “Copy Local” and “Embed Interop Types” are
set to “No”.
Distributing `VoiceAttack.exe` with your plugin would technically be a copyright
violation. _Do_ make sure to take the steps outlined in the last paragraph to
prevent accidentally doing that! Using it as a reference assembly is generally
OK and I have received confirmation from Gary, the author of VoiceAttack.
## Setting Up Debugging Through VoiceAttack
In order to be able to run VoiceAttack when debugging and actually debug your
plugin, you will need to open “Debug” → “<your project\> Debug Properties”.
There you will need to “Create a new profile” → “Executable”. Set the path to
your VoiceAttack executable and any command line options you might prefer.
Personally I like to set a custom `-datadir` in order to not mess with my
regular profile database accidentally.
The example plugin project has a `Properties\launchSettings.sample.json` file
that you can copy to `Properties\launchSettings.json` and edit accordingly to
accomplish the same thing.
The last thing youll need to do is make your plugin available to VoiceAttack
in a place where it can find it. I have requested an equivalent `-appdir`
parameter, but as long as that is not available you will need to have your
plugin present inside the regular `Apps` folder of VoiceAttack. I recommend
creating a directory junction (`mklink /j`, or `New-Item -ItemType Junction` in
PowerShell) between an `Apps` subfolder and your projects debug output
directory (usually `<project name>\bin\Debug\net48` inside your solution
folder).
## Building Through Github Actions
If you, like me, want to automate building/testing/releasing through [Github
Actions](https://docs.github.com/en/actions), youll need to have VoiceAttack
available while building on the worker. Obviously that will only work on a
Windows worker.
I have created the
[`alterNERDtive/setup-voiceattack-action`](https://github.com/alterNERDtive/setup-voiceattack-action)
to facilitate that. Usage example:
```yaml
- name: Install VoiceAttack
uses: alterNERDtive/setup-voiceattack-action
with:
version: "1.10"
```
Make sure that the path to VoiceAttack on your machine (which is the path
referenced in the project file) matches the path where you install VoiceAttack
on the worker! Alternatively, if you have installed VoiceAttack in a custom
folder locally, you can create a symlink (`mklink`, or
`New-Item -ItemType SymbolicLink` in PowerShell) to your `VoiceAttack.exe`
location at `C:\Program Files\VoiceAttack\VoiceAttack.exe` and include that as
the assembly reference.
## Creating a Minimum Viable Plugin
A valid VoiceAttack plugin must implement a selection of public, static methods:
* `VA_DisplayName()`: Must return the name of the plugin.
* `VA_DisplayInfo()`: Must return the description of the plugin.
* `VA_Id()`: Must return the GUID of the plugin.
* `VA_Init1(dynamic)`: Is executed when the plugin is loaded into VoiceAttack.
* `VA_Invoke1(dynamic)`: Is executed whenever a plugin context is run from a
command.
* `VA_Exit1(dynamic)`: Is executed when VoiceAttack shuts down.
* `VA_StopCommand()`: Is executed when VoiceAttack stops all commands, e.g.
through the command action or main interface button.
When using YAVAPF these methods are to be passed straight to the corresponding
methods of a `VoiceAttackPlugin` object that handles most things for you. It has
a few required properties:
* `Name`: The name of the plugin.
* `Version`: The version of the plugin.
* `Info`: The description of the plugin.
* `Guid`: The GUID of the plugin.
All of those are `string`s for ease of use, though the `Guid` obviously has to
be a valid string representation of a GUID. You can generate one using “Tools” →
“Create GUID”. Make sure to select “Registry Format”.
For a YAVAPF plugin you will have to derive your plugin class from
`alterNERDtive.Yavapf.VoiceAttackPlugin`. Since VoiceAttacks plugin API relies
entirely on static methods, youll need to instantiate your plugin object in its
static constructor and hold it in a static variable for future reference (no pun
intended).
So a minimum viable plugin using YAVAPF looks kind of like this:
```csharp
using System;
using alterNERDtive.Yavapf;
namespace YourNamespace
{
public class YourPlugin : VoiceAttackPlugin
{
private static readonly YourPlugin Plugin;
static YourPlugin()
{
Plugin = new ()
{
Name = "Your Plugin",
Version = "0.0.1",
Info = "This is a description",
Guid = "{5E93F293-B2CB-4B3F-AFC5-AE500A7EEBA9}",
};
}
public static string VA_DisplayName() => Plugin.VaDisplayName();
public static string VA_DisplayInfo() => Plugin.VaDisplayInfo();
public static Guid VA_Id() => Plugin.VaId();
public static void VA_Init1(dynamic vaProxy) => Plugin.VaInit1(vaProxy);
public static void VA_Invoke1(dynamic vaProxy) => Plugin.VaInvoke1(vaProxy);
public static void VA_Exit1(dynamic vaProxy) => Plugin.VaExit1(vaProxy);
public static void VA_StopCommand() => Plugin.VaStopCommand();
}
}
```
Thats it! Technically youre done. Hit the debug button, and VoiceAttack should
find your plugin on startup, report loading it in the event log, and list it
under “Options” → “General” → “Plugin Manager”.
Of course you are only just getting started if you want your plugin to actually
_do_ something!

View file

@ -13,13 +13,23 @@ The goal is to get you up & running with as little code and as little knowledge
the inner workings of VoiceAttack as possible.
You can find an [example plugin on
Github](https://github.com/alterNERDtive/YAVAPF/tree/develop/ExamplePlugin).
Github](https://github.com/alterNERDtive/YAVAPF/tree/release/ExamplePlugin).
## Current Implementation Status
* [x] VoiceAttack plugin API
* [x] Handlers for Init/Invoke/Exit/StopCommand
* [x] Plugin contexts
* [x] Handlers for variable changed events
* [x] Logging to the VoiceAttack event log
* [ ] Logging to a log file
* [ ] Wrapper for executing commands
* [ ] Miscellaneous VoiceAttack proxy functionality
* [ ] Full unit test coverage 😬
## Need Help / Want to Contribute?
Have a look at [the troubleshooting
guide](https://alterNERDtive.github.io/YAVAPF/troubleshooting). If your problem
persists, please [file an
Have a look at [the FAQ](faq.md). If your problem persists, please [file an
issue](https://github.com/alterNERDtive/YAVAPF/issues/new). Thanks! :)
You can also [say “Hi” on Discord](https://discord.gg/3pWdJwfJc5) if that is

73
docs/logging.md Normal file
View file

@ -0,0 +1,73 @@
# Logging
YAVAPF allows logging to the VoiceAttack event log.
Logging to a log file is planned, but not implemented yet.
## Write a Log Line
To write a log message from plugin code, use the methods provided by the
`VoiceAttackLog` object availabe in the `Log` property of your plugin object.
There is one per log level.
```csharp
Plugin.Log.Error("Example error message.");
Plugin.Log.Debug("Just sent an error message.");
```
You can also log messages from a VoiceAttack command. Unlike a regular “Write to
Log” command action going through the plugin will enforce the correct format and
log level.
Your plugin will automatically provide the reserved plugin contexts
`log.<log level>` for each of the 5 log levels. Simply set a `~message` string
and call the appropriate plugin context. This should be equivalent to the code
above:
```
Set text [~message] to 'Example error message.'
Execute external plugin, 'Your Plugin v0.0.1' using context 'log.error' and wait for return
Set text [~message] to 'Just sent an error message.'
Execute external plugin, 'Your Plugin v0.0.1' using context 'log.debug' and wait for return
```
## Log Level
A message will be colour coded with the corresponding colour. If the log level
assigned to a message is below the current log level, it will not be displayed.
E.g. an `INFO` message will not be displayed by default since the default log
level is `NOTICE`. A `DEBUG` message will only ever be displayed if the current
log level is `DEBUG`.
| Log Level | Log Colour | Recommended Use
|-----------|---------------|----------------------------
| ERROR | 🟥 red | unrecoverable error
| WARN | 🟨 yellow | recoverable error, warning
| NOTICE | 🟩 green | noteworthy information
| INFO | 🟦 blue | miscellaneous information
| DEBUG | ⬜ gray | debugging
## Setting the Current Log Level
You can set the current log level by either setting the `LogLevel` property or
by invoking `SetLogLevel(string)` of your plugins `Log` property.
The latter is mostly useful when dealing with input from a VoiceAttack command.
Its parameter is not case sensitive.
```csharp
Plugin.Log.LogLevel = LogLevel.WARN;
Plugin.Log.SetLogLevel = "info";
```
You can also set the log level from a VoiceAttack command directly by simply
setting `<plugin name>.loglevel#` to the desired log level.
```
Set text [Your Plugin.loglevel#] to 'debug'
```
The variable changed event for this specific variable name will be handled by
YAVAPF internally. You can of course still define your own handlers in addition
to it.

View file

@ -0,0 +1,157 @@
# Using VoiceAttack Variables
VoiceAttack allows you to set a plethora of variables. Each variable has
* a name
* a type
* a scope
Variable names are unique to each variable type. E.g. you can have _both_ a text
variable `test` _and_ a boolean variable `test`.
In addition to that variable names are also unique to each _scope_; or,
technically speaking, the scope is part of the variable name. E.g. a text
variable `~test` is different from a text variable `>test`, and both are
different from a text variable `test`.
## Variable Types
| VoiceAttack type | .Net type | Note
|-----------------------|-----------|-------------------------------------
| Text | string |
| True/False (Boolean) | bool |
| Integer | int |
| Decimal | decimal |
| Date/Time | DateTime |
| Small Integer | short | deprecated; not supported by YAVAPF
Technical note: VoiceAttack internally holds a `Dictionary<string, T>` for each
variable type. That is why, unlike regular variables in a .Net scope, the names
are unique to their type.
## Variable Scopes
| Prefix | Scope | Accessibility
|-----------|-----------------------|-------------------------------------------------
| none | global | everywhere
| `>>` | profile, persistent | same profile, preserved across profile switches
| `>` | profile | same profile, reset on profile switch
| `~~` | command + subcommands | this command invocation and its subcommands
| `~` | command only | this command invocation
There is no scope that retains variable values across VoiceAttack restarts. You
can save variable values to / load them from the current profile using a “Set a
<Type\> Value command action and ticking the corresponding box, but there is
currently no way to do it from a plugin. Feature request pending.
Restricting variable scope as far as possible is recommended. For communication
between commands and their plugin invocations scope should almost always be
command only (`~`).
For global commands used by your plugin having some unique prefix is a sensible
idea. For example, YAVAPF automatically sets the text variable
`<plugin name>.version` to the current version of your plugin.
## Default Variables
By default, YAVAPF automatically sets the following variables for your plugin:
| Variable | Type | Description
|-------------------------------|-----------|--------------------------------------------
| `<plugin name>.version` | string | The current version of your plugin.
| `<plugin name>.initialized` | bool | The plugin has been initialized correctly.
## Getting Variable Values
To get the value of a variable, invoke the `Get<T>(string name)` method of your
plugin where `T` is the type of the variable and `name` is its name including
its scope:
```csharp
string? foo = Plugin.Get<string>("foo");
bool bar = Plugin.Get<bool>("~bar") ?? false;
```
Remember that variable values will be returned as `null` (“Not Set” in
VoiceAttack terminology) if they are currently not holding a value.
## Setting Variable Values
To set the value of a variable, invoke the `Set<T>(string name, T value)` method
of your plugin where `T` is the type of the variable, `name` is its name
including its scope and `value` is the desired new value:
```csharp
Plugin.Set<DateTime>("current", DateTime.Now);
Plugin.Set<int>(">>deaths", (Plugin.Get<int>(">>deaths") ?? 0) + 1);
```
## Clearing Variable Values
To clear a variable, invoke the `UnSet<T>(string name)` method of your plugin
where `T` is the type of the variable and `name` is its name including its
scope:
```csharp
Plugin.UnSet<decimal>("π");
```
Or `Set<T>(string, T)` it to `null`:
```csharp
Plugin.Set<string>(">fizzbang", null);
```
## Subscribing to “Variable Changed” Events
VoiceAttack allows triggering plugins when a variable value changes. Handlers
must have an Attribute corresponding to the variable type (`BoolAttribute`,
`DateTimeAttribute`, `DecimalAttribute`, `IntAttribute`, `StringAttribute`) and
accept the following parameters:
* `string name`: the name of the variable that has changed
* `T? from`: the old value of the variable
* `T? to`: the new value of the variable
where `T` is the type of the variable. Remember that at any point either `to`
or `from` might be `null`.
**Note**: In order for a variable to trigger variable changed events, the
variable name **must** end with a number sign (`#`)! This is a limitation
enforced by VoiceAttack. The purpose of this constraint is to not constantly
invoke plugins whenever any variable changes.
```csharp
[Bool("isDay#")]
public static void DayChanged(string name, bool? from, bool? to)
{
Plugin.Log.Notice($"It is now {(to ?? false ? "day" : "night")}.");
}
```
This constraint also applies to catchall handlers. Even though the following
method accepts any variable name, it will still only be invoked if the name of a
changed variable ends with a `#`. E.g. changing the text variable `foo#` will
invoke it while changing the text variable `foo` will not.
```csharp
[String]
public static void StringChanged(string name, string? from, string? to)
{
Plugin.Log.Notice($"Text variable '{name}' changed from '{from ?? "Not Set"}' to '{to ?? "Not Set"}'.");
}
```
[More on logging](logging.md).
Do be aware that changing the value of a variable from within its handler will
trigger another variable changed event and run the handler again. It is very
much possible to create an infinite loop.
Attribute names work in the same way as [context names](contexts.md). You can
have handlers for singular variable names, regular expressions, and catchall
handlers.
The text variable `<plugin name>.loglevel#` is handled by YAVAPF internally to
set the current log level. You can still add additional handlers for this
variable.

View file

@ -1,4 +1,4 @@
site_name: "bindED VoiceAttack plugin"
site_name: "YAVAPF Yet Another VoiceAttack Plugin Framework"
site_url: https://alterNERDtive.github.io/YAVAPF
repo_url: https://github.com/alterNERDtive/YAVAPF
edit_uri: "edit/devel/docs/"
@ -6,6 +6,8 @@ site_description: "YAVAPF is yet another VoiceAttack plugin framework."
site_author: "alterNERDtive"
remote_name: "origin"
extra_css: [extra.css]
theme:
name: readthedocs
prev_next_buttons_location: both
@ -18,15 +20,18 @@ markdown_extensions:
- toc:
permalink: True
- sane_lists
- pymdownx.tasklist
nav:
- 'Home': 'index.md'
- 'Usage':
- 'Getting Started': 'gettingstarted.md'
- 'Event Handlers': 'events.md'
- 'Plugin Contexts': 'contexts.md'
- 'Working with Variables': 'variables.md'
- 'Running Commands': 'commands.md'
- 'troubleshooting.md'
- 'gettingstarted.md'
- 'events.md'
- 'contexts.md'
- 'variables.md'
- 'commands.md'
- 'logging.md'
- Troubleshooting:
- 'faq.md'
- '⎋ Changelog': 'https://github.com/alterNERDtive/YAVAPF/blob/release/CHANGELOG.md'
- '⎋ Report a Bug': 'https://github.com/alterNERDtive/YAVAPF/issues/'

View file

@ -1 +1,2 @@
mkdocs-roamlinks-plugin
mkdocs-roamlinks-plugin
pymdown-extensions