From 6636a0ed4ed756a5f181f35ed4a659ceebbb92a3 Mon Sep 17 00:00:00 2001 From: alterNERDtive Date: Mon, 11 Jul 2022 10:25:06 +0200 Subject: [PATCH] added proxy extension methods fixes #2 --- ExamplePlugin/ExamplePlugin.cs | 6 +- VoiceAttack-Framework-Test/Usings.cs | 21 ++- .../VoiceAttackInitProxyExtension.cs | 134 ++++++++++++++++++ VoiceAttack-Framework/VoiceAttackPlugin.cs | 68 +++------ docs/contexts.md | 22 ++- docs/events.md | 9 +- docs/variables.md | 34 ++++- 7 files changed, 228 insertions(+), 66 deletions(-) create mode 100644 VoiceAttack-Framework/VoiceAttackInitProxyExtension.cs diff --git a/ExamplePlugin/ExamplePlugin.cs b/ExamplePlugin/ExamplePlugin.cs index 3a85eb9..c38b87b 100644 --- a/ExamplePlugin/ExamplePlugin.cs +++ b/ExamplePlugin/ExamplePlugin.cs @@ -165,9 +165,9 @@ namespace alterNERDtive.Example /// execute anything when all commands are stopped this is the place. /// [StopCommand] - public static void Stop() + public static void StopCommand() { - Plugin.Log.Notice("This is the example Stop handler method."); + Plugin.Log.Notice("This is the example StopCommand handler method."); } /// @@ -186,7 +186,7 @@ namespace alterNERDtive.Example Plugin.Log.Notice( $"This is the example handler for the plugin contexts “test” and “different test”. It has been invoked with '{vaProxy.Context}'."); - string test = Plugin.Get("~test") ?? throw new ArgumentNullException("~test"); + string test = vaProxy.Get("~test") ?? throw new ArgumentNullException("~test"); Plugin.Log.Notice($"The value of 'TXT:~test' is '{test}'"); } diff --git a/VoiceAttack-Framework-Test/Usings.cs b/VoiceAttack-Framework-Test/Usings.cs index 8c927eb..90d616f 100644 --- a/VoiceAttack-Framework-Test/Usings.cs +++ b/VoiceAttack-Framework-Test/Usings.cs @@ -1 +1,20 @@ -global using Xunit; \ No newline at end of file +// +// 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/>. +// + +global using Xunit; diff --git a/VoiceAttack-Framework/VoiceAttackInitProxyExtension.cs b/VoiceAttack-Framework/VoiceAttackInitProxyExtension.cs new file mode 100644 index 0000000..cc0b857 --- /dev/null +++ b/VoiceAttack-Framework/VoiceAttackInitProxyExtension.cs @@ -0,0 +1,134 @@ +// +// Copyright 2022 alterNERDtive. +// +// This file is part of YAVAPF. +// +// YAVAPF is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// YAVAPF is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with YAVAPF. If not, see <https://www.gnu.org/licenses/>. +// + +using System; +using System.IO; + +using VoiceAttack; + +namespace alterNERDtive.Yavapf +{ + /// + /// Extends the class with generic + /// methods for getting and setting VoiceAttack variables. + /// + public static class VoiceAttackInitProxyExtension + { + /// + /// Gets the value of a variable from VoiceAttack. + /// + /// Valid varible types are , , + /// , and . + /// + /// The type of the variable. + /// The object. + /// The name of the variable. + /// The value of the variable. Can be null. + /// Thrown when the variable is of an invalid type. + public static T? Get(this VoiceAttackInitProxyClass vaProxy, string name) + { + dynamic? ret = typeof(T) switch + { + Type boolType when boolType == typeof(bool) => vaProxy.GetBoolean(name), + Type dateType when dateType == typeof(DateTime) => vaProxy.GetDate(name), + Type decType when decType == typeof(decimal) => vaProxy.GetDecimal(name), + Type intType when intType == typeof(int) => vaProxy.GetInt(name), + Type stringType when stringType == typeof(string) => vaProxy.GetText(name), + _ => throw new InvalidDataException($"Cannot get variable '{name}': invalid type '{typeof(T).Name}'."), + }; + return ret; + } + + /// + /// Sets a variable for use in VoiceAttack. + /// + /// Valid varible types are , , + /// , and . + /// + /// The type of the variable. + /// The object. + /// The name of the variable. + /// The value of the variable. Can not be null. + /// Thrown when the variable is of an invalid type. + public static void Set(this VoiceAttackInitProxyClass vaProxy, string name, T? value) + { + if (value == null) + { + vaProxy.Unset(name); + } + else + { + switch (value) + { + case bool b: + vaProxy.SetBoolean(name, b); + break; + case DateTime d: + vaProxy.SetDate(name, d); + break; + case decimal d: + vaProxy.SetDecimal(name, d); + break; + case int i: + vaProxy.SetInt(name, i); + break; + case string s: + vaProxy.SetText(name, s); + break; + default: + throw new InvalidDataException($"Cannot set variable '{name}': invalid type '{typeof(T).Name}'."); + } + } + } + + /// + /// Unsets a variable for use in VoiceAttack (= sets it to null). + /// + /// Valid varible types are , , + /// , and . + /// + /// The type of the variable. + /// The object. + /// The name of the variable. + /// Thrown when the variable is of an invalid type. + public static void Unset(this VoiceAttackInitProxyClass vaProxy, string name) + { + switch (typeof(T)) + { + case Type boolType when boolType == typeof(bool): + vaProxy.SetBoolean(name, null); + break; + case Type dateType when dateType == typeof(DateTime): + vaProxy.SetDate(name, null); + break; + case Type decType when decType == typeof(decimal): + vaProxy.SetDecimal(name, null); + break; + case Type intType when intType == typeof(int): + vaProxy.SetInt(name, null); + break; + case Type stringType when stringType == typeof(string): + vaProxy.SetText(name, null); + break; + default: + throw new InvalidDataException($"Cannot set variable '{name}': invalid type '{typeof(T).Name}'."); + } + } + } +} diff --git a/VoiceAttack-Framework/VoiceAttackPlugin.cs b/VoiceAttack-Framework/VoiceAttackPlugin.cs index e4a8de5..67f7594 100644 --- a/VoiceAttack-Framework/VoiceAttackPlugin.cs +++ b/VoiceAttack-Framework/VoiceAttackPlugin.cs @@ -143,16 +143,13 @@ namespace alterNERDtive.Yavapf /// Thrown when the variable is of an invalid type. protected T? Get(string name) { - dynamic? ret = typeof(T) switch + if (name.StartsWith("~")) { - Type boolType when boolType == typeof(bool) => this.Proxy.GetBoolean(name), - Type dateType when dateType == typeof(DateTime) => this.Proxy.GetDate(name), - Type decType when decType == typeof(decimal) => this.Proxy.GetDecimal(name), - Type intType when intType == typeof(int) => this.Proxy.GetInt(name), - Type stringType when stringType == typeof(string) => this.Proxy.GetText(name), - _ => throw new InvalidDataException($"Cannot get variable '{name}': invalid type '{typeof(T).Name}'."), - }; - return ret; + this.Log.Warn( + $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); + } + + return this.Proxy.Get(name); } /// @@ -167,33 +164,13 @@ namespace alterNERDtive.Yavapf /// Thrown when the variable is of an invalid type. protected void Set(string name, T? value) { - if (value == null) + if (name.StartsWith("~")) { - this.Unset(name); - } - else - { - switch (value) - { - case bool b: - this.Proxy.SetBoolean(name, b); - break; - case DateTime d: - this.Proxy.SetDate(name, d); - break; - case decimal d: - this.Proxy.SetDecimal(name, d); - break; - case int i: - this.Proxy.SetInt(name, i); - break; - case string s: - this.Proxy.SetText(name, s); - break; - default: - throw new InvalidDataException($"Cannot set variable '{name}': invalid type '{typeof(T).Name}'."); - } + this.Log.Warn( + $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); } + + this.Proxy.Set(name, value); } /// @@ -207,26 +184,13 @@ namespace alterNERDtive.Yavapf /// Thrown when the variable is of an invalid type. protected void Unset(string name) { - switch (typeof(T)) + if (name.StartsWith("~")) { - case Type boolType when boolType == typeof(bool): - this.Proxy.SetBoolean(name, null); - break; - case Type dateType when dateType == typeof(DateTime): - this.Proxy.SetDate(name, null); - break; - case Type decType when decType == typeof(decimal): - this.Proxy.SetDecimal(name, null); - break; - case Type intType when intType == typeof(int): - this.Proxy.SetInt(name, null); - break; - case Type stringType when stringType == typeof(string): - this.Proxy.SetText(name, null); - break; - default: - throw new InvalidDataException($"Cannot set variable '{name}': invalid type '{typeof(T).Name}'."); + this.Log.Warn( + $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions."); } + + this.Proxy.Unset(name); } /// diff --git a/docs/contexts.md b/docs/contexts.md index 584eb28..cb3dd03 100644 --- a/docs/contexts.md +++ b/docs/contexts.md @@ -36,7 +36,8 @@ Separate functionality, separate handler method(s). [Context("test")] [Context("test context")] [Context("alternate context name")] -public static void TestContext(VoiceAttackInvokeProxyClass vaProxy) { +public static void TestContext(VoiceAttackInvokeProxyClass vaProxy) +{ […] } ``` @@ -68,7 +69,8 @@ conditionals: [Context(@"^.*bar.*")] [Context(@"^.*baz")] [Context("some name")] -public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy) { +public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy) +{ string context = vaProxy.Context; if (context.StartsWith("foo")) { […] @@ -93,7 +95,8 @@ intended to be used in practice: ```csharp [Context(@"^edsm\..*")] -public static void EdsmContext(VoiceAttackInvokeProxyClass vaProxy) { +public static void EdsmContext(VoiceAttackInvokeProxyClass vaProxy) +{ switch(vaProxy.Context) { case "edsm.findsystem": @@ -129,7 +132,8 @@ again, doesn’t matter. ```csharp [Context] -public static void CatchallContext(VoiceAttackInvokeProxyClass vaProxy) { +public static void CatchallContext(VoiceAttackInvokeProxyClass vaProxy) +{ switch (vaProxy.Context) { case "some context": @@ -158,14 +162,20 @@ invocations and their data. This example accesses the `~test` text variable from plugin code: ```csharp -string? testParameter = Plugin.Get("~test"); +string? testParameter = vaProxy.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"); +string testParameter = vaProxy.Get("~test") ?? throw new ArgumentNullException("~test"); ``` +**Note**: You should always use the `Get(string)` and `Set(string)` +extension methods of the `VoiceAttackInvokeProxyClass` object when accessing +command scoped variables. They are only available in the proxy object passed to +the corresponding context handler; the cached proxy object of your plugin object +might have changed already, leading to race conditions. + [More about variables](variables.md). diff --git a/docs/events.md b/docs/events.md index 909dbd6..3773631 100644 --- a/docs/events.md +++ b/docs/events.md @@ -22,7 +22,8 @@ must have an `InitAttribute`. `InitAttribute` does not have any properties. ```csharp [Init] -public static void MyInitHandler(VoiceAttackInitProxyClass vaProxy) { +public static void MyInitHandler(VoiceAttackInitProxyClass vaProxy) +{ […] } ``` @@ -38,7 +39,8 @@ have an `ExitAttribute`. `ExitAttribute` does not have any properties. ```csharp [Exit] -public static void MyExitHandler(VoiceAttackProxyClass vaProxy) { +public static void MyExitHandler(VoiceAttackProxyClass vaProxy) +{ […] } ``` @@ -55,7 +57,8 @@ respond to that, use these. ```csharp [StopCommand] -public static void MyStopCommandHandler() { +public static void MyStopCommandHandler() +{ […] } ``` diff --git a/docs/variables.md b/docs/variables.md index 5e70b82..d11b900 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -61,6 +61,38 @@ By default, YAVAPF automatically sets the following variables for your plugin: | `.version` | string | The current version of your plugin. | `.initialized` | bool | The plugin has been initialized correctly. +## Using Proxy Methods vs. Using Plugin Methods + +YAVAPF extends the `VoiceAttackInitProxyClass` and `VoiceAttackInvokeProxyClass` +classes with new generic methods for getting and setting variables. On top of +that `VoiceAttackPlugin` objects provide the same methods for ease of use and +when no proxy object is readily available to the current code path. + +There is one **caveat** here: plugin objects cache a new proxy object every time +a plugin context is invoked. If you need to access command scoped variables (`~` +or `~~`), you should use the `VoiceAttackInvokeProxyClass` object directly. +Otherwise you might run into race conditions. + +Correct: + +```csharp +[Context("test")] +private static void TestContext(VoiceAttackInvokeProxyClass vaProxy) +{ + string test = vaProxy.Get("~test"); +} +``` + +Incorrect, might lead to race condition: + +```csharp +[Context("test")] +private static void TestContext(VoiceAttackInvokeProxyClass vaProxy) +{ + string test = Plugin.Get("~test"); +} +``` + ## Getting Variable Values To get the value of a variable, invoke the `Get(string name)` method of your @@ -69,7 +101,7 @@ its scope: ```csharp string? foo = Plugin.Get("foo"); -bool bar = Plugin.Get("~bar") ?? false; +bool bar = vaProxy.Get("~bar") ?? false; ``` Remember that variable values will be returned as `null` (“Not Set” in