diff --git a/CHANGELOG.md b/CHANGELOG.md index 221fc06d3..04b594fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,22 @@ 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.27 +https://github.com/nwn-dotnet/Anvil/compare/v8193.34.26...v8193.34.27 + +### Added +- NwCreature: Added `BodyBag`, `BodyBagTemplate` properties. +- NwPlaceable: Added `IsBodyBag` property. + +### Changed +- OnPlayerGuiEvent: `EffectIcon` property is now nullable. +- OnDebugPlayVisualEffect: `Effect` property is now nullable. +- APIs that accept a `TableEntry` parameter now have implicit casts (e.g. `EffectIconTableEntry` & `EffectIcon`). + +### Fixed +- NwCreature: Fixed an infinite loop caused by the `Associates` property when having dominated creature associates. +- Added some index/range checks for some usages of game table data. + ## 8193.34.26 https://github.com/nwn-dotnet/Anvil/compare/v8193.34.25...v8193.34.26 diff --git a/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs b/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs index 141263e59..6ed0d6810 100644 --- a/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs +++ b/NWN.Anvil/src/main/API/EngineStructures/ItemProperty.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using NWN.Core; using NWN.Native.API; @@ -17,19 +18,7 @@ internal ItemProperty(CGameEffect effect, bool memoryOwn) : base(effect, memoryO /// <summary> /// Gets the cost table used by this item property. /// </summary> - public TwoDimArray<ItemPropertyCostTableEntry>? CostTable - { - get - { - int tableIndex = Effect.GetInteger(2); - if (tableIndex >= 0 && tableIndex < NwGameTables.ItemPropertyCostTables.Count) - { - return NwGameTables.ItemPropertyCostTables[tableIndex].Table; - } - - return null; - } - } + public TwoDimArray<ItemPropertyCostTableEntry>? CostTable => NwGameTables.ItemPropertyCostTables.ElementAtOrDefault(Effect.GetInteger(2))?.Table; /// <summary> /// Gets or sets the cost table entry that is set for this item property.<br/> @@ -62,19 +51,7 @@ public EffectDuration DurationType /// <summary> /// Gets the #1 param table used by this item property. /// </summary> - public TwoDimArray<ItemPropertyParamTableEntry>? Param1Table - { - get - { - int tableIndex = Effect.GetInteger(4); - if (tableIndex >= 0 && tableIndex < NwGameTables.ItemPropertyParamTables.Count) - { - return NwGameTables.ItemPropertyParamTables[tableIndex].Table; - } - - return null; - } - } + public TwoDimArray<ItemPropertyParamTableEntry>? Param1Table => NwGameTables.ItemPropertyParamTables.ElementAtOrDefault(Effect.GetInteger(4))?.Table; /// <summary> /// Gets or sets the #1 param table entry that is set for this item property.<br/> @@ -119,18 +96,7 @@ public ItemPropertyParamTableEntry? Param1TableValue /// </summary> public ItemPropertySubTypeTableEntry? SubType { - get - { - int tableIndex = Effect.GetInteger(1); - TwoDimArray<ItemPropertySubTypeTableEntry>? table = Property.SubTypeTable; - - if (tableIndex >= 0 && table != null && tableIndex < table.Count) - { - return table[tableIndex]; - } - - return null; - } + get => Property.SubTypeTable?.ElementAtOrDefault(Effect.GetInteger(1)); set => Effect.SetInteger(1, value?.RowIndex ?? -1); } 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 569e2fb6c..cb69de62b 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 @@ -1,4 +1,5 @@ using System; +using System.Linq; using Anvil.API.Events; using NWN.Core; @@ -25,7 +26,7 @@ public sealed class OnPlayerGuiEvent : IEvent /// <summary> /// Gets the effect icon that was selected. Only valid in <see cref="GuiEventType.EffectIconClick"/> events. /// </summary> - public EffectIconTableEntry EffectIcon => NwGameTables.EffectIconTable[integerEventData]; + public EffectIconTableEntry? EffectIcon => NwGameTables.EffectIconTable.ElementAtOrDefault(integerEventData); /// <summary> /// Gets the object data associated with this GUI event. diff --git a/NWN.Anvil/src/main/API/Events/Native/DebugEvents/DebugEventFactory.cs b/NWN.Anvil/src/main/API/Events/Native/DebugEvents/DebugEventFactory.cs index f189f8a0a..6cc53268c 100644 --- a/NWN.Anvil/src/main/API/Events/Native/DebugEvents/DebugEventFactory.cs +++ b/NWN.Anvil/src/main/API/Events/Native/DebugEvents/DebugEventFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Anvil.Services; @@ -120,7 +121,7 @@ private static OnDebugPlayVisualEffect HandleVisualEffectEvent(CNWSMessage messa { Player = player, TargetObject = target.ToNwObject(), - Effect = NwGameTables.VisualEffectTable[visualEffect], + Effect = NwGameTables.VisualEffectTable.ElementAtOrDefault(visualEffect), Duration = TimeSpan.FromSeconds(duration), TargetPosition = position, }); diff --git a/NWN.Anvil/src/main/API/Events/Native/DebugEvents/OnDebugPlayVisualEffect.cs b/NWN.Anvil/src/main/API/Events/Native/DebugEvents/OnDebugPlayVisualEffect.cs index 90a58e3cd..5cadcb4f5 100644 --- a/NWN.Anvil/src/main/API/Events/Native/DebugEvents/OnDebugPlayVisualEffect.cs +++ b/NWN.Anvil/src/main/API/Events/Native/DebugEvents/OnDebugPlayVisualEffect.cs @@ -12,7 +12,7 @@ public sealed class OnDebugPlayVisualEffect : IEvent /// <summary> /// Gets the effect that is attempting to be played. /// </summary> - public VisualEffectTableEntry Effect { get; internal init; } = null!; + public VisualEffectTableEntry? Effect { get; internal init; } /// <summary> /// Gets the player attempting to spawn the visual effect. diff --git a/NWN.Anvil/src/main/API/Objects/NwCreature.cs b/NWN.Anvil/src/main/API/Objects/NwCreature.cs index d60c20e14..0cdf78bf2 100644 --- a/NWN.Anvil/src/main/API/Objects/NwCreature.cs +++ b/NWN.Anvil/src/main/API/Objects/NwCreature.cs @@ -52,7 +52,7 @@ internal CNWSCreature Creature internal NwCreature(CNWSCreature creature) : base(creature) { this.creature = creature; - faction = new NwFaction(creature.GetFaction()); + faction = new NwFaction(Creature.GetFaction()); Inventory = new Inventory(this, Creature.m_pcItemRepository); } @@ -197,6 +197,20 @@ public byte BaseShieldArcaneSpellFailure set => Creature.m_pStats.m_nBaseShieldArcaneSpellFailure = value; } + /// <summary> + /// Gets the body bag assigned to this creature as a result of its death. + /// </summary> + public NwPlaceable? BodyBag => Creature.m_oidBodyBag.ToNwObject<NwPlaceable>(); + + /// <summary> + /// Gets or sets the body bag template to use when this creature dies. + /// </summary> + public BodyBagTableEntry BodyBagTemplate + { + get => NwGameTables.BodyBagTable[Creature.m_nBodyBag]; + set => Creature.m_nBodyBag = (byte)value.RowIndex; + } + /// <summary> /// Gets or sets the calculated challenge rating for this creature. /// </summary> @@ -2529,16 +2543,23 @@ private async Task DoActionUnlockObject(NwGameObject target) NWScript.ActionUnlockObject(target); } - private IEnumerable<NwCreature> GetAssociates(AssociateType associateType) + private List<NwCreature> GetAssociates(AssociateType associateType) { - int i; - uint current; + List<NwCreature> associates = new List<NwCreature>(); int type = (int)associateType; - for (i = 1, current = NWScript.GetAssociate(type, this, i); current != Invalid; i++, current = NWScript.GetAssociate(type, this, i)) + for (int i = 0;; i++) { - yield return current.ToNwObject<NwCreature>()!; + NwCreature? associate = NWScript.GetAssociate(type, this, i).ToNwObject<NwCreature>(); + if (associate == null || associates.Contains(associate)) + { + break; + } + + associates.Add(associate); } + + return associates; } private PlayerQuickBarButton InternalGetQuickBarButton(byte index) diff --git a/NWN.Anvil/src/main/API/Objects/NwPlaceable.cs b/NWN.Anvil/src/main/API/Objects/NwPlaceable.cs index f3eeb7c59..1227b1813 100644 --- a/NWN.Anvil/src/main/API/Objects/NwPlaceable.cs +++ b/NWN.Anvil/src/main/API/Objects/NwPlaceable.cs @@ -70,6 +70,11 @@ public bool Illumination /// </summary> public Inventory Inventory { get; } + /// <summary> + /// Gets if this is a body bag placeable, spawned from a creature's death. + /// </summary> + public bool IsBodyBag => placeable.m_bIsBodyBag.ToBool(); + public bool IsStatic { get => Placeable.m_bStaticObject.ToBool(); diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/AppearanceTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/AppearanceTableEntry.cs index 989a7c020..8c72c08d3 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/AppearanceTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/AppearanceTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { /// <summary> @@ -115,5 +117,10 @@ void ITwoDimArrayEntry.InterpretEntry(TwoDimArrayEntry entry) BodyBag = entry.GetTableEntry("BODY_BAG", NwGameTables.BodyBagTable); Targetable = entry.GetBool("TARGETABLE"); } + + public static implicit operator AppearanceTableEntry?(AppearanceType appearanceType) + { + return NwGameTables.AppearanceTable.ElementAtOrDefault((int)appearanceType); + } } } diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs index 368c622a5..43b49bb61 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/EffectIconTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { /// <summary> @@ -28,5 +30,10 @@ public void InterpretEntry(TwoDimArrayEntry entry) Icon = entry.GetString("Icon"); StrRef = entry.GetStrRef("StrRef"); } + + public static implicit operator EffectIconTableEntry?(EffectIcon effectIcon) + { + return NwGameTables.EffectIconTable.ElementAtOrDefault((int)effectIcon); + } } } diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyTableEntry.cs index 186de59a5..613eb8ae1 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ItemPropertyTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { public sealed class ItemPropertyTableEntry : ITwoDimArrayEntry @@ -53,7 +55,7 @@ public sealed class ItemPropertyTableEntry : ITwoDimArrayEntry void ITwoDimArrayEntry.InterpretEntry(TwoDimArrayEntry entry) { - ItemMap = NwGameTables.ItemPropertyItemMapTable.GetRow(RowIndex); + ItemMap = NwGameTables.ItemPropertyItemMapTable.ElementAtOrDefault(RowIndex); Name = entry.GetStrRef("Name"); Label = entry.GetString("Label"); SubTypeTable = entry.GetTable<ItemPropertySubTypeTableEntry>("SubTypeResRef"); @@ -63,5 +65,10 @@ void ITwoDimArrayEntry.InterpretEntry(TwoDimArrayEntry entry) GameStrRef = entry.GetStrRef("GameStrRef"); Description = entry.GetStrRef("Description"); } + + public static implicit operator ItemPropertyTableEntry?(ItemPropertyType propertyType) + { + return NwGameTables.ItemPropertyTable.ElementAtOrDefault((int)propertyType); + } } } diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/PartsTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/PartsTableEntry.cs index 6c325f538..b61aee853 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/PartsTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/PartsTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { public sealed class PartsTableEntry : ITwoDimArrayEntry @@ -14,7 +16,7 @@ public void InterpretEntry(TwoDimArrayEntry entry) { CostModifier = entry.GetInt("COSTMODIFIER"); ACBonus = entry.GetFloat("ACBONUS"); - ArmorTableEntry = ACBonus.HasValue ? NwGameTables.ArmorTable[(int)ACBonus.Value] : null; + ArmorTableEntry = ACBonus.HasValue ? NwGameTables.ArmorTable.ElementAtOrDefault((int)ACBonus.Value) : null; } } } diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ProgrammedEffectTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ProgrammedEffectTableEntry.cs index f7778a69d..dea174ca7 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/ProgrammedEffectTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/ProgrammedEffectTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { public sealed class ProgrammedEffectTableEntry : ITwoDimArrayEntry @@ -43,5 +45,10 @@ public void InterpretEntry(TwoDimArrayEntry entry) intParamValues[i] = entry.GetInt(columnName); } } + + public static implicit operator ProgrammedEffectTableEntry?(ProgFxType fxType) + { + return NwGameTables.ProgrammedEffectTable.ElementAtOrDefault((int)fxType); + } } } diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/VisualEffectTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/VisualEffectTableEntry.cs index bb2ea81a8..49e79fd42 100644 --- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/VisualEffectTableEntry.cs +++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/VisualEffectTableEntry.cs @@ -1,3 +1,5 @@ +using System.Linq; + namespace Anvil.API { public sealed class VisualEffectTableEntry : ITwoDimArrayEntry @@ -88,5 +90,10 @@ void ITwoDimArrayEntry.InterpretEntry(TwoDimArrayEntry entry) LowQualityVariant = entry.GetString("LowQuality"); OrientWithObject = entry.GetBool("OrientWithObject"); } + + public static implicit operator VisualEffectTableEntry?(VfxType vfxType) + { + return NwGameTables.VisualEffectTable.ElementAtOrDefault((int)vfxType); + } } } diff --git a/NWN.Anvil/src/main/Services/API/Creature/DamageLevelOverrideService.cs b/NWN.Anvil/src/main/Services/API/Creature/DamageLevelOverrideService.cs index 5c02edcdb..713b8fdf1 100644 --- a/NWN.Anvil/src/main/Services/API/Creature/DamageLevelOverrideService.cs +++ b/NWN.Anvil/src/main/Services/API/Creature/DamageLevelOverrideService.cs @@ -1,3 +1,4 @@ +using System.Linq; using Anvil.API; using NLog; using NWN.Native.API; @@ -36,7 +37,7 @@ public void ClearDamageLevelOverride(NwCreature creature) InternalVariableInt damageLevelOverride = InternalVariables.DamageLevelOverride(creature); if (damageLevelOverride.HasValue) { - return NwGameTables.DamageLevelTable[damageLevelOverride.Value]; + return NwGameTables.DamageLevelTable.ElementAtOrDefault(damageLevelOverride.Value); } return null;