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 @@ - +