added proxy extension methods

fixes #2
This commit is contained in:
alterNERDtive 2022-07-11 10:25:06 +02:00
parent 5b6db94bce
commit 6636a0ed4e
7 changed files with 228 additions and 66 deletions

View file

@ -165,9 +165,9 @@ namespace alterNERDtive.Example
/// execute anything when all commands are stopped this is the place. /// execute anything when all commands are stopped this is the place.
/// </summary> /// </summary>
[StopCommand] [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.");
} }
/// <summary> /// <summary>
@ -186,7 +186,7 @@ namespace alterNERDtive.Example
Plugin.Log.Notice( Plugin.Log.Notice(
$"This is the example handler for the plugin contexts “test” and “different test”. It has been invoked with '{vaProxy.Context}'."); $"This is the example handler for the plugin contexts “test” and “different test”. It has been invoked with '{vaProxy.Context}'.");
string test = Plugin.Get<string?>("~test") ?? throw new ArgumentNullException("~test"); string test = vaProxy.Get<string?>("~test") ?? throw new ArgumentNullException("~test");
Plugin.Log.Notice($"The value of 'TXT:~test' is '{test}'"); Plugin.Log.Notice($"The value of 'TXT:~test' is '{test}'");
} }

View file

@ -1 +1,20 @@
global using Xunit; // <copyright file="Usings.cs" company="alterNERDtive">
// Copyright 2022 alterNERDtive.
//
// This file is part of YAVAPF.
//
// YAVAPF is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// YAVAPF is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with YAVAPF. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
global using Xunit;

View file

@ -0,0 +1,134 @@
// <copyright file="VoiceAttackProxyExtension.cs" company="alterNERDtive">
// Copyright 2022 alterNERDtive.
//
// This file is part of YAVAPF.
//
// YAVAPF is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// YAVAPF is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with YAVAPF. If not, see &lt;https://www.gnu.org/licenses/&gt;.
// </copyright>
using System;
using System.IO;
using VoiceAttack;
namespace alterNERDtive.Yavapf
{
/// <summary>
/// Extends the <see cref="VoiceAttackInitProxyClass"/> class with generic
/// methods for getting and setting VoiceAttack variables.
/// </summary>
public static class VoiceAttackInitProxyExtension
{
/// <summary>
/// Gets the value of a variable from VoiceAttack.
///
/// Valid varible types are <see cref="bool"/>, <see cref="DateTime"/>,
/// <see cref="decimal"/>, <see cref="int"/> and <see cref="string"/>.
/// </summary>
/// <typeparam name="T">The type of the variable.</typeparam>
/// <param name="vaProxy">The <see cref="VoiceAttackInitProxyClass"/> object.</param>
/// <param name="name">The name of the variable.</param>
/// <returns>The value of the variable. Can be null.</returns>
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
public static T? Get<T>(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;
}
/// <summary>
/// Sets a variable for use in VoiceAttack.
///
/// Valid varible types are <see cref="bool"/>, <see cref="DateTime"/>,
/// <see cref="decimal"/>, <see cref="int"/> and <see cref="string"/>.
/// </summary>
/// <typeparam name="T">The type of the variable.</typeparam>
/// <param name="vaProxy">The <see cref="VoiceAttackInitProxyClass"/> object.</param>
/// <param name="name">The name of the variable.</param>
/// <param name="value">The value of the variable. Can not be null.</param>
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
public static void Set<T>(this VoiceAttackInitProxyClass vaProxy, string name, T? value)
{
if (value == null)
{
vaProxy.Unset<T>(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}'.");
}
}
}
/// <summary>
/// Unsets a variable for use in VoiceAttack (= sets it to null).
///
/// Valid varible types are <see cref="bool"/>, <see cref="DateTime"/>,
/// <see cref="decimal"/>, <see cref="int"/> and <see cref="string"/>.
/// </summary>
/// <typeparam name="T">The type of the variable.</typeparam>
/// <param name="vaProxy">The <see cref="VoiceAttackInitProxyClass"/> object.</param>
/// <param name="name">The name of the variable.</param>
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
public static void Unset<T>(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}'.");
}
}
}
}

View file

@ -143,16 +143,13 @@ namespace alterNERDtive.Yavapf
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception> /// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
protected T? Get<T>(string name) protected T? Get<T>(string name)
{ {
dynamic? ret = typeof(T) switch if (name.StartsWith("~"))
{ {
Type boolType when boolType == typeof(bool) => this.Proxy.GetBoolean(name), this.Log.Warn(
Type dateType when dateType == typeof(DateTime) => this.Proxy.GetDate(name), $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions.");
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), return this.Proxy.Get<T>(name);
_ => throw new InvalidDataException($"Cannot get variable '{name}': invalid type '{typeof(T).Name}'."),
};
return ret;
} }
/// <summary> /// <summary>
@ -167,33 +164,13 @@ namespace alterNERDtive.Yavapf
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception> /// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
protected void Set<T>(string name, T? value) protected void Set<T>(string name, T? value)
{ {
if (value == null) if (name.StartsWith("~"))
{ {
this.Unset<T>(name); this.Log.Warn(
} $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions.");
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.Proxy.Set<T>(name, value);
} }
/// <summary> /// <summary>
@ -207,26 +184,13 @@ namespace alterNERDtive.Yavapf
/// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception> /// <exception cref="InvalidDataException">Thrown when the variable is of an invalid type.</exception>
protected void Unset<T>(string name) protected void Unset<T>(string name)
{ {
switch (typeof(T)) if (name.StartsWith("~"))
{ {
case Type boolType when boolType == typeof(bool): this.Log.Warn(
this.Proxy.SetBoolean(name, null); $"Accessing command scoped variable '{name}' outside of its context proxy object. This might lead to race conditions.");
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.Proxy.Unset<T>(name);
} }
/// <summary> /// <summary>

View file

@ -36,7 +36,8 @@ Separate functionality, separate handler method(s).
[Context("test")] [Context("test")]
[Context("test context")] [Context("test context")]
[Context("alternate context name")] [Context("alternate context name")]
public static void TestContext(VoiceAttackInvokeProxyClass vaProxy) { public static void TestContext(VoiceAttackInvokeProxyClass vaProxy)
{
[…] […]
} }
``` ```
@ -68,7 +69,8 @@ conditionals:
[Context(@"^.*bar.*")] [Context(@"^.*bar.*")]
[Context(@"^.*baz")] [Context(@"^.*baz")]
[Context("some name")] [Context("some name")]
public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy) { public static void RegexContext(VoiceAttackInvokeProxyClass vaProxy)
{
string context = vaProxy.Context; string context = vaProxy.Context;
if (context.StartsWith("foo")) { if (context.StartsWith("foo")) {
[…] […]
@ -93,7 +95,8 @@ intended to be used in practice:
```csharp ```csharp
[Context(@"^edsm\..*")] [Context(@"^edsm\..*")]
public static void EdsmContext(VoiceAttackInvokeProxyClass vaProxy) { public static void EdsmContext(VoiceAttackInvokeProxyClass vaProxy)
{
switch(vaProxy.Context) switch(vaProxy.Context)
{ {
case "edsm.findsystem": case "edsm.findsystem":
@ -129,7 +132,8 @@ again, doesnt matter.
```csharp ```csharp
[Context] [Context]
public static void CatchallContext(VoiceAttackInvokeProxyClass vaProxy) { public static void CatchallContext(VoiceAttackInvokeProxyClass vaProxy)
{
switch (vaProxy.Context) switch (vaProxy.Context)
{ {
case "some context": case "some context":
@ -158,14 +162,20 @@ invocations and their data.
This example accesses the `~test` text variable from plugin code: This example accesses the `~test` text variable from plugin code:
```csharp ```csharp
string? testParameter = Plugin.Get<string>("~test"); string? testParameter = vaProxy.Get<string>("~test");
``` ```
In case a parameter is missing that is _required_ for your context `throw` an In case a parameter is missing that is _required_ for your context `throw` an
`ArgumentNullException` with the variable name as the parameter name: `ArgumentNullException` with the variable name as the parameter name:
```csharp ```csharp
string testParameter = Plugin.Get<string>("~test") ?? throw new ArgumentNullException("~test"); string testParameter = vaProxy.Get<string>("~test") ?? throw new ArgumentNullException("~test");
``` ```
**Note**: You should always use the `Get<T>(string)` and `Set<T>(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). [More about variables](variables.md).

View file

@ -22,7 +22,8 @@ must have an `InitAttribute`. `InitAttribute` does not have any properties.
```csharp ```csharp
[Init] [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 ```csharp
[Exit] [Exit]
public static void MyExitHandler(VoiceAttackProxyClass vaProxy) { public static void MyExitHandler(VoiceAttackProxyClass vaProxy)
{
[…] […]
} }
``` ```
@ -55,7 +57,8 @@ respond to that, use these.
```csharp ```csharp
[StopCommand] [StopCommand]
public static void MyStopCommandHandler() { public static void MyStopCommandHandler()
{
[…] […]
} }
``` ```

View file

@ -61,6 +61,38 @@ By default, YAVAPF automatically sets the following variables for your plugin:
| `<plugin name>.version` | string | The current version of your plugin. | `<plugin name>.version` | string | The current version of your plugin.
| `<plugin name>.initialized` | bool | The plugin has been initialized correctly. | `<plugin name>.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<string>("~test");
}
```
Incorrect, might lead to race condition:
```csharp
[Context("test")]
private static void TestContext(VoiceAttackInvokeProxyClass vaProxy)
{
string test = Plugin.Get<string>("~test");
}
```
## Getting Variable Values ## Getting Variable Values
To get the value of a variable, invoke the `Get<T>(string name)` method of your To get the value of a variable, invoke the `Get<T>(string name)` method of your
@ -69,7 +101,7 @@ its scope:
```csharp ```csharp
string? foo = Plugin.Get<string>("foo"); string? foo = Plugin.Get<string>("foo");
bool bar = Plugin.Get<bool>("~bar") ?? false; bool bar = vaProxy.Get<bool>("~bar") ?? false;
``` ```
Remember that variable values will be returned as `null` (“Not Set” in Remember that variable values will be returned as `null` (“Not Set” in