diff --git a/.env b/.env
index 7ed132ebd..e6ca9a206 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
NWN_VERSION=8193.34
-NWNX_VERSION=2692ecb
+NWNX_VERSION=d44d373
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d04532722..221fc06d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,41 @@ 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.26
+https://github.com/nwn-dotnet/Anvil/compare/v8193.34.25...v8193.34.26
+
+### Added
+- NwGameTables: Added `EffectIconTable`.
+- CreatureClassInfo: Added `School`, `KnownSpells` properties.
+- CreatureLevelInfo: Added `AddedKnownSpells`, `RemovedKnownSpells` properties.
+
+### Package Updates
+- NWNX: 2692ecb -> d44d373
+- NWN.Core: 8193.34.12 -> 8193.34.15
+- NWN.Native: 8193.34.5 -> 8193.34.7
+- Newtonsoft.Json: 13.0.2 -> 13.0.3
+- NLog: 5.1.2 -> 5.1.3
+- Paket.Core: 7.2.0 -> 7.2.1
+
+### Changed
+- CollectionExtensions: `InsertOrdered` now returns the index in which the item was inserted.
+- Anvil will now log a managed stack trace during an assertion failure. We're hoping this will help track down issues where the nwscript VM reports an invalid stack state.
+
+### Deprecated
+- `CreatureClassInfo.AddKnownSpell` - use `CreatureClassInfo.KnownSpells[].Add` instead.
+- `CreatureClassInfo.RemoveKnownSpell` - use `CreatureClassInfo.KnownSpells[].Remove` instead.
+- `CreatureClassInfo.GetKnownSpellCountByLevel` - use `CreatureClassInfo.KnownSpells[].Count` instead.
+- `CreatureClassInfo.GetKnownSpells` - use `CreatureClassInfo.KnownSpells` instead.
+
+### Fixed
+- Fixed null/empty script names not clearing object event scripts.
+- Fixed an issue where an invalid script name could be assigned to an object event.
+- Fixed a NRE in the `ModuleEvents.OnAcquireItem` event caused by characters failing ELC.
+- Fixed a cast exception in the `PlaceableEvents.OnDisturbed` event when the last inventory event was not caused by a creature.
+- NwStore: Fixed `WillNotBuyItems`, `WillOnlyBuyItems` lists not removing items, and LINQ functions (ToList/ToArray) not working.
+- CreatureLevelInfo: `ClassInfo` now returns the correct creature class.
+- ItemPropertyItemMapTable: Fixed some item property values returning valid when they shouldn't be.
+
## 8193.34.25
https://github.com/nwn-dotnet/Anvil/compare/v8193.34.24...v8193.34.25
diff --git a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
index 817b1ec4d..85594a93d 100644
--- a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
+++ b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
@@ -69,8 +69,8 @@
-
-
+
+
diff --git a/NWN.Anvil.TestRunner/src/lib/nunit b/NWN.Anvil.TestRunner/src/lib/nunit
index 2e7e89ddb..0eb5286fe 160000
--- a/NWN.Anvil.TestRunner/src/lib/nunit
+++ b/NWN.Anvil.TestRunner/src/lib/nunit
@@ -1 +1 @@
-Subproject commit 2e7e89ddb47662b573b9e60c9b15a5a84e6d7236
+Subproject commit 0eb5286fee2548146c0026d470aa7096a924da41
diff --git a/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj
index 68f77ccf5..1b2bc797b 100644
--- a/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj
+++ b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj
@@ -42,8 +42,8 @@
-
-
+
+
diff --git a/NWN.Anvil.Tests/src/main/API/Objects/NwObjectTests.cs b/NWN.Anvil.Tests/src/main/API/Objects/NwObjectTests.cs
index 2a01e56a4..7e0a36a2b 100644
--- a/NWN.Anvil.Tests/src/main/API/Objects/NwObjectTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/Objects/NwObjectTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Anvil.API;
@@ -51,6 +52,69 @@ public async Task QueueCreatureActionIsQueued()
Assert.That(actionExecuted, Is.EqualTo(true));
}
+ [Test(Description = "Tests if assigning a valid event script correctly updates the event script.")]
+ [TestCase("my_event")]
+ [TestCase("")]
+ [TestCase(null)]
+ public void SetValidEventScriptCorrectlyUpdatesEventScript(string? script)
+ {
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation)!;
+ Assert.That(creature, Is.Not.Null);
+
+ createdTestObjects.Add(creature);
+
+ creature.SetEventScript(EventScriptType.CreatureOnSpawnIn, script);
+
+ Assert.That(creature.GetEventScript(EventScriptType.CreatureOnSpawnIn), script == null ? Is.EqualTo(string.Empty) : Is.EqualTo(script));
+ }
+
+ [Test(Description = "Tests if assigning an invalid event script correctly throws an exception")]
+ [TestCase("reallylongscriptname")]
+ [TestCase("@&^/*7")]
+ [TestCase(ScriptConstants.GameEventScriptName)]
+ [TestCase(ScriptConstants.NWNXEventScriptName)]
+ public void SetInvalidEventScriptCorrectlyThrowsException(string? script)
+ {
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation)!;
+ Assert.That(creature, Is.Not.Null);
+
+ createdTestObjects.Add(creature);
+
+ Assert.Throws(() =>
+ {
+ creature.SetEventScript(EventScriptType.CreatureOnSpawnIn, script);
+ });
+ }
+
+ [Test(Description = "Tests if attempting to set an event script after subscribing correctly throws an exception.")]
+ public void SetEventScriptAfterSubscribeCorrectlyThrowsException()
+ {
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation)!;
+ Assert.That(creature, Is.Not.Null);
+
+ createdTestObjects.Add(creature);
+ creature.OnSpawn += _ => {};
+
+ Assert.Throws(() =>
+ {
+ creature.SetEventScript(EventScriptType.CreatureOnSpawnIn, null);
+ });
+ }
+
+ [Test(Description = "Tests if attempting to set an invalid event type correctly throws an exception.")]
+ public void SetInvalidEventScriptCorrectlyThrowsException()
+ {
+ NwCreature creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, NwModule.Instance.StartingLocation)!;
+ Assert.That(creature, Is.Not.Null);
+
+ createdTestObjects.Add(creature);
+
+ Assert.Throws(() =>
+ {
+ creature.SetEventScript(EventScriptType.ModuleOnClientEnter, null);
+ });
+ }
+
[TearDown]
public void CleanupTestObjects()
{
diff --git a/NWN.Anvil/NWN.Anvil.csproj b/NWN.Anvil/NWN.Anvil.csproj
index 54518063a..cb5193850 100644
--- a/NWN.Anvil/NWN.Anvil.csproj
+++ b/NWN.Anvil/NWN.Anvil.csproj
@@ -58,11 +58,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/NWN.Anvil/src/main/API/Async/NwTask.cs b/NWN.Anvil/src/main/API/Async/NwTask.cs
index c8c3c4954..160da6446 100644
--- a/NWN.Anvil/src/main/API/Async/NwTask.cs
+++ b/NWN.Anvil/src/main/API/Async/NwTask.cs
@@ -10,7 +10,7 @@
namespace Anvil.API
{
//! ## Examples
- //! @import NwTaskExamples.cs
+ //! @include NwTaskExamples.cs
///
/// Asynchronous tasks and helpers for running NWN APIs in an async context.
diff --git a/NWN.Anvil/src/main/API/EngineStructures/Cassowary.cs b/NWN.Anvil/src/main/API/EngineStructures/Cassowary.cs
index 3573090c5..117a692b8 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/Cassowary.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/Cassowary.cs
@@ -4,7 +4,7 @@
namespace Anvil.API
{
//! ## Examples
- //! @import CassowaryExamples.cs
+ //! @include CassowaryExamples.cs
///
/// Represents a Cassowary engine structure.
diff --git a/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs b/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
index 877ddfe3f..62000ffbd 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
@@ -411,13 +411,19 @@ public static Effect HitPointChangeWhenDying(float hpChangePerRound)
return NWScript.EffectHitPointChangeWhenDying(hpChangePerRound)!;
}
+ [Obsolete("Use the EffectIconTableEntry overload instead.")]
+ public static Effect Icon(EffectIcon icon)
+ {
+ return NWScript.EffectIcon((int)icon)!;
+ }
+
///
/// Creates an icon effect. Icons appear in the top right and in the character sheet and examine panels.
///
/// The icon to display.
- public static Effect Icon(EffectIcon icon)
+ public static Effect Icon(EffectIconTableEntry icon)
{
- return NWScript.EffectIcon((int)icon)!;
+ return NWScript.EffectIcon(icon.RowIndex)!;
}
///
diff --git a/NWN.Anvil/src/main/API/EngineStructures/Effect.cs b/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
index f670d961b..691bf2a85 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
@@ -5,7 +5,7 @@
namespace Anvil.API
{
//! ## Examples
- //! @import EffectExamples.cs
+ //! @include EffectExamples.cs
///
/// Represents an effect engine structure.
diff --git a/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs b/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs
index 486f85e99..141263e59 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs
@@ -5,7 +5,7 @@
namespace Anvil.API
{
//! ## Examples
- //! @import ItemPropertyExamples.cs
+ //! @include ItemPropertyExamples.cs
///
/// Represents an item property effect engine structure.
diff --git a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnAcquireItem.cs b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnAcquireItem.cs
index e8b89a2e8..c7a7db41e 100644
--- a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnAcquireItem.cs
+++ b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnAcquireItem.cs
@@ -12,6 +12,10 @@ public static partial class ModuleEvents
///
/// Triggered whenever an is added to inventory.
///
+ ///
+ /// This event fires for all items when a player connects to the server, in addition to item/inventory interactions while playing.
+ /// It will also fire for characters failing ELC. In this case, it is recommended to do an early return in your event handler by checking if is null.
+ ///
[GameEvent(EventScriptType.ModuleOnAcquireItem)]
public sealed class OnAcquireItem : IEvent
{
@@ -19,7 +23,7 @@ public OnAcquireItem()
{
// Patch player reference due to a reference bug during client enter context
// See https://github.com/Beamdog/nwn-issues/issues/367
- if (AcquiredBy is null && Item.Possessor is NwCreature creature)
+ if (AcquiredBy is null && Item?.Possessor is NwCreature creature)
{
AcquiredBy = creature;
}
@@ -43,7 +47,10 @@ public OnAcquireItem()
///
/// Gets the that triggered the event.
///
- public NwItem Item { get; } = NWScript.GetModuleItemAcquired().ToNwObject()!;
+ ///
+ /// This property will return null when a character fails ELC. It is recommended to do an early exit if this is null.
+ ///
+ public NwItem? Item { get; } = NWScript.GetModuleItemAcquired().ToNwObject();
NwObject? IEvent.Context => AcquiredBy;
}
diff --git a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerDeath.cs b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerDeath.cs
index 071158e21..d6e2b9d2e 100644
--- a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerDeath.cs
+++ b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerDeath.cs
@@ -17,8 +17,8 @@ public sealed class OnPlayerDeath : IEvent
{
public OnPlayerDeath()
{
- Killer = NWScript.GetLastHostileActor(DeadPlayer.ControlledCreature).ToNwObject()
- ?? NWScript.GetLastDamager(DeadPlayer.ControlledCreature).ToNwObject();
+ Killer = NWScript.GetLastHostileActor(DeadPlayer.ControlledCreature).ToNwObject()
+ ?? NWScript.GetLastDamager(DeadPlayer.ControlledCreature).ToNwObject();
}
///
@@ -29,7 +29,7 @@ public OnPlayerDeath()
///
/// Gets the that caused to trigger the event.
///
- public NwGameObject? Killer { get; }
+ public NwObject? Killer { get; }
NwObject? IEvent.Context => DeadPlayer.ControlledCreature;
}
diff --git a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerGuiEvent.cs b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerGuiEvent.cs
index 177cec0b8..569e2fb6c 100644
--- a/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerGuiEvent.cs
+++ b/NWN.Anvil/src/main/API/Events/Game/ModuleEvents/ModuleEvents.OnPlayerGuiEvent.cs
@@ -25,7 +25,7 @@ public sealed class OnPlayerGuiEvent : IEvent
///
/// Gets the effect icon that was selected. Only valid in events.
///
- public EffectIcon EffectIcon => (EffectIcon)integerEventData;
+ public EffectIconTableEntry EffectIcon => NwGameTables.EffectIconTable[integerEventData];
///
/// Gets the object data associated with this GUI event.
diff --git a/NWN.Anvil/src/main/API/Events/Game/PlaceableEvents/PlaceableEvents.OnDisturbed.cs b/NWN.Anvil/src/main/API/Events/Game/PlaceableEvents/PlaceableEvents.OnDisturbed.cs
index cd79b2b2f..a3d1fad28 100644
--- a/NWN.Anvil/src/main/API/Events/Game/PlaceableEvents/PlaceableEvents.OnDisturbed.cs
+++ b/NWN.Anvil/src/main/API/Events/Game/PlaceableEvents/PlaceableEvents.OnDisturbed.cs
@@ -21,9 +21,9 @@ public sealed class OnDisturbed : IEvent
public NwItem? DisturbedItem { get; } = NWScript.GetInventoryDisturbItem().ToNwObject();
///
- /// Gets the that disturbed .
+ /// Gets the object that disturbed .
///
- public NwCreature? Disturber { get; } = NWScript.GetLastDisturbed().ToNwObject();
+ public NwGameObject? Disturber { get; } = NWScript.GetLastDisturbed().ToNwObject();
///
/// Gets the .
diff --git a/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs b/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
index 234054616..2800d156c 100644
--- a/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
+++ b/NWN.Anvil/src/main/API/Extensions/CollectionExtensions.cs
@@ -80,11 +80,13 @@ public static void DisposeAll(this IEnumerable? disposables)
/// The item to insert.
/// A custom comparer to use when comparing the item against elements in the collection.
/// The type of item to insert.
- public static void InsertOrdered(this List sortedList, T item, IComparer? comparer = null)
+ public static int InsertOrdered(this List sortedList, T item, IComparer? comparer = null)
{
int binaryIndex = sortedList.BinarySearch(item, comparer);
int index = binaryIndex < 0 ? ~binaryIndex : binaryIndex;
sortedList.Insert(index, item);
+
+ return index;
}
///
diff --git a/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs b/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
index fa62261e6..a5f15d223 100644
--- a/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
+++ b/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
@@ -33,15 +33,27 @@ public static bool IsReservedScriptName(this string scriptName)
return lowerName is ScriptConstants.GameEventScriptName or ScriptConstants.NWNXEventScriptName;
}
- public static bool IsValidScriptName(this string scriptName)
+ public static bool IsValidScriptName(this string? scriptName, bool allowEmpty)
{
if (string.IsNullOrEmpty(scriptName))
+ {
+ return allowEmpty;
+ }
+
+ if (scriptName.Length > 16)
{
return false;
}
- string lowerName = scriptName.ToLower();
- return lowerName != ScriptConstants.GameEventScriptName && lowerName != ScriptConstants.NWNXEventScriptName;
+ foreach (char c in scriptName)
+ {
+ if (!char.IsLetterOrDigit(c) && c != '_' && c != '-')
+ {
+ return false;
+ }
+ }
+
+ return !scriptName.Equals(ScriptConstants.GameEventScriptName, StringComparison.OrdinalIgnoreCase) && !scriptName.Equals(ScriptConstants.NWNXEventScriptName, StringComparison.OrdinalIgnoreCase);
}
///
diff --git a/NWN.Anvil/src/main/API/Objects/CreatureClassInfo.cs b/NWN.Anvil/src/main/API/Objects/CreatureClassInfo.cs
index 2421064b5..bfad1b038 100644
--- a/NWN.Anvil/src/main/API/Objects/CreatureClassInfo.cs
+++ b/NWN.Anvil/src/main/API/Objects/CreatureClassInfo.cs
@@ -1,11 +1,15 @@
+using System;
using System.Collections.Generic;
using Anvil.Native;
using NWN.Native.API;
namespace Anvil.API
{
- public sealed class CreatureClassInfo
+ public sealed unsafe class CreatureClassInfo
{
+ private const int KnownSpellArraySize = 10; // Cantrips + 9 spell levels
+ private static readonly int KnownSpellArrayListStructSize = sizeof(IntPtr) + sizeof(int) + sizeof(int);
+
private readonly CNWSCreatureStats_ClassInfo classInfo;
internal CreatureClassInfo(CNWSCreatureStats_ClassInfo classInfo)
@@ -23,11 +27,29 @@ internal CreatureClassInfo(CNWSCreatureStats_ClassInfo classInfo)
/// Domains can be modified by editing the contents of this array.
///
/// By default, a non-domain class will be populated with and (index 0 and 1 respectively).
- public IArray Domains
+ public IArray Domains => new ArrayWrapper(classInfo.m_nDomain, id => NwDomain.FromDomainId(id), domain => domain?.Id ?? 0);
+
+ ///
+ /// Gets a mutable list of known spells.
+ /// The returned array is indexed by spell level, 0 = cantrips, 1 = level 1 spells, etc.
+ ///
+ ///
+ /// When used on players, you also need to update and on the relevant level taken in this class, otherwise players will fail ELC checks.
+ ///
+ public IReadOnlyList> KnownSpells
{
get
{
- return new ArrayWrapper(classInfo.m_nDomain, id => NwDomain.FromDomainId(id), domain => domain?.Id ?? 0);
+ IList[] spells = new IList[KnownSpellArraySize];
+ IntPtr ptr = classInfo.m_pKnownSpellList.Pointer;
+
+ for (int i = 0; i < spells.Length; i++)
+ {
+ CExoArrayListUInt32 spellList = CExoArrayListUInt32.FromPointer(ptr + i * KnownSpellArrayListStructSize);
+ spells[i] = new ListWrapper(spellList, spellId => NwSpell.FromSpellId((int)spellId)!, spell => (uint)spell.Id);
+ }
+
+ return spells;
}
}
@@ -41,11 +63,17 @@ public IArray Domains
///
public byte NegativeLevels => classInfo.m_nNegativeLevels;
+ ///
+ /// Gets the spell school for this class.
+ ///
+ public SpellSchool School => (SpellSchool)classInfo.m_nSchool;
+
///
/// Adds the specified spell as a known spell at the specified spell level.
///
/// The spell to be added.
/// The spell level for the spell to be added.
+ [Obsolete("Use the KnownSpells property instead.")]
public void AddKnownSpell(NwSpell spell, byte spellLevel)
{
classInfo.AddKnownSpell(spellLevel, spell.Id.AsUInt());
@@ -65,6 +93,7 @@ public void ClearMemorizedKnownSpells(NwSpell spell)
///
/// The spell level to query.
/// An integer representing the number of spells known.
+ [Obsolete("Use the KnownSpells property instead.")]
public ushort GetKnownSpellCountByLevel(byte spellLevel)
{
return classInfo.GetNumberKnownSpells(spellLevel);
@@ -75,6 +104,7 @@ public ushort GetKnownSpellCountByLevel(byte spellLevel)
///
/// The spell level to query.
/// A list containing the creatures known spells.
+ [Obsolete("Use the KnownSpells property instead.")]
public IReadOnlyList GetKnownSpells(byte spellLevel)
{
int spellCount = GetKnownSpellCountByLevel(spellLevel);
@@ -131,6 +161,7 @@ public byte GetRemainingSpellSlots(byte spellLevel)
///
/// The spell level to query.
/// The spell to remove.
+ [Obsolete("Use the KnownSpells property instead.")]
public void RemoveKnownSpell(byte spellLevel, NwSpell spell)
{
classInfo.RemoveKnownSpell(spellLevel, spell.Id.AsUInt());
diff --git a/NWN.Anvil/src/main/API/Objects/CreatureLevelInfo.cs b/NWN.Anvil/src/main/API/Objects/CreatureLevelInfo.cs
index 891180ebc..bcbb3fbdd 100644
--- a/NWN.Anvil/src/main/API/Objects/CreatureLevelInfo.cs
+++ b/NWN.Anvil/src/main/API/Objects/CreatureLevelInfo.cs
@@ -1,10 +1,16 @@
+using System;
using System.Collections.Generic;
+using System.Linq;
+using Anvil.Native;
using NWN.Native.API;
namespace Anvil.API
{
public sealed unsafe class CreatureLevelInfo
{
+ private const int KnownSpellArraySize = 10; // Cantrips + 9 spell levels
+ private static readonly int KnownSpellArrayListStructSize = sizeof(IntPtr) + sizeof(int) + sizeof(int);
+
private readonly NwCreature creature;
private readonly CNWLevelStats levelStats;
@@ -17,7 +23,14 @@ internal CreatureLevelInfo(NwCreature creature, CNWLevelStats levelStats)
///
/// Gets the class chosen at this level.
///
- public CreatureClassInfo ClassInfo => creature.Classes[levelStats.m_nClass - 1];
+ public CreatureClassInfo ClassInfo
+ {
+ get
+ {
+ byte classId = levelStats.m_nClass;
+ return creature.Classes.First(info => info.Class.Id == classId);
+ }
+ }
///
/// Gets the number of feats gained at this level.
@@ -32,7 +45,6 @@ public IReadOnlyList Feats
get
{
NwFeat[] feats = new NwFeat[FeatCount];
-
for (int i = 0; i < feats.Length; i++)
{
feats[i] = NwFeat.FromFeatId(levelStats.m_lstFeats[i])!;
@@ -42,6 +54,48 @@ public IReadOnlyList Feats
}
}
+ ///
+ /// Gets a mutable list of known spells added at this level.
+ /// The returned array is indexed by spell level, 0 = cantrips, 1 = level 1 spells, etc.
+ ///
+ public IReadOnlyList> AddedKnownSpells
+ {
+ get
+ {
+ IList[] spells = new IList[KnownSpellArraySize];
+ IntPtr ptr = levelStats.m_pAddedKnownSpellList.Pointer;
+
+ for (int i = 0; i < spells.Length; i++)
+ {
+ CExoArrayListUInt32? spellList = CExoArrayListUInt32.FromPointer(ptr + i * KnownSpellArrayListStructSize);
+ spells[i] = new ListWrapper(spellList, spellId => NwSpell.FromSpellId((int)spellId)!, spell => (uint)spell.Id);
+ }
+
+ return spells;
+ }
+ }
+
+ ///
+ /// Gets a mutable list of known spells removed at this level.
+ /// The returned array is indexed by spell level, 0 = cantrips, 1 = level 1 spells, etc.
+ ///
+ public IReadOnlyList> RemovedKnownSpells
+ {
+ get
+ {
+ IList[] spells = new IList[KnownSpellArraySize];
+ IntPtr ptr = levelStats.m_pRemovedKnownSpellList.Pointer;
+
+ for (int i = 0; i < spells.Length; i++)
+ {
+ CExoArrayListUInt32 spellList = CExoArrayListUInt32.FromPointer(ptr + i * KnownSpellArrayListStructSize);
+ spells[i] = new ListWrapper(spellList, spellId => NwSpell.FromSpellId((int)spellId)!, spell => (uint)spell.Id);
+ }
+
+ return spells;
+ }
+ }
+
///
/// Gets or sets the hitpoints gained by this creature for this level.
///
diff --git a/NWN.Anvil/src/main/API/Objects/NwObject.cs b/NWN.Anvil/src/main/API/Objects/NwObject.cs
index 5ad68d64b..255b942af 100644
--- a/NWN.Anvil/src/main/API/Objects/NwObject.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwObject.cs
@@ -295,17 +295,23 @@ public bool IsEventLocked(EventScriptType eventType)
///
/// The event to be assigned.
/// The new script to assign to this event.
- /// Thrown if this event is locked as a service has subscribed to this event. See to determine if an event script can be changed.
- public void SetEventScript(EventScriptType eventType, string script)
+ /// Thrown if setting the event script failed. This can be from an invalid event script type, or this event is locked as a service has subscribed to this event. See to determine if an event script can be changed.
+ /// Thrown if the specified script name is invalid.
+ public void SetEventScript(EventScriptType eventType, string? script)
{
if (IsEventLocked(eventType))
{
throw new InvalidOperationException("The specified event has already been subscribed by an event handler and cannot be modified.");
}
- if (script.IsValidScriptName())
+ if (!script.IsValidScriptName(true))
{
- NWScript.SetEventScript(this, (int)eventType, script);
+ throw new ArgumentOutOfRangeException(nameof(script), $"The specified script name '{script}' is invalid.");
+ }
+
+ if (!NWScript.SetEventScript(this, (int)eventType, script!).ToBool())
+ {
+ throw new InvalidOperationException("The event script failed to apply. Are you using the correct script type?");
}
}
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs
new file mode 100644
index 000000000..368c622a5
--- /dev/null
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs
@@ -0,0 +1,32 @@
+namespace Anvil.API
+{
+ ///
+ /// An effect icon table entry (effecticons.2da)
+ ///
+ public sealed class EffectIconTableEntry : ITwoDimArrayEntry
+ {
+ public int RowIndex { get; init; }
+
+ ///
+ /// Gets the developer label for this icon entry.
+ ///
+ public string? Label { get; private set; }
+
+ ///
+ /// Gets the localised string reference for this icon.
+ ///
+ public StrRef? StrRef { get; private set; }
+
+ ///
+ /// Gets the ResRef for this effect icon.
+ ///
+ public string? Icon { get; private set; }
+
+ public void InterpretEntry(TwoDimArrayEntry entry)
+ {
+ Label = entry.GetString("Label");
+ Icon = entry.GetString("Icon");
+ StrRef = entry.GetStrRef("StrRef");
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyItemMapTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyItemMapTableEntry.cs
index 3789a4331..a1ad6e66e 100644
--- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyItemMapTableEntry.cs
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyItemMapTableEntry.cs
@@ -55,7 +55,7 @@ private IReadOnlyDictionary CalculateValidItemsForProperty(Two
for (int i = 0; i < entry.Columns.Length; i++)
{
string column = entry.Columns[i];
- if (column is not "StringRef" or "Label")
+ if (column != "StringRef" && column != "Label")
{
isValid[i] = entry.GetBool(i) == true;
}
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs
index ea42163ce..3610982f3 100644
--- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs
@@ -100,6 +100,7 @@ private void LoadTables()
AppearanceTable = GetTable(arrays.m_pAppearanceTable);
ArmorTable = GetTable(arrays.m_pArmorTable);
BodyBagTable = GetTable(arrays.m_pBodyBagTable);
+ EffectIconTable = GetTable("effecticons.2da");
EnvironmentPresetTable = GetTable("environment.2da");
LightColorTable = GetTable(arrays.m_pLightColorTable);
LoadScreenTable = GetTable("loadscreens.2da");
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
index 2d26a10a9..9cea01fe7 100644
--- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
@@ -18,10 +18,15 @@ public static partial class NwGameTables
public static TwoDimArray BodyBagTable { get; private set; } = null!;
///
- /// Gets the damage level table (damagelevels.2da
+ /// Gets the damage level table (damagelevels.2da)
///
public static TwoDimArray DamageLevelTable { get; private set; } = null!;
+ ///
+ /// Gets the effect icon table (effecticons.2da)
+ ///
+ public static TwoDimArray EffectIconTable { get; private set; } = null!;
+
///
/// Gets the environment preset table (environment.2da)
///
diff --git a/NWN.Anvil/src/main/AnvilCore.cs b/NWN.Anvil/src/main/AnvilCore.cs
index e0d321d1f..3cbaffa17 100644
--- a/NWN.Anvil/src/main/AnvilCore.cs
+++ b/NWN.Anvil/src/main/AnvilCore.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -63,6 +64,7 @@ public static int Init(IntPtr arg, int argLength, IServiceManager? serviceManage
RunScript = instance.VirtualMachineFunctionHandler.OnRunScript,
Closure = instance.VirtualMachineFunctionHandler.OnClosure,
MainLoop = instance.VirtualMachineFunctionHandler.OnLoop,
+ AssertFail = instance.OnAssertFail,
};
return NWNCore.Init(arg, argLength, instance.VirtualMachineFunctionHandler, eventHandles);
@@ -154,6 +156,14 @@ private void OnNWNXSignal(string signal)
}
}
+ private void OnAssertFail(string message, string nativeStackTrace)
+ {
+ StackTrace stackTrace = new StackTrace(true);
+ Log.Error("An assertion failure occurred in native code.\n" +
+ $"{message}{nativeStackTrace}\n" +
+ $"{stackTrace}");
+ }
+
private void PrelinkNative()
{
if (!EnvironmentConfig.NativePrelinkEnabled)
diff --git a/NWN.Anvil/src/main/Native/ListWrapper.cs b/NWN.Anvil/src/main/Native/ListWrapper.cs
index 65b20ab48..b9ee59fc0 100644
--- a/NWN.Anvil/src/main/Native/ListWrapper.cs
+++ b/NWN.Anvil/src/main/Native/ListWrapper.cs
@@ -52,17 +52,31 @@ public bool Contains(T2 item)
public void CopyTo(T2[] array, int arrayIndex)
{
- T1[] values = new T1[array.Length];
- for (int i = 0; i < array.Length; i++)
+ if (array == null)
{
- values[i] = set(array[i]);
+ throw new NullReferenceException("array is null");
+ }
+
+ if (arrayIndex < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(arrayIndex), "arrayIndex is less than 0.");
+ }
+
+ if (array.Length < Count - arrayIndex)
+ {
+ throw new ArgumentException("Copy would exceed size of target array.");
+ }
+
+ for (int i = arrayIndex; i < Count; i++)
+ {
+ array[i] = get(list[i]);
}
}
public bool Remove(T2 item)
{
T1 value = set(item);
- return list.Contains(value);
+ return list.Remove(value);
}
public int IndexOf(T2 item)
diff --git a/NWN.Anvil/src/main/Services/ScriptDispatch/AttributeScriptDispatchService.cs b/NWN.Anvil/src/main/Services/ScriptDispatch/AttributeScriptDispatchService.cs
index df3a4fb05..5b8096102 100644
--- a/NWN.Anvil/src/main/Services/ScriptDispatch/AttributeScriptDispatchService.cs
+++ b/NWN.Anvil/src/main/Services/ScriptDispatch/AttributeScriptDispatchService.cs
@@ -42,7 +42,7 @@ ScriptHandleResult IScriptDispatcher.ExecuteScript(string script, uint objectSel
private void RegisterMethod(object service, MethodInfo method, string scriptName)
{
- if (!scriptName.IsValidScriptName())
+ if (!scriptName.IsValidScriptName(false))
{
Log.Warn("Script Handler {ScriptName} - name exceeds character limit ({MaxScriptSize}) and will be ignored\n" +
"Method: {Method}",
diff --git a/NWN.Anvil/src/main/Services/ScriptDispatch/ScriptHandleFactory.cs b/NWN.Anvil/src/main/Services/ScriptDispatch/ScriptHandleFactory.cs
index 2b66ada9d..049fd467e 100644
--- a/NWN.Anvil/src/main/Services/ScriptDispatch/ScriptHandleFactory.cs
+++ b/NWN.Anvil/src/main/Services/ScriptDispatch/ScriptHandleFactory.cs
@@ -54,7 +54,7 @@ public bool IsScriptRegistered(string scriptName)
/// Thrown if the specified script already has a handler defined.
public ScriptCallbackHandle RegisterScriptHandler(string scriptName, Func callback)
{
- if (!scriptName.IsValidScriptName())
+ if (!scriptName.IsValidScriptName(false))
{
throw new ArgumentException("The specified script name is not valid.", scriptName);
}
diff --git a/README.md b/README.md
index 34bd70024..baee02210 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Builders can add functionality like opening a store from a dialogue with a few l
- [Changelog](https://github.com/nwn-dotnet/Anvil/blob/main/CHANGELOG.md) ([Development](https://github.com/nwn-dotnet/Anvil/blob/development/CHANGELOG.md))
- View the [API Reference](https://nwn-dotnet.github.io/Anvil/annotated.html)
- View [Community Submitted Plugins](https://github.com/nwn-dotnet/Anvil/discussions/categories/plugins)
-- Join the community: [![Discord](https://img.shields.io/discord/714927668826472600?color=7289DA&label=Discord&logo=discord&logoColor=7289DA)](https://discord.gg/gKt495UBgS)
+- Join the community: [![Discord](https://img.shields.io/discord/382306806866771978?color=7289DA&label=Discord&logo=discord&logoColor=7289DA)](https://discord.gg/CukSHZq)
## Getting Started
@@ -35,7 +35,7 @@ To run tests locally, install the plugins in the anvil home directory as documen
## Contributions
All contributions are welcome!
-Join the discussion on [Discord](https://discord.gg/gKt495UBgS), and also see our [contribution guidelines](https://github.com/nwn-dotnet/Anvil/blob/development/CONTRIBUTING.md).
+Join the discussion on the NWN Developer Discord [Discord](https://discord.gg/CukSHZq), and also see our [contribution guidelines](https://github.com/nwn-dotnet/Anvil/blob/development/CONTRIBUTING.md).
## Credits
The Anvil Framework builds heavily on the foundations of the [NWNX:EE DotNET plugin](https://github.com/nwnxee/unified/tree/master/Plugins/DotNET) that was written by [Milos Tijanic](https://github.com/mtijanic "Milos Tijanic"), and derives several service implementations from plugins developed by the NWNX:EE team and its contributors.
diff --git a/docs/NWN.Anvil.Samples.csproj b/docs/NWN.Anvil.Samples.csproj
index c16c7c734..05ea15e09 100644
--- a/docs/NWN.Anvil.Samples.csproj
+++ b/docs/NWN.Anvil.Samples.csproj
@@ -30,7 +30,7 @@
-
+