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); + } + } +}