diff --git a/CHANGELOG.md b/CHANGELOG.md
index 435e60e18..a78b9fc83 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+## 8193.34.20
+https://github.com/nwn-dotnet/Anvil/compare/v8193.34.19...v8193.34.20
+
+### Added
+- CollectionExtensions: Added IList `AddRange` extension.
+- NwCreature: Added `IsBartering` property.
+- NwObject: Added `TryGetUUID` method.
+- NwObject: Added `SerializeToJson` method.
+- NwStore: Added `BuyStolenGoods`, `MarkDown`, `MarkDownStolen`, `MarkUp`, `WillNotBuyItems`, `WillOnlyBuyItems` properties.
+
+### Package Updates
+- LightInject: 6.5.1 -> 6.6.1
+
+### Changed
+- Exposed Json engine structure.
+
+### Fixed
+- VirtualMachine: Fixed an issue where ObjectSelf would not be correctly assigned.
+
## 8193.34.19
https://github.com/nwn-dotnet/Anvil/compare/v8193.34.18...v8193.34.19
diff --git a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
index bf08a3373..f87d7a193 100644
--- a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
+++ b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
@@ -56,7 +56,7 @@
-
+
diff --git a/NWN.Anvil.Tests/src/main/Services/API/Utils/VirtualMachineTests.cs b/NWN.Anvil.Tests/src/main/Services/API/Utils/VirtualMachineTests.cs
new file mode 100644
index 000000000..2bc6308f3
--- /dev/null
+++ b/NWN.Anvil.Tests/src/main/Services/API/Utils/VirtualMachineTests.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Anvil.API;
+using Anvil.Services;
+using NUnit.Framework;
+
+namespace Anvil.Tests.Services.API.Utils
+{
+ [TestFixture(Category = "Services.API")]
+ public sealed class VirtualMachineTests
+ {
+ [Inject]
+ private static VirtualMachine VirtualMachine { get; set; } = null!;
+
+ [Inject]
+ private static ScriptHandleFactory ScriptHandleFactory { get; set; } = null!;
+
+ [Test(Description = "Running ExecuteScript correctly executes the specified script with the right object context.")]
+ [Timeout(5000)]
+ public void ExecuteScriptAssignsCorrectObjectSelf()
+ {
+ foreach (NwObject nwObject in GetTestObjects())
+ {
+ ExecuteScript(nwObject);
+ }
+ }
+
+ [Test(Description = "Running ExecuteScript in the main loop correctly executes the specified script with the right object context.")]
+ [Timeout(5000)]
+ public async Task ExecuteScriptInvalidContextAssignsCorrectObjectSelf()
+ {
+ await NwTask.NextFrame();
+ foreach (NwObject nwObject in GetTestObjects())
+ {
+ ExecuteScript(nwObject);
+ }
+ }
+
+ private void ExecuteScript(NwObject nwObject)
+ {
+ string scriptParamKey = "someParamKey";
+ string scriptParamValue = "someParamValue";
+
+ ScriptCallbackHandle handle = ScriptHandleFactory.CreateUniqueHandler(info =>
+ {
+ Assert.That(info.ObjectSelf, Is.EqualTo(nwObject));
+ Assert.That(info.ScriptParams[scriptParamKey], Is.EqualTo(scriptParamValue));
+ return ScriptHandleResult.Handled;
+ });
+
+ VirtualMachine.Execute(handle.ScriptName, nwObject, (scriptParamKey, scriptParamValue));
+ }
+
+ private static IEnumerable GetTestObjects()
+ {
+ yield return NwModule.Instance;
+
+ foreach (NwGameObject gameObject in NwModule.Instance.Areas.First().Objects)
+ {
+ yield return gameObject;
+ }
+ }
+ }
+}
diff --git a/NWN.Anvil/NWN.Anvil.csproj b/NWN.Anvil/NWN.Anvil.csproj
index 435f5250c..d1aa23d6c 100644
--- a/NWN.Anvil/NWN.Anvil.csproj
+++ b/NWN.Anvil/NWN.Anvil.csproj
@@ -57,7 +57,7 @@
-
+
diff --git a/NWN.Anvil/src/main/API/EngineStructure/Json.cs b/NWN.Anvil/src/main/API/EngineStructure/Json.cs
index 3e1e03a29..62f87caad 100644
--- a/NWN.Anvil/src/main/API/EngineStructure/Json.cs
+++ b/NWN.Anvil/src/main/API/EngineStructure/Json.cs
@@ -3,7 +3,7 @@
namespace Anvil.API
{
- internal sealed class Json : EngineStructure
+ public sealed class Json : EngineStructure
{
internal Json(IntPtr handle, bool memoryOwn) : base(handle, memoryOwn) {}
@@ -23,5 +23,15 @@ public string Dump()
{
return NWScript.JsonDump(this);
}
+
+ public NwObject? ToNwObject(Location location, NwGameObject? owner = null, bool loadObjectState = true)
+ {
+ return NWScript.JsonToObject(this, location, owner, loadObjectState.ToInt()).ToNwObject();
+ }
+
+ public T? ToNwObject(Location location, NwGameObject? owner = null, bool loadObjectState = true) where T : NwObject
+ {
+ return NWScript.JsonToObject(this, location, owner, loadObjectState.ToInt()).ToNwObject();
+ }
}
}
diff --git a/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs b/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
index 8cf84eba1..234054616 100644
--- a/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
+++ b/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
@@ -39,6 +39,27 @@ public static bool ContainsElement(this IDictionary
+ /// Adds a range of values to the specified IList.
+ ///
+ /// The list to update.
+ /// The elements to add to the list.
+ /// The list element type.
+ public static void AddRange(this IList list, IEnumerable values)
+ {
+ if (list is List genericList)
+ {
+ genericList.AddRange(values);
+ }
+ else
+ {
+ foreach (T value in values)
+ {
+ list.Add(value);
+ }
+ }
+ }
+
public static void DisposeAll(this IEnumerable? disposables)
{
if (disposables == null)
diff --git a/NWN.Anvil/src/main/API/Object/NwCreature.cs b/NWN.Anvil/src/main/API/Object/NwCreature.cs
index e160df10a..d03dd082c 100644
--- a/NWN.Anvil/src/main/API/Object/NwCreature.cs
+++ b/NWN.Anvil/src/main/API/Object/NwCreature.cs
@@ -439,6 +439,11 @@ public bool Immortal
///
public Inventory Inventory { get; }
+ ///
+ /// Gets a value indicating whether this creature is currently bartering.
+ ///
+ public bool IsBartering => Creature.m_pBarterInfo?.m_bWindowOpen.ToBool() == true;
+
///
/// Gets a value indicating whether this creature is a dead NPC, dead PC, or dying PC.
///
diff --git a/NWN.Anvil/src/main/API/Object/NwObject.cs b/NWN.Anvil/src/main/API/Object/NwObject.cs
index cb01fc006..f49865161 100644
--- a/NWN.Anvil/src/main/API/Object/NwObject.cs
+++ b/NWN.Anvil/src/main/API/Object/NwObject.cs
@@ -142,23 +142,26 @@ public string Tag
///
/// Gets the globally unique identifier for this object.
///
+ ///
+ /// If the UUID conflicts with an existing object, a new one will be generated.
+ /// Use to control this behaviour.
+ ///
public Guid UUID
{
get
{
- if (this == Invalid)
+ if (!IsValid)
{
return Guid.Empty;
}
- string uid = NWScript.GetObjectUUID(this);
- if (string.IsNullOrEmpty(uid))
+ if (!TryGetUUID(out Guid uid))
{
ForceRefreshUUID();
- uid = NWScript.GetObjectUUID(this);
+ TryGetUUID(out uid);
}
- return Guid.TryParse(uid, out Guid guid) ? guid : Guid.Empty;
+ return uid;
}
}
@@ -334,6 +337,34 @@ public override string ToString()
return ObjectId.ToString("x");
}
+ ///
+ /// Attempts to get the UUID for this object, assigning a new ID if it does not already exist.
+ ///
+ /// See to check if the object has an existing UUID, without creating a new one.
+ /// This function will return false if the UUID is not globally unique, and conflicts with an existing object.
+ ///
+ /// The object's UUID.
+ /// True if the object has a valid unique identifier, otherwise false.
+ public bool TryGetUUID(out Guid uid)
+ {
+ string uidString = NWScript.GetObjectUUID(this);
+ if (!string.IsNullOrEmpty(uidString))
+ {
+ return Guid.TryParse(uidString, out uid);
+ }
+
+ uid = Guid.Empty;
+ return false;
+ }
+
+ ///
+ /// Serializes this game object to a json representation
+ ///
+ public Json SerializeToJson(bool saveObjectState)
+ {
+ return NWScript.ObjectToJson(this, saveObjectState.ToInt());
+ }
+
///
/// Notifies then awaits for this object to become the current active object for the purpose of implicitly assigned values (e.g. effect creators).
/// If the current active object is already this object, then the code runs immediately. Otherwise, it will be run with all other closures.
diff --git a/NWN.Anvil/src/main/API/Object/NwStore.cs b/NWN.Anvil/src/main/API/Object/NwStore.cs
index faa991736..4feb83d6e 100644
--- a/NWN.Anvil/src/main/API/Object/NwStore.cs
+++ b/NWN.Anvil/src/main/API/Object/NwStore.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Anvil.Native;
using NWN.Core;
using NWN.Native.API;
@@ -27,6 +28,15 @@ internal NwStore(CNWSStore store) : base(store)
this.store = store;
}
+ ///
+ /// Gets or sets if this store purchases stolen goods.
+ ///
+ public bool BuyStolenGoods
+ {
+ get => Store.m_bBlackMarket.ToBool();
+ set => Store.m_bBlackMarket = value.ToInt();
+ }
+
///
/// Gets the current customers of this store.
///
@@ -55,10 +65,14 @@ public IReadOnlyList CurrentCustomers
///
public int CustomerCount => Store.m_aCurrentCustomers.Count;
+ ///
+ /// Gets or sets the amount this store charges to identify an item.
+ /// Returns -1 if the store does not identify items.
+ ///
public int IdentifyCost
{
- get => NWScript.GetStoreIdentifyCost(this);
- set => NWScript.SetStoreIdentifyCost(this, value);
+ get => Store.m_iIdentifyCost;
+ set => Store.m_iIdentifyCost = value;
}
///
@@ -75,16 +89,71 @@ public IEnumerable Items
}
}
+ ///
+ /// Gets or sets the base markdown price for items sold to this store.
+ ///
+ public int MarkDown
+ {
+ get => Store.m_nMarkDown;
+ set => Store.m_nMarkDown = value;
+ }
+
+ ///
+ /// Gets or sets the base markdown price for stolen items sold to this store.
+ ///
+ public int MarkDownStolen
+ {
+ get => Store.m_nBlackMarketMarkDown;
+ set => Store.m_nBlackMarketMarkDown = value;
+ }
+
+ ///
+ /// Gets or sets the base markup price for items in the store's inventory.
+ ///
+ public int MarkUp
+ {
+ get => Store.m_nMarkUp;
+ set => Store.m_nMarkUp = value;
+ }
+
+ ///
+ /// Gets or sets the maximum price this store will pay for an item.
+ /// Returns -1 if the store has no limit.
+ ///
public int MaxBuyPrice
{
- get => NWScript.GetStoreMaxBuyPrice(this);
- set => NWScript.SetStoreMaxBuyPrice(this, value);
+ get => Store.m_iMaxBuyPrice;
+ set => Store.m_iMaxBuyPrice = value;
}
public int StoreGold
{
- get => NWScript.GetStoreGold(this);
- set => NWScript.SetStoreGold(this, value);
+ get => Store.m_iGold;
+ set => Store.m_iGold = value;
+ }
+
+ ///
+ /// Gets the list of base item types that this store will not buy.
+ /// Has precedence over .
+ ///
+ public IList WillNotBuyItems
+ {
+ get
+ {
+ return new ListWrapper(Store.m_lstWillNotBuy, NwBaseItem.FromItemId, item => (int)(item?.Id ?? 0));
+ }
+ }
+
+ ///
+ /// Gets the list of base item types that this store will only buy.
+ /// Does nothing if is populated.
+ ///
+ public IList WillOnlyBuyItems
+ {
+ get
+ {
+ return new ListWrapper(Store.m_lstWillOnlyBuy, NwBaseItem.FromItemId, item => (int)(item?.Id ?? 0));
+ }
}
public static NwStore? Create(string template, Location location, bool useAppearAnim = false, string newTag = "")
@@ -124,13 +193,12 @@ public int StoreGold
return store?.Store;
}
+ ///
+ /// Adds the specified item to this store's inventory.
+ ///
+ /// The item to add.
public void AcquireItem(NwItem item)
{
- if (item == null)
- {
- throw new ArgumentNullException(nameof(item), "Item cannot be null.");
- }
-
Store.AcquireItem(item.Item, true.ToInt(), 0xFF, 0xFF);
}
diff --git a/NWN.Anvil/src/main/API/Utils/VirtualMachine.cs b/NWN.Anvil/src/main/API/Utils/VirtualMachine.cs
index 343e7bc09..74d8609ee 100644
--- a/NWN.Anvil/src/main/API/Utils/VirtualMachine.cs
+++ b/NWN.Anvil/src/main/API/Utils/VirtualMachine.cs
@@ -77,7 +77,7 @@ public void Execute(string scriptName, NwObject? target, params (string ParamNam
NWScript.SetScriptParam(paramName, paramValue);
}
- NWScript.ExecuteScript(scriptName, target);
+ virtualMachine.RunScript(scriptName.ToExoString(), target);
}
///
diff --git a/NWN.Anvil/src/main/Native/ListWrapper.cs b/NWN.Anvil/src/main/Native/ListWrapper.cs
new file mode 100644
index 000000000..65b20ab48
--- /dev/null
+++ b/NWN.Anvil/src/main/Native/ListWrapper.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Anvil.Native
+{
+ internal sealed class ListWrapper : IList
+ {
+ private readonly IList list;
+ private readonly Func get;
+ private readonly Func set;
+
+ public int Count => list.Count;
+ public bool IsReadOnly => list.IsReadOnly;
+
+ public ListWrapper(IList list, Func get, Func set)
+ {
+ this.list = list;
+ this.get = get;
+ this.set = set;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ foreach (T1 value in list)
+ {
+ yield return get(value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public void Add(T2 item)
+ {
+ T1 value = set(item);
+ list.Add(value);
+ }
+
+ public void Clear()
+ {
+ list.Clear();
+ }
+
+ public bool Contains(T2 item)
+ {
+ T1 value = set(item);
+ return list.Contains(value);
+ }
+
+ public void CopyTo(T2[] array, int arrayIndex)
+ {
+ T1[] values = new T1[array.Length];
+ for (int i = 0; i < array.Length; i++)
+ {
+ values[i] = set(array[i]);
+ }
+ }
+
+ public bool Remove(T2 item)
+ {
+ T1 value = set(item);
+ return list.Contains(value);
+ }
+
+ public int IndexOf(T2 item)
+ {
+ T1 value = set(item);
+ return list.IndexOf(value);
+ }
+
+ public void Insert(int index, T2 item)
+ {
+ T1 value = set(item);
+ list.Insert(index, value);
+ }
+
+ public void RemoveAt(int index)
+ {
+ list.RemoveAt(index);
+ }
+
+ public T2 this[int index]
+ {
+ get => get(list[index]);
+ set => list[index] = set(value);
+ }
+ }
+}