diff --git a/ExamplePlugin/ExamplePlugin.cs b/ExamplePlugin/ExamplePlugin.cs
index 781f772..3a85eb9 100644
--- a/ExamplePlugin/ExamplePlugin.cs
+++ b/ExamplePlugin/ExamplePlugin.cs
@@ -19,9 +19,10 @@
using System;
+using alterNERDtive.Yavapf;
using VoiceAttack;
-namespace alterNERDtive.Yavapf.Example
+namespace alterNERDtive.Example
{
///
/// 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.
///
- [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”.
///
/// The current VoiceAttack proxy object.
- [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
/// The name of the variable.
/// The old value of the variable.
/// The new value of the variable.
- /// The GUID of the variable.
[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
/// The name of the variable.
/// The old value of the variable.
/// The new value of the variable.
- /// The GUID of the variable.
[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"}'.");
+ }
}
}
}
diff --git a/ExamplePlugin/ExamplePlugin.csproj b/ExamplePlugin/ExamplePlugin.csproj
index 996ab4c..0dd6441 100644
--- a/ExamplePlugin/ExamplePlugin.csproj
+++ b/ExamplePlugin/ExamplePlugin.csproj
@@ -2,7 +2,7 @@
net48
- alterNERDtive.Yavapf.Example
+ alterNERDtive.Example
enable
true
@@ -20,6 +20,7 @@
C:\Program Files\VoiceAttack\VoiceAttack.exe
False
+ False
diff --git a/ExamplePlugin/GlobalSuppressions.cs b/ExamplePlugin/GlobalSuppressions.cs
index 18fda33..2b12704 100644
--- a/ExamplePlugin/GlobalSuppressions.cs
+++ b/ExamplePlugin/GlobalSuppressions.cs
@@ -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 = "It’s 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 = "It’s my name, you prick :)", Scope = "namespace", Target = "~N:alterNERDtive.Example")]
diff --git a/ExamplePlugin/MinimumViablePlugin.cs b/ExamplePlugin/MinimumViablePlugin.cs
new file mode 100644
index 0000000..64f94ad
--- /dev/null
+++ b/ExamplePlugin/MinimumViablePlugin.cs
@@ -0,0 +1,125 @@
+//
+// 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 alterNERDtive.Yavapf;
+
+namespace alterNERDtive.Example
+{
+ ///
+ /// This is an example for a VoiceAttack plugin using YAVAPF.
+ ///
+ /// You can use this class and this project as base for your own implementation.
+ ///
+ public class MinimumViablePlugin : VoiceAttackPlugin
+ {
+ private static readonly MinimumViablePlugin Plugin;
+
+ ///
+ /// Initializes static members of the class.
+ ///
+ /// Since VoiceAttack’s 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 method.
+ ///
+ 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}",
+ };
+ }
+
+ ///
+ /// The plugin’s 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.
+ ///
+ /// The display name.
+ public static string VA_DisplayName() => Plugin.VaDisplayName();
+
+ ///
+ /// The plugin’s 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.
+ ///
+ /// The description.
+ public static string VA_DisplayInfo() => Plugin.VaDisplayInfo();
+
+ ///
+ /// The plugin’s 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.
+ ///
+ /// The GUID.
+ public static Guid VA_Id() => Plugin.VaId();
+
+ ///
+ /// 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.
+ ///
+ /// The VoiceAttack proxy object.
+ public static void VA_Init1(dynamic vaProxy) => Plugin.VaInit1(vaProxy);
+
+ ///
+ /// 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.
+ ///
+ /// The VoiceAttack proxy object.
+ public static void VA_Invoke1(dynamic vaProxy) => Plugin.VaInvoke1(vaProxy);
+
+ ///
+ /// 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.
+ ///
+ /// The VoiceAttack proxy object.
+ public static void VA_Exit1(dynamic vaProxy) => Plugin.VaExit1(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.
+ ///
+ /// Since it is required to be static, it must be defined in your plugin
+ /// class for VoiceAttack to pick it up as a plugin.
+ ///
+ public static void VA_StopCommand() => Plugin.VaStopCommand();
+ }
+}
diff --git a/ExamplePlugin/Properties/Settings.Designer.cs b/ExamplePlugin/Properties/Settings.Designer.cs
index 9a7f3e6..8c9570c 100644
--- a/ExamplePlugin/Properties/Settings.Designer.cs
+++ b/ExamplePlugin/Properties/Settings.Designer.cs
@@ -8,7 +8,7 @@
//
//------------------------------------------------------------------------------
-namespace alterNERDtive.Yavapf.Example.Properties {
+namespace alterNERDtive.Example.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
diff --git a/VoiceAttack-Framework.sln b/VoiceAttack-Framework.sln
index 49cc899..7eb2ac4 100644
--- a/VoiceAttack-Framework.sln
+++ b/VoiceAttack-Framework.sln
@@ -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
diff --git a/VoiceAttack-Framework/VoiceAttack-Framework.csproj b/VoiceAttack-Framework/VoiceAttack-Framework.csproj
index d1a8e1d..7b0c9bb 100644
--- a/VoiceAttack-Framework/VoiceAttack-Framework.csproj
+++ b/VoiceAttack-Framework/VoiceAttack-Framework.csproj
@@ -4,7 +4,7 @@
net48
alterNERDtive.Yavapf
alterNERDtive.YAVAPF
- 0.0.1
+ 0.1.0
alterNERDtive
alterNERDtive
YAVAPF is yet another VoiceAttack plugin framework.
@@ -15,7 +15,7 @@
https://github.com/alterNERDtive/YAVAPF
git
- VoiceAttack
+ VoiceAttack;plugin;framework
en
enable
True
@@ -44,6 +44,7 @@
C:\Program Files\VoiceAttack\VoiceAttack.exe
False
+ False
diff --git a/VoiceAttack-Framework/VoiceAttackLog.cs b/VoiceAttack-Framework/VoiceAttackLog.cs
index 44f0e8c..d170ceb 100644
--- a/VoiceAttack-Framework/VoiceAttackLog.cs
+++ b/VoiceAttack-Framework/VoiceAttackLog.cs
@@ -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.
///
/// The new log level.
- public void SetLogLevel(string level)
+ /// Thrown when is not a valid log level.
+ public void SetLogLevel(string? level)
{
if (level == null)
{
@@ -77,25 +83,6 @@ namespace alterNERDtive.Yavapf
}
}
- ///
- /// Logs a given message with the specified log level.
- ///
- /// If the current log level is higher than the message’s log level it
- /// will not be logged.
- ///
- /// The message to be logged.
- /// The desired log level.
- /// Thrown when the message is null.
- 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]);
- }
- }
-
///
/// Logs a given message with the ERROR log level.
///
@@ -135,6 +122,25 @@ namespace alterNERDtive.Yavapf
///
/// The message to be logged.
public void Debug(string message) => this.Log(message, Yavapf.LogLevel.DEBUG);
+
+ ///
+ /// Logs a given message with the specified log level.
+ ///
+ /// If the current log level is higher than the message’s log level it
+ /// will not be logged.
+ ///
+ /// The message to be logged.
+ /// The desired log level.
+ /// Thrown when the message is null.
+ 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]);
+ }
+ }
}
///
diff --git a/VoiceAttack-Framework/VoiceAttackPlugin.cs b/VoiceAttack-Framework/VoiceAttackPlugin.cs
index 00ba08b..e4a8de5 100644
--- a/VoiceAttack-Framework/VoiceAttackPlugin.cs
+++ b/VoiceAttack-Framework/VoiceAttackPlugin.cs
@@ -62,31 +62,31 @@ namespace alterNERDtive.Yavapf
/// Gets or sets the Actions to be run when a
/// variable changed.
///
- protected HandlerList> BoolChangedHandlers { get; set; } = new ();
+ protected HandlerList> BoolChangedHandlers { get; set; } = new ();
///
/// Gets or sets the Actions to be run when a
/// variable changed.
///
- protected HandlerList> DateTimeChangedHandlers { get; set; } = new ();
+ protected HandlerList> DateTimeChangedHandlers { get; set; } = new ();
///
/// Gets or sets the Actions to be run when a
/// variable changed.
///
- protected HandlerList> DecimalChangedHandlers { get; set; } = new ();
+ protected HandlerList> DecimalChangedHandlers { get; set; } = new ();
///
/// Gets or sets the Actions to be run when a
/// variable changed.
///
- protected HandlerList> IntChangedHandlers { get; set; } = new ();
+ protected HandlerList> IntChangedHandlers { get; set; } = new ();
///
/// Gets or sets the Actions to be run when a
/// variable changed.
///
- protected HandlerList> StringChangedHandlers { get; set; } = new ();
+ protected HandlerList> StringChangedHandlers { get; set; } = new ();
///
/// Gets the currently stored VoiceAttackInitProxyClass object which is
@@ -271,26 +271,26 @@ namespace alterNERDtive.Yavapf
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(
+ 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)));
+ 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)));
+ 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)));
+ 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)));
+ 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)));
+ m => this.StringChangedHandlers += (Action)m.CreateDelegate(typeof(Action)));
this.Log.Debug("Running Init handlers …");
this.InitActions?.Invoke(vaProxy);
@@ -311,33 +311,79 @@ namespace alterNERDtive.Yavapf
string context = vaProxy.Context.ToLower();
- 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())
+ if (context.StartsWith("log."))
{
- foreach (Action action in actions)
+ try
{
- try
+ string message = this.Get("~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> 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}'.");
+ }
}
}
@@ -370,7 +416,7 @@ namespace alterNERDtive.Yavapf
/// 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(
+ 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))
@@ -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
/// 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(
+ 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))
@@ -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
/// 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(
+ 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))
@@ -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
/// 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(
+ 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))
@@ -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
/// The internal GUID of the variable.
private void TextVariableChanged(string name, string? from, string? to, Guid? internalID = null)
{
- foreach (Action 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 action in this.StringChangedHandlers.Where(
action => action.Method.GetCustomAttributes().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 .
///
[AttributeUsage(AttributeTargets.Method)]
- protected class StopAttribute : Attribute
+ protected class StopCommandAttribute : Attribute
{
}
@@ -562,7 +620,7 @@ namespace alterNERDtive.Yavapf
/// The name of or regex for the context.
public ContextAttribute(string name)
{
- this.Name = name;
+ this.Name = name.ToLower();
}
///
diff --git a/docs/commands.md b/docs/commands.md
index e69de29..7bdcd93 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -0,0 +1,3 @@
+# Executing VoiceAttack Commands
+
+Not implemented yet.
diff --git a/docs/contexts.md b/docs/contexts.md
index e69de29..584eb28 100644
--- a/docs/contexts.md
+++ b/docs/contexts.md
@@ -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, doesn’t 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("~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("~test") ?? throw new ArgumentNullException("~test");
+```
+
+[More about variables](variables.md).
diff --git a/docs/events.md b/docs/events.md
index e69de29..909dbd6 100644
--- a/docs/events.md
+++ b/docs/events.md
@@ -0,0 +1,61 @@
+# Handling VoiceAttack Events
+
+In order to handle VoiceAttack’s `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() {
+ […]
+}
+```
diff --git a/docs/extra.css b/docs/extra.css
new file mode 100644
index 0000000..1cbf3b0
--- /dev/null
+++ b/docs/extra.css
@@ -0,0 +1,5 @@
+div.section > p,
+div.section > ol,
+div.section > ul {
+ text-align: justify
+}
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..8f181b1
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,3 @@
+# Frequently Asked Questions
+
+There doesn’t seem to be anything here. Maybe you should ask one :)
diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md
index e69de29..89ad747 100644
--- a/docs/gettingstarted.md
+++ b/docs/gettingstarted.md
@@ -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, I’ve
+done it both ways.
+
+So, create a new “Class Library” project, then use a text editor to change the
+“TargetFrameworks” property to `net48`. While you’re 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 haven’t 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 don’t 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” → “ 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 you’ll 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 project’s debug output
+directory (usually `\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), you’ll 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 VoiceAttack’s plugin API relies
+entirely on static methods, you’ll 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();
+ }
+}
+```
+
+That’s it! Technically you’re 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!
diff --git a/docs/index.md b/docs/index.md
index 9dd82ba..850d44b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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
diff --git a/docs/logging.md b/docs/logging.md
new file mode 100644
index 0000000..7409bb5
--- /dev/null
+++ b/docs/logging.md
@@ -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.` 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 plugin’s `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 `.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.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
deleted file mode 100644
index e69de29..0000000
diff --git a/docs/variables.md b/docs/variables.md
index e69de29..5e70b82 100644
--- a/docs/variables.md
+++ b/docs/variables.md
@@ -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` 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
+ 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
+`.version` to the current version of your plugin.
+
+## Default Variables
+
+By default, YAVAPF automatically sets the following variables for your plugin:
+
+| Variable | Type | Description
+|-------------------------------|-----------|--------------------------------------------
+| `.version` | string | The current version of your plugin.
+| `.initialized` | bool | The plugin has been initialized correctly.
+
+## Getting Variable Values
+
+To get the value of a variable, invoke the `Get(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("foo");
+bool bar = Plugin.Get("~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(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("current", DateTime.Now);
+Plugin.Set(">>deaths", (Plugin.Get(">>deaths") ?? 0) + 1);
+```
+
+## Clearing Variable Values
+
+To clear a variable, invoke the `UnSet(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("π");
+```
+
+Or `Set(string, T)` it to `null`:
+
+```csharp
+Plugin.Set(">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 `.loglevel#` is handled by YAVAPF internally to
+set the current log level. You can still add additional handlers for this
+variable.
diff --git a/mkdocs.yml b/mkdocs.yml
index 69c920f..5e70856 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -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/'
diff --git a/requirements.txt b/requirements.txt
index 978c5a1..4bc5c4a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-mkdocs-roamlinks-plugin
+mkdocs-roamlinks-plugin
+pymdown-extensions