From 5b6db94bce8754a128d873867c5a0694e708bb59 Mon Sep 17 00:00:00 2001 From: alterNERDtive Date: Mon, 11 Jul 2022 01:46:42 +0200 Subject: [PATCH] documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plus some refactoring / added code on the way … --- ExamplePlugin/ExamplePlugin.cs | 21 +- ExamplePlugin/ExamplePlugin.csproj | 3 +- ExamplePlugin/GlobalSuppressions.cs | 2 +- ExamplePlugin/MinimumViablePlugin.cs | 125 ++++++++++++ ExamplePlugin/Properties/Settings.Designer.cs | 2 +- VoiceAttack-Framework.sln | 7 +- .../VoiceAttack-Framework.csproj | 5 +- VoiceAttack-Framework/VoiceAttackLog.cs | 50 +++-- VoiceAttack-Framework/VoiceAttackPlugin.cs | 142 +++++++++---- docs/commands.md | 3 + docs/contexts.md | 171 ++++++++++++++++ docs/events.md | 61 ++++++ docs/extra.css | 5 + docs/faq.md | 3 + docs/gettingstarted.md | 193 ++++++++++++++++++ docs/index.md | 18 +- docs/logging.md | 73 +++++++ docs/troubleshooting.md | 0 docs/variables.md | 157 ++++++++++++++ mkdocs.yml | 19 +- requirements.txt | 3 +- 21 files changed, 971 insertions(+), 92 deletions(-) create mode 100644 ExamplePlugin/MinimumViablePlugin.cs create mode 100644 docs/extra.css create mode 100644 docs/faq.md create mode 100644 docs/logging.md delete mode 100644 docs/troubleshooting.md 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