diff --git a/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj.DotSettings b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj.DotSettings
index f99e4ea18..de4dd1d16 100644
--- a/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj.DotSettings
+++ b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj.DotSettings
@@ -2,8 +2,14 @@
True
True
True
+ True
+ True
+ True
+ True
True
True
True
+ True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/NWN.Anvil.Tests/src/main/API/Async/NwTaskTests.cs b/NWN.Anvil.Tests/src/main/API/Async/NwTaskTests.cs
index 5b3e694c2..f48d15c43 100644
--- a/NWN.Anvil.Tests/src/main/API/Async/NwTaskTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/Async/NwTaskTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.Threading.Tasks;
using Anvil.API;
using Anvil.Services;
@@ -13,6 +14,7 @@ public sealed class NwTaskTests
private static VirtualMachine VirtualMachine { get; set; }
[Test(Description = "Starts an async task, then attempts to switch back to the main thread & script context.")]
+ [Timeout(10000)]
public async Task ReturnToMainThreadAfterSwitch()
{
await Task.Run(async () =>
@@ -24,5 +26,21 @@ await Task.Run(async () =>
await NwTask.SwitchToMainThread();
Assert.That(VirtualMachine.IsInScriptContext, Is.True, "Did not return to the main thread after SwitchToMainThread.");
}
+
+ [Test(Description = "Await a fixed delay then continue execution.")]
+ [Timeout(10000)]
+ [TestCase(500)]
+ [TestCase(1000)]
+ [TestCase(5000)]
+ public async Task AwaitDelayContinuesExecutionAtExpectedTime(int delayMs)
+ {
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayMs);
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ await NwTask.Delay(delay);
+
+ Assert.That(stopwatch.Elapsed.TotalMilliseconds, Is.EqualTo(delay.TotalMilliseconds).Within(2).Percent, "Delay was not within the margin of error.");
+ Assert.That(VirtualMachine.IsInScriptContext, Is.True, "Did not return to the main thread after NwTask.Delay.");
+ }
}
}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs
new file mode 100644
index 000000000..484341079
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs
@@ -0,0 +1,37 @@
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public sealed class NuiBindTests
+ {
+ [Test(Description = "Serializing a NuiBind creates a valid JSON structure.")]
+ public void SerializeNuiBindStringReturnsValidJsonStructure()
+ {
+ NuiBind test = new NuiBind("test");
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""bind"":""test""}"));
+ }
+
+ [Test(Description = "Serializing a NuiBind creates a valid JSON structure.")]
+ public void SerializeNuiBindNuiRectReturnsValidJsonStructure()
+ {
+ NuiBind test = new NuiBind("test");
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""bind"":""test""}"));
+ }
+
+ [Test(Description = "Deerializing a NuiBind creates a valid JSON structure.")]
+ public void DeserializeNuiBindStringReturnsValidJsonStructure()
+ {
+ NuiBind test = JsonUtility.FromJson>(@"{""bind"":""test""}");
+ Assert.That(test.Key, Is.EqualTo("test"));
+ }
+
+ [Test(Description = "Deerializing a NuiBind creates a valid JSON structure.")]
+ public void DeserializeNuiBindNuiRectReturnsValidJsonStructure()
+ {
+ NuiBind test = JsonUtility.FromJson>(@"{""bind"":""test""}");
+ Assert.That(test.Key, Is.EqualTo("test"));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs
new file mode 100644
index 000000000..de9e63323
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs
@@ -0,0 +1,182 @@
+using System.Collections.Generic;
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public class NuiValueTests
+ {
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ [TestCase("test", @"""test""")]
+ [TestCase(null, @"null")]
+ [TestCase("", @"""""")]
+ public void SerializeNuiValueStringReturnsValidJsonStructure(string value, string expected)
+ {
+ NuiValue test = new NuiValue(value);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0, @"0")]
+ [TestCase(-0, @"0")]
+ [TestCase(10, @"10")]
+ [TestCase(-10, @"-10")]
+ [TestCase(int.MaxValue, @"2147483647")]
+ [TestCase(int.MinValue, @"-2147483648")]
+ public void SerializeNuiValueIntReturnsValidJsonStructure(int value, string expected)
+ {
+ NuiValue test = new NuiValue(value);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0, @"0")]
+ [TestCase(-0, @"0")]
+ [TestCase(10, @"10")]
+ [TestCase(-10, @"-10")]
+ [TestCase(null, @"null")]
+ [TestCase(int.MaxValue, @"2147483647")]
+ [TestCase(int.MinValue, @"-2147483648")]
+ public void SerializeNuiValueNullableIntReturnsValidJsonStructure(int? value, string expected)
+ {
+ NuiValue test = new NuiValue(value);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0f, @"0.0")]
+ [TestCase(0.1f, @"0.1")]
+ [TestCase(0.125f, @"0.125")]
+ [TestCase(2f, @"2.0")]
+ [TestCase(2.5f, @"2.5")]
+ [TestCase(2.5122f, @"2.5122")]
+ [TestCase(float.NaN, @"""NaN""")]
+ [TestCase(float.NegativeInfinity, @"""-Infinity""")]
+ [TestCase(float.PositiveInfinity, @"""Infinity""")]
+ public void SerializeNuiValueFloatReturnsValidJsonStructure(float value, string expected)
+ {
+ NuiValue test = new NuiValue(value);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0f, @"0.0")]
+ [TestCase(0.1f, @"0.1")]
+ [TestCase(0.125f, @"0.125")]
+ [TestCase(2f, @"2.0")]
+ [TestCase(2.5f, @"2.5")]
+ [TestCase(2.5122f, @"2.5122")]
+ [TestCase(null, @"null")]
+ [TestCase(float.NaN, @"""NaN""")]
+ [TestCase(float.NegativeInfinity, @"""-Infinity""")]
+ [TestCase(float.PositiveInfinity, @"""Infinity""")]
+ public void SerializeNuiValueFloatNullableReturnsValidJsonStructure(float? value, string expected)
+ {
+ NuiValue test = new NuiValue(value);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
+ public void SerializeNuiValueNuiRectReturnsValidJsonStructure()
+ {
+ NuiValue test = new NuiValue(new NuiRect(100f, 50.251f, 30.11f, 20f));
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""h"":20.0,""w"":30.11,""x"":100.0,""y"":50.251}"));
+ }
+
+ [Test(Description = "Serializing a NuiValue> creates a valid JSON structure.")]
+ public void SerializeNuiValueIntListReturnsValidJsonStructure()
+ {
+ NuiValue> test = new NuiValue>(new List { 1, 2, 3 });
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"[1,2,3]"));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [TestCase("test", @"""test""")]
+ [TestCase(null, @"null")]
+ [TestCase("", @"""""")]
+ public void DeserializeNuiValueStringReturnsValidJsonStructure(string expected, string serialized)
+ {
+ NuiValue test = JsonUtility.FromJson>(serialized);
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0, @"0")]
+ [TestCase(-0, @"0")]
+ [TestCase(10, @"10")]
+ [TestCase(-10, @"-10")]
+ [TestCase(int.MaxValue, @"2147483647")]
+ [TestCase(int.MinValue, @"-2147483648")]
+ public void DeserializeNuiValueIntReturnsValidJsonStructure(int expected, string serialized)
+ {
+ NuiValue test = JsonUtility.FromJson>(serialized);
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0, @"0")]
+ [TestCase(-0, @"0")]
+ [TestCase(10, @"10")]
+ [TestCase(-10, @"-10")]
+ [TestCase(null, @"null")]
+ [TestCase(int.MaxValue, @"2147483647")]
+ [TestCase(int.MinValue, @"-2147483648")]
+ public void DeserializeNuiValueNullableIntReturnsValidJsonStructure(int? expected, string serialized)
+ {
+ NuiValue test = JsonUtility.FromJson>(serialized);
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0f, @"0.0")]
+ [TestCase(0.1f, @"0.1")]
+ [TestCase(0.125f, @"0.125")]
+ [TestCase(2f, @"2.0")]
+ [TestCase(2.5f, @"2.5")]
+ [TestCase(2.5122f, @"2.5122")]
+ [TestCase(float.NaN, @"""NaN""")]
+ [TestCase(float.NegativeInfinity, @"""-Infinity""")]
+ [TestCase(float.PositiveInfinity, @"""Infinity""")]
+ public void DeserializeNuiValueFloatReturnsValidJsonStructure(float expected, string serialized)
+ {
+ NuiValue test = JsonUtility.FromJson>(serialized);
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [TestCase(0f, @"0.0")]
+ [TestCase(0.1f, @"0.1")]
+ [TestCase(0.125f, @"0.125")]
+ [TestCase(2f, @"2.0")]
+ [TestCase(2.5f, @"2.5")]
+ [TestCase(2.5122f, @"2.5122")]
+ [TestCase(null, @"null")]
+ [TestCase(float.NaN, @"""NaN""")]
+ [TestCase(float.NegativeInfinity, @"""-Infinity""")]
+ [TestCase(float.PositiveInfinity, @"""Infinity""")]
+ public void DeserializeNuiValueFloatNullableReturnsValidJsonStructure(float? expected, string serialized)
+ {
+ NuiValue test = JsonUtility.FromJson>(serialized);
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ public void DeserializeNuiValueNuiRectReturnsValidJsonStructure()
+ {
+ NuiValue test = JsonUtility.FromJson>(@"{""h"":20.0,""w"":30.11,""x"":100.0,""y"":50.251}");
+ NuiRect expected = new NuiRect(100.0f, 50.251f, 30.11f, 20.0f);
+
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deserializing a NuiValue> creates a valid JSON structure.")]
+ public void DeserializeNuiValueIntListReturnsValidJsonStructure()
+ {
+ NuiValue> test = JsonUtility.FromJson>>(@"[1,2,3]");
+ List expected = new List { 1, 2, 3 };
+
+ Assert.That(test.Value, Is.EqualTo(expected));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiColumnTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiColumnTests.cs
new file mode 100644
index 000000000..92da3238b
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiColumnTests.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public sealed class NuiColumnTests
+ {
+ [Test(Description = "Serializing a NuiColumn creates a valid JSON structure.")]
+ public void SerializeNuiColumnReturnsValidJsonStructure()
+ {
+ NuiColumn nuiColumn = new NuiColumn
+ {
+ Id = "test_column",
+ Aspect = 1.5f,
+ Enabled = new NuiBind("enabled_bind"),
+ Height = 10f,
+ Margin = 2f,
+ Padding = 3f,
+ ForegroundColor = new NuiBind("color_bind"),
+ Tooltip = "test_tooltip",
+ Width = 100f,
+ Visible = false,
+ Children = new List
+ {
+ new NuiLabel("test"),
+ new NuiRow(),
+ },
+ };
+
+ Assert.That(JsonUtility.ToJson(nuiColumn), Is.EqualTo(@"{""type"":""col"",""children"":[{""text_halign"":1,""value"":""test"",""type"":""label"",""text_valign"":1},{""type"":""row"",""children"":[]}],""aspect"":1.5,""enabled"":{""bind"":""enabled_bind""},""foreground_color"":{""bind"":""color_bind""},""height"":10.0,""id"":""test_column"",""margin"":2.0,""padding"":3.0,""tooltip"":""test_tooltip"",""visible"":false,""width"":100.0}"));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiGroupTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiGroupTests.cs
new file mode 100644
index 000000000..fdbad518b
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiGroupTests.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public sealed class NuiGroupTests
+ {
+ [Test(Description = "Serializing a NuiGroup creates a valid JSON structure.")]
+ public void SerializeNuiGroupReturnsValidJsonStructure()
+ {
+ NuiGroup nuiGroup = new NuiGroup
+ {
+ Id = "test_group",
+ Aspect = 1.5f,
+ Border = true,
+ Enabled = new NuiBind("enabled_bind"),
+ Height = 10f,
+ Margin = 2f,
+ Padding = 3f,
+ ForegroundColor = new NuiBind("color_bind"),
+ Scrollbars = NuiScrollbars.Both,
+ Tooltip = "test_tooltip",
+ Width = 100f,
+ Visible = false,
+ Layout = new NuiColumn
+ {
+ Children = new List
+ {
+ new NuiLabel("Test"),
+ },
+ },
+ };
+
+ Assert.That(JsonUtility.ToJson(nuiGroup), Is.EqualTo(@"{""border"":true,""scrollbars"":3,""type"":""group"",""children"":[{""type"":""col"",""children"":[{""text_halign"":1,""value"":""Test"",""type"":""label"",""text_valign"":1}]}],""aspect"":1.5,""enabled"":{""bind"":""enabled_bind""},""foreground_color"":{""bind"":""color_bind""},""height"":10.0,""id"":""test_group"",""margin"":2.0,""padding"":3.0,""tooltip"":""test_tooltip"",""visible"":false,""width"":100.0}"));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiRowTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiRowTests.cs
new file mode 100644
index 000000000..9c7939fbe
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Layout/NuiRowTests.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public sealed class NuiRowTests
+ {
+ [Test(Description = "Serializing a NuiRow creates a valid JSON structure.")]
+ public void SerializeNuiRowReturnsValidJsonStructure()
+ {
+ NuiRow nuiRow = new NuiRow
+ {
+ Id = "test_row",
+ Aspect = 1.5f,
+ Enabled = new NuiBind("enabled_bind"),
+ Height = 10f,
+ Margin = 2f,
+ Padding = 3f,
+ ForegroundColor = new NuiBind("color_bind"),
+ Tooltip = "test_tooltip",
+ Width = 100f,
+ Visible = false,
+ Children = new List
+ {
+ new NuiLabel("test"),
+ new NuiRow(),
+ },
+ };
+
+ Assert.That(JsonUtility.ToJson(nuiRow), Is.EqualTo(@"{""type"":""row"",""children"":[{""text_halign"":1,""value"":""test"",""type"":""label"",""text_valign"":1},{""type"":""row"",""children"":[]}],""aspect"":1.5,""enabled"":{""bind"":""enabled_bind""},""foreground_color"":{""bind"":""color_bind""},""height"":10.0,""id"":""test_row"",""margin"":2.0,""padding"":3.0,""tooltip"":""test_tooltip"",""visible"":false,""width"":100.0}"));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Widgets/NuiDrawListTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Widgets/NuiDrawListTests.cs
new file mode 100644
index 000000000..f3022eda8
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Widgets/NuiDrawListTests.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using Anvil.API;
+using NUnit.Framework;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Nui")]
+ public sealed class NuiDrawListTests
+ {
+ [Test(Description = "Serializing a NuiDrawListArc creates a valid JSON structure.")]
+ public void SerializeNuiDrawListArcReturnsValidJsonStructure()
+ {
+ NuiDrawListArc drawListArc = new NuiDrawListArc(ColorConstants.Pink, true, 1.0f, new NuiVector(1.0f, 2.0f), 2.0f, 90f, 170f)
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListArc), Is.EqualTo(@"{""amax"":170.0,""amin"":90.0,""c"":{""x"":1.0,""y"":2.0},""radius"":2.0,""type"":3,""color"":{""a"":255,""b"":170,""g"":170,""r"":255},""enabled"":false,""fill"":true,""line_thickness"":1.0}"));
+ }
+
+ [Test(Description = "Serializing a NuiDrawListCircle creates a valid JSON structure.")]
+ public void SerializeNuiDrawListCircleReturnsValidJsonStructure()
+ {
+ NuiDrawListCircle drawListCircle = new NuiDrawListCircle(ColorConstants.Pink, true, 1.0f, new NuiRect(1.0f, 2.0f, 3.0f, 4.0f))
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListCircle), Is.EqualTo(@"{""rect"":{""h"":4.0,""w"":3.0,""x"":1.0,""y"":2.0},""type"":2,""color"":{""a"":255,""b"":170,""g"":170,""r"":255},""enabled"":false,""fill"":true,""line_thickness"":1.0}"));
+ }
+
+ [Test(Description = "Serializing a NuiDrawListCurve creates a valid JSON structure.")]
+ public void SerializeNuiDrawListCurveReturnsValidJsonStructure()
+ {
+ NuiDrawListCurve drawListCurve = new NuiDrawListCurve(ColorConstants.Pink, 1.0f, new NuiVector(10.0f, 5.0f), new NuiVector(6.0f, 2.0f), new NuiVector(9.5f, 3.0f), new NuiVector(22.0f, 11.3f))
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListCurve), Is.EqualTo(@"{""ctrl0"":{""x"":9.5,""y"":3.0},""ctrl1"":{""x"":22.0,""y"":11.3},""a"":{""x"":10.0,""y"":5.0},""b"":{""x"":6.0,""y"":2.0},""type"":1,""color"":{""a"":255,""b"":170,""g"":170,""r"":255},""enabled"":false,""fill"":false,""line_thickness"":1.0}"));
+ }
+
+ [Test(Description = "Serializing a NuiDrawListImage creates a valid JSON structure.")]
+ public void SerializeNuiDrawListImageReturnsValidJsonStructure()
+ {
+ NuiDrawListImage drawListImage = new NuiDrawListImage("test_img", new NuiRect(1.0f, 2.0f, 3.0f, 4.0f))
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListImage), Is.EqualTo(@"{""image_aspect"":3,""image_halign"":1,""rect"":{""h"":4.0,""w"":3.0,""x"":1.0,""y"":2.0},""image"":""test_img"",""type"":5,""image_valign"":1,""color"":null,""enabled"":false,""fill"":null,""line_thickness"":null}"));
+ }
+
+ [Test(Description = "Serializing a NuiDrawListPolyLine creates a valid JSON structure.")]
+ public void SerializeNuiDrawListPolyLineReturnsValidJsonStructure()
+ {
+ NuiDrawListPolyLine drawListPolyLine = new NuiDrawListPolyLine(ColorConstants.Pink, true, 2.0f, new List { 2.0f, 4.0f, 6.0f, 11.0f })
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListPolyLine), Is.EqualTo(@"{""points"":[2.0,4.0,6.0,11.0],""type"":0,""color"":{""a"":255,""b"":170,""g"":170,""r"":255},""enabled"":false,""fill"":true,""line_thickness"":2.0}"));
+ }
+
+ [Test(Description = "Serializing a NuiDrawListText creates a valid JSON structure.")]
+ public void SerializeNuiDrawListTextReturnsValidJsonStructure()
+ {
+ NuiDrawListText drawListText = new NuiDrawListText(ColorConstants.Pink, new NuiRect(5.0f, 6.0f, 7.0f, 8.0f), "Test string")
+ {
+ Enabled = false,
+ };
+
+ Assert.That(JsonUtility.ToJson(drawListText), Is.EqualTo(@"{""rect"":{""h"":8.0,""w"":7.0,""x"":5.0,""y"":6.0},""text"":""Test string"",""type"":4,""color"":{""a"":255,""b"":170,""g"":170,""r"":255},""enabled"":false,""fill"":null,""line_thickness"":null}"));
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Object/NwObjectTests.cs b/NWN.Anvil.Tests/src/main/API/Object/NwObjectTests.cs
new file mode 100644
index 000000000..bc581be1f
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Object/NwObjectTests.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Anvil.API;
+using Anvil.Tests.Resources;
+using NUnit.Framework;
+using NWN.Core;
+
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Object")]
+ public sealed class NwObjectTests
+ {
+ private readonly List createdTestObjects = new List();
+
+ [Test(Description = "Tests if assigning an action/closure using WaitForObjectContext correctly updates the script context.")]
+ [Timeout(5000)]
+ public async Task WaitForObjectContextEntersCorrectContext()
+ {
+ NwModule module = NwModule.Instance;
+
+ await module.WaitForObjectContext();
+
+ Assert.That(NWScript.OBJECT_SELF.ToNwObject(), Is.EqualTo(module));
+
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation);
+ createdTestObjects.Add(creature);
+
+ await creature.WaitForObjectContext();
+
+ Assert.That(NWScript.OBJECT_SELF.ToNwObject(), Is.EqualTo(creature));
+ }
+
+ [Test(Description = "Tests if adding an action correctly queues an action on the game object.")]
+ [Timeout(5000)]
+ public async Task QueueCreatureActionIsQueued()
+ {
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation);
+ createdTestObjects.Add(creature);
+
+ bool actionExecuted = false;
+ await creature.AddActionToQueue(() =>
+ {
+ actionExecuted = true;
+ });
+
+ await NwTask.WaitUntil(() => actionExecuted);
+ Assert.That(actionExecuted, Is.EqualTo(true));
+ }
+
+ [TearDown]
+ public void CleanupTestObjects()
+ {
+ foreach (NwGameObject testObject in createdTestObjects)
+ {
+ testObject.PlotFlag = false;
+ testObject.Destroy();
+ }
+
+ createdTestObjects.Clear();
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/API/Utils/JsonUtilityTests.cs b/NWN.Anvil.Tests/src/main/API/Utils/JsonUtilityTests.cs
new file mode 100644
index 000000000..f45d4030b
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/API/Utils/JsonUtilityTests.cs
@@ -0,0 +1,92 @@
+using Anvil.API;
+using NUnit.Framework;
+
+// ReSharper disable UnusedAutoPropertyAccessor.Local
+namespace Anvil.Tests.API
+{
+ [TestFixture(Category = "API.Utils")]
+ public sealed class JsonUtilityTests
+ {
+ [Test(Description = "Serializing a value creates valid json.")]
+ [TestCase(null, "null")]
+ [TestCase(1, "1")]
+ [TestCase(1f, "1.0")]
+ [TestCase(1.532f, "1.532")]
+ [TestCase(1.0d, "1.0")]
+ [TestCase(1.689d, "1.689")]
+ [TestCase(false, "false")]
+ [TestCase(true, "true")]
+ [TestCase("test", @"""test""")]
+ [TestCase("", @"""""")]
+ public void SerializeValueCreatesValidJson(object value, string expected)
+ {
+ Assert.That(JsonUtility.ToJson(value), Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Serializing a struct creates valid json.")]
+ public void SerializeStructCreatesValidJson()
+ {
+ TestStruct value = new TestStruct
+ {
+ TestB = true,
+ TestF = 10f,
+ TestI = 5,
+ TestS = "test",
+ };
+
+ Assert.That(JsonUtility.ToJson(value), Is.EqualTo(@"{""TestI"":5,""TestS"":""test"",""TestF"":10.0,""TestB"":true}"));
+ }
+
+ [Test(Description = "Serializing a class creates valid json.")]
+ public void SerializeClassCreatesValidJson()
+ {
+ TestClass value = new TestClass
+ {
+ TestB = true,
+ TestF = 10f,
+ TestI = 5,
+ TestS = "test",
+ };
+
+ Assert.That(JsonUtility.ToJson(value), Is.EqualTo(@"{""TestI"":5,""TestS"":""test"",""TestF"":10.0,""TestB"":true}"));
+ }
+
+ [Test(Description = "Serializing a record creates valid json.")]
+ public void SerializeRecordCreatesValidJson()
+ {
+ TestRecord value = new TestRecord
+ {
+ TestB = true,
+ TestF = 10f,
+ TestI = 5,
+ TestS = "test",
+ };
+
+ Assert.That(JsonUtility.ToJson(value), Is.EqualTo(@"{""TestI"":5,""TestS"":""test"",""TestF"":10.0,""TestB"":true}"));
+ }
+
+ private struct TestStruct
+ {
+ public int TestI { get; set; }
+ public string TestS { get; set; }
+ public float TestF { get; set; }
+ public bool TestB { get; set; }
+ }
+
+ private sealed class TestClass
+ {
+ public int TestI { get; set; }
+ public string TestS { get; set; }
+ public float TestF { get; set; }
+ public bool TestB { get; set; }
+ }
+
+ private sealed record TestRecord
+ {
+ public int TestI { get; set; }
+ public string TestS { get; set; }
+ public float TestF { get; set; }
+ public bool TestB { get; set; }
+ }
+ }
+}
diff --git a/NWN.Anvil.Tests/src/main/Services/Scheduler/SchedulerServiceTests.cs b/NWN.Anvil.Tests/src/main/Services/Scheduler/SchedulerServiceTests.cs
new file mode 100644
index 000000000..3bf65846c
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/Services/Scheduler/SchedulerServiceTests.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Anvil.API;
+using Anvil.Services;
+using NUnit.Framework;
+
+namespace Anvil.Tests.Services
+{
+ [TestFixture(Category = "Services.Scheduler")]
+ public sealed class SchedulerServiceTests
+ {
+ [Inject]
+ private static SchedulerService SchedulerService { get; set; }
+
+ [Test(Description = "Scheduling a task correctly schedules and runs the task with the specified delay.")]
+ [Timeout(10000)]
+ [TestCase(500)]
+ [TestCase(1000)]
+ [TestCase(5000)]
+ public async Task ScheduleDelayRunsTaskAfterDelay(int delayMs)
+ {
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayMs);
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ bool executed = false;
+ SchedulerService.Schedule(() =>
+ {
+ Assert.That(stopwatch.Elapsed.TotalMilliseconds, Is.EqualTo(delay.TotalMilliseconds).Within(2).Percent, "Delay was not within the margin of error.");
+ executed = true;
+ }, delay);
+
+ await NwTask.WaitUntil(() => executed);
+ Assert.That(executed, Is.EqualTo(true));
+ }
+
+ [Test(Description = "Scheduling a task and cancelling it prevents the schedule from running.")]
+ [Timeout(10000)]
+ [TestCase(500)]
+ [TestCase(1000)]
+ [TestCase(5000)]
+ public async Task ScheduleAndCancelDoesNotRunTask(int delayMs)
+ {
+ TimeSpan delay = TimeSpan.FromMilliseconds(delayMs);
+
+ ScheduledTask task = SchedulerService.Schedule(() =>
+ {
+ Assert.Fail("The task executed when it shouldn't have.");
+ }, delay);
+
+ task.Cancel();
+ await NwTask.Delay(delay + TimeSpan.FromSeconds(1));
+ }
+
+ [Test(Description = "Scheduling a repeating task correctly schedules and runs the task with the specified interval.")]
+ [Timeout(10000)]
+ [TestCase(500)]
+ [TestCase(1000)]
+ public async Task RepeatingScheduleRunsRepeatingTask(int delayMs)
+ {
+ TimeSpan interval = TimeSpan.FromMilliseconds(delayMs);
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ int executionCount = 0;
+ ScheduledTask task = SchedulerService.ScheduleRepeating(() =>
+ {
+ Assert.That(stopwatch.Elapsed.TotalMilliseconds, Is.EqualTo(interval.TotalMilliseconds).Within(2).Percent, "Delay was not within the margin of error.");
+ executionCount++;
+ stopwatch.Restart();
+ }, interval);
+
+ await NwTask.WaitUntil(() => executionCount > 5);
+ Assert.That(executionCount, Is.GreaterThan(5));
+
+ task.Cancel();
+ }
+
+ [Test(Description = "Scheduling a repeating task and cancelling the task after the first run correctly cancels the task.")]
+ [Timeout(10000)]
+ [TestCase(500)]
+ [TestCase(1000)]
+ public async Task RepeatingScheduleCancelAfterRunPreventsSubsequentRuns(int delayMs)
+ {
+ TimeSpan interval = TimeSpan.FromMilliseconds(delayMs);
+
+ ScheduledTask scheduledTask = null;
+ int executionCount = 0;
+
+ scheduledTask = SchedulerService.ScheduleRepeating(() =>
+ {
+ Assert.That(executionCount, Is.EqualTo(0), "Repeating task ran after being cancelled.");
+ executionCount++;
+
+ // ReSharper disable once AccessToModifiedClosure
+ scheduledTask!.Cancel();
+ }, interval);
+
+ await NwTask.WaitUntil(() => executionCount > 0);
+ Assert.That(executionCount, Is.EqualTo(1));
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Color.cs b/NWN.Anvil/src/main/API/Color.cs
index 5eb2a7717..04330a556 100644
--- a/NWN.Anvil/src/main/API/Color.cs
+++ b/NWN.Anvil/src/main/API/Color.cs
@@ -67,21 +67,25 @@ public Color(float red, float green, float blue, float alpha = 1.0f)
///
/// Gets the alpha value of this color as a float (0-1).
///
+ [JsonIgnore]
public float AlphaF => Alpha / 255f;
///
/// Gets the blue value of this color as a float (0-1).
///
+ [JsonIgnore]
public float BlueF => Blue / 255f;
///
/// Gets the green value of this color as a float (0-1).
///
+ [JsonIgnore]
public float GreenF => Green / 255f;
///
/// Gets the red value of this color as a float (0-1).
///
+ [JsonIgnore]
public float RedF => Red / 255f;
///
diff --git a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnNuiEvent.cs b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnNuiEvent.cs
index f96a38268..881d1b858 100644
--- a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnNuiEvent.cs
+++ b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnNuiEvent.cs
@@ -1,6 +1,5 @@
using System;
using Anvil.API.Events;
-using Newtonsoft.Json;
using NWN.Core;
namespace Anvil.API.Events
@@ -73,7 +72,7 @@ public OnNuiEvent()
/// The payload data, or null if the event has no payload.
public T GetEventPayload()
{
- return JsonConvert.DeserializeObject(eventPayload);
+ return JsonUtility.FromJson(eventPayload);
}
}
}
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiBind.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiBind.cs
index 60eca11c4..26c986be6 100644
--- a/NWN.Anvil/src/main/API/Nui/Bindings/NuiBind.cs
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiBind.cs
@@ -27,8 +27,7 @@ public NuiBind(string key)
/// The current value of the binding.
public T GetBindValue(NwPlayer player, int uiToken)
{
- Json json = NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key);
- return JsonConvert.DeserializeObject(json.Dump());
+ return JsonUtility.FromJson(NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key));
}
///
@@ -39,8 +38,7 @@ public T GetBindValue(NwPlayer player, int uiToken)
/// The current values of the binding.
public List GetBindValues(NwPlayer player, int uiToken)
{
- Json json = NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key);
- return JsonConvert.DeserializeObject>(json.Dump());
+ return JsonUtility.FromJson>(NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key));
}
///
@@ -51,10 +49,7 @@ public List GetBindValues(NwPlayer player, int uiToken)
/// The new value to assign.
public void SetBindValue(NwPlayer player, int uiToken, T value)
{
- string jsonString = JsonConvert.SerializeObject(value);
- Json json = Json.Parse(jsonString);
-
- NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, json);
+ NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, JsonUtility.ToJsonStructure(value));
}
///
@@ -65,10 +60,7 @@ public void SetBindValue(NwPlayer player, int uiToken, T value)
/// The new value to assign.
public void SetBindValues(NwPlayer player, int uiToken, IEnumerable values)
{
- string jsonString = JsonConvert.SerializeObject(values);
- Json json = Json.Parse(jsonString);
-
- NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, json);
+ NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, JsonUtility.ToJsonStructure(values));
}
///
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValue.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValue.cs
index d37138a90..2d98de587 100644
--- a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValue.cs
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValue.cs
@@ -1,4 +1,3 @@
-using JetBrains.Annotations;
using Newtonsoft.Json;
namespace Anvil.API
@@ -15,7 +14,6 @@ public NuiValue(T value)
Value = value;
}
- [UsedImplicitly]
internal NuiValue() {}
///
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueConverter.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueConverter.cs
index c7e8fe33e..d7091a22a 100644
--- a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueConverter.cs
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueConverter.cs
@@ -13,7 +13,7 @@ public override bool CanConvert(Type objectType)
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
- object retVal = Activator.CreateInstance(objectType);
+ object retVal = Activator.CreateInstance(objectType, true);
if (retVal == null)
{
return null;
@@ -25,7 +25,9 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
return null;
}
- propertyInfo.SetValue(retVal, serializer.Deserialize(reader));
+ Type valueType = objectType.GetGenericArguments()[0];
+ propertyInfo.SetValue(retVal, serializer.Deserialize(reader, valueType));
+
return retVal;
}
diff --git a/NWN.Anvil/src/main/API/Nui/Layout/NuiGroup.cs b/NWN.Anvil/src/main/API/Nui/Layout/NuiGroup.cs
index e941a5821..7ac76df1b 100644
--- a/NWN.Anvil/src/main/API/Nui/Layout/NuiGroup.cs
+++ b/NWN.Anvil/src/main/API/Nui/Layout/NuiGroup.cs
@@ -38,8 +38,7 @@ public void SetLayout(NwPlayer player, int token, NuiLayout newLayout)
throw new InvalidOperationException("Layout cannot be updated as the NuiGroup does not have an ID.");
}
- Json json = Json.Parse(JsonConvert.SerializeObject(newLayout));
- NWScript.NuiSetGroupLayout(player.ControlledCreature, token, Id, json);
+ NWScript.NuiSetGroupLayout(player.ControlledCreature, token, Id, JsonUtility.ToJsonStructure(newLayout));
}
}
}
diff --git a/NWN.Anvil/src/main/API/Object/NwPlayer.cs b/NWN.Anvil/src/main/API/Object/NwPlayer.cs
index c088d3be2..2a445b246 100644
--- a/NWN.Anvil/src/main/API/Object/NwPlayer.cs
+++ b/NWN.Anvil/src/main/API/Object/NwPlayer.cs
@@ -7,7 +7,6 @@
using Anvil.API.Events;
using Anvil.Internal;
using Anvil.Services;
-using Newtonsoft.Json;
using NLog;
using NWN.Core;
using NWN.Native.API;
@@ -499,9 +498,7 @@ public void ClearTlkOverride(int strRef, bool restoreGlobal = true)
/// The window token on success (!= 0), or 0 on error.
public int CreateNuiWindow(NuiWindow window, string windowId = "")
{
- string jsonString = JsonConvert.SerializeObject(window);
- Json json = Json.Parse(jsonString);
- return NWScript.NuiCreate(ControlledCreature, json, windowId);
+ return NWScript.NuiCreate(ControlledCreature, JsonUtility.ToJsonStructure(window), windowId);
}
///
@@ -933,8 +930,7 @@ public void NuiDestroy(int uiToken)
/// The fetched data, or null if the window does not exist on the given player, or has no userdata set.
public T NuiGetUserData(int uiToken)
{
- Json json = NWScript.NuiGetUserData(ControlledCreature, uiToken);
- return JsonConvert.DeserializeObject(json.Dump());
+ return JsonUtility.FromJson(NWScript.NuiGetUserData(ControlledCreature, uiToken));
}
///
@@ -958,8 +954,7 @@ public string NuiGetWindowId(int uiToken)
/// The type of data to store. Must be serializable to JSON.
public void NuiSetUserData(int uiToken, T userData)
{
- Json json = Json.Parse(JsonConvert.SerializeObject(userData));
- NWScript.NuiSetUserData(ControlledCreature, uiToken, json);
+ NWScript.NuiSetUserData(ControlledCreature, uiToken, JsonUtility.ToJsonStructure(userData));
}
///
@@ -1293,9 +1288,7 @@ public async Task StoreCameraFacing()
/// True if the window was successfully created, otherwise false.
public bool TryCreateNuiWindow(NuiWindow window, out int token, string windowId = "")
{
- string jsonString = JsonConvert.SerializeObject(window);
- Json json = Json.Parse(jsonString);
- token = NWScript.NuiCreate(ControlledCreature, json, windowId);
+ token = NWScript.NuiCreate(ControlledCreature, JsonUtility.ToJsonStructure(window), windowId);
return token != 0;
}
diff --git a/NWN.Anvil/src/main/API/Utils/JsonUtility.cs b/NWN.Anvil/src/main/API/Utils/JsonUtility.cs
new file mode 100644
index 000000000..2628e10e6
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Utils/JsonUtility.cs
@@ -0,0 +1,55 @@
+using Newtonsoft.Json;
+
+namespace Anvil.API
+{
+ ///
+ /// Utility methods for serializing/deserializing JSON data.
+ ///
+ public static class JsonUtility
+ {
+ ///
+ /// Deserializes a Json game engine structure.
+ ///
+ /// The json to deserialize.
+ /// The type to deserialize to.
+ /// The deserialized object.
+ internal static T FromJson(Json json)
+ {
+ return JsonConvert.DeserializeObject(json.Dump());
+ }
+
+ ///
+ /// Deserializes a JSON string.
+ ///
+ /// The JSON to deserialize.
+ /// The type to deserialize to.
+ /// The deserialized object.
+ public static T FromJson(string json)
+ {
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ ///
+ /// Serializes a value as JSON.
+ ///
+ /// The value to serialize.
+ /// The type of the value to serialize.
+ /// A JSON string representing the value.
+ public static string ToJson(T value)
+ {
+ return JsonConvert.SerializeObject(value);
+ }
+
+ ///
+ /// Serializes a value as a JSON engine structure.
+ ///
+ /// The value to serialize.
+ /// The type of the value to serialize.
+ /// A JSON engine structure representing the value.
+ internal static Json ToJsonStructure(T value)
+ {
+ string serialized = ToJson(value);
+ return Json.Parse(serialized);
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Variable/Local/LocalVariableStruct.cs b/NWN.Anvil/src/main/API/Variable/Local/LocalVariableStruct.cs
index 303ba526f..23b7e51ad 100644
--- a/NWN.Anvil/src/main/API/Variable/Local/LocalVariableStruct.cs
+++ b/NWN.Anvil/src/main/API/Variable/Local/LocalVariableStruct.cs
@@ -1,4 +1,3 @@
-using System.Text.Json;
using NWN.Core;
namespace Anvil.API
@@ -15,8 +14,8 @@ public sealed class LocalVariableStruct : LocalVariable
{
public override T Value
{
- get => HasValue ? JsonSerializer.Deserialize(((Json)NWScript.GetLocalJson(Object, Name)).Dump()) : default;
- set => NWScript.SetLocalJson(Object, Name, Json.Parse(JsonSerializer.Serialize(value)));
+ get => HasValue ? JsonUtility.FromJson(NWScript.GetLocalJson(Object, Name)) : default;
+ set => NWScript.SetLocalJson(Object, Name, JsonUtility.ToJsonStructure(value));
}
public override void Delete()
diff --git a/NWN.Anvil/src/main/API/Variable/ObjectStorage/ObjectStorageVariableStruct.cs b/NWN.Anvil/src/main/API/Variable/ObjectStorage/ObjectStorageVariableStruct.cs
index 349983388..6983d04b5 100644
--- a/NWN.Anvil/src/main/API/Variable/ObjectStorage/ObjectStorageVariableStruct.cs
+++ b/NWN.Anvil/src/main/API/Variable/ObjectStorage/ObjectStorageVariableStruct.cs
@@ -1,4 +1,3 @@
-using System.Text.Json;
using Anvil.Services;
namespace Anvil.API
@@ -12,8 +11,8 @@ public abstract class ObjectStorageVariableStruct : ObjectStorageVariable
public sealed override T Value
{
- get => HasValue ? JsonSerializer.Deserialize(ObjectStorageService.GetObjectStorage(Object).GetString(ObjectStoragePrefix, Key)) : default;
- set => ObjectStorageService.GetObjectStorage(Object).Set(ObjectStoragePrefix, Key, JsonSerializer.Serialize(value), Persist);
+ get => HasValue ? JsonUtility.FromJson(ObjectStorageService.GetObjectStorage(Object).GetString(ObjectStoragePrefix, Key)) : default;
+ set => ObjectStorageService.GetObjectStorage(Object).Set(ObjectStoragePrefix, Key, JsonUtility.ToJson(value), Persist);
}
protected sealed override string VariableTypePrefix => "PERSTR!";