From 0cbe6931bb12352e68e092b81aa30f51ad1bc9f5 Mon Sep 17 00:00:00 2001
From: Jhett Black <10942655+jhett12321@users.noreply.github.com>
Date: Sat, 17 Feb 2024 17:23:07 +0100
Subject: [PATCH] Implement 8193.36 new script functions/APIs (#753)
* Bump NWNX/NWN.Core/NWN.Native versions.
* Update constants.
* Add item possessor tests.
* Use new GetItemPossessor APIs for root/non-root item possessor.
* Add placeable version of possessor test.
* Implement 8193.36 new functions/API.
* Add missing surface material table.
* Bump NWNX version.
* Formatting fixes.
---
.env | 2 +-
.../NWN.Anvil.TestRunner.csproj | 4 +-
NWN.Anvil.Tests/NWN.Anvil.Tests.csproj | 4 +-
.../src/main/API/Objects/NwItemTests.cs | 84 +++++++++++++++++++
NWN.Anvil/NWN.Anvil.csproj | 4 +-
.../API/Constants/AudioStreamIdentifier.cs | 18 ++++
.../src/main/API/Constants/EffectType.cs | 12 +++
.../src/main/API/Constants/GuiEventType.cs | 2 +
.../API/Constants/PlayerDeviceProperty.cs | 3 +
.../src/main/API/Constants/ResRefType.cs | 7 ++
.../API/EngineStructures/Effect.Create.cs | 19 ++++-
.../src/main/API/EngineStructures/Effect.cs | 2 +-
.../src/main/API/EngineStructures/SQLQuery.cs | 17 ++++
.../main/API/EngineStructures/SQLResult.cs | 53 ++++++++++++
.../ModuleEvents.OnPlayerGuiEvent.cs | 11 ++-
.../main/API/Objects/CreatureSpellAbility.cs | 42 ++++++++++
NWN.Anvil/src/main/API/Objects/NwArea.cs | 44 ++++++++++
NWN.Anvil/src/main/API/Objects/NwCreature.cs | 19 +++--
NWN.Anvil/src/main/API/Objects/NwDoor.cs | 2 +-
NWN.Anvil/src/main/API/Objects/NwEncounter.cs | 2 +-
.../src/main/API/Objects/NwGameObject.cs | 28 +++++--
NWN.Anvil/src/main/API/Objects/NwItem.cs | 16 +---
NWN.Anvil/src/main/API/Objects/NwPlayer.cs | 67 ++++++++++++++-
.../Tables/NwGameTables.Factory.cs | 1 +
.../API/TwoDimArray/Tables/NwGameTables.cs | 5 ++
.../Tables/SurfaceMaterialTableEntry.cs | 71 ++++++++++++++++
.../Campaign/CampaignVariableObject.cs | 2 +-
docs/NWN.Anvil.Samples.csproj | 2 +-
28 files changed, 496 insertions(+), 47 deletions(-)
create mode 100644 NWN.Anvil/src/main/API/Constants/AudioStreamIdentifier.cs
create mode 100644 NWN.Anvil/src/main/API/Objects/CreatureSpellAbility.cs
create mode 100644 NWN.Anvil/src/main/API/TwoDimArray/Tables/SurfaceMaterialTableEntry.cs
diff --git a/.env b/.env
index c782be1a2..ddc351438 100644
--- a/.env
+++ b/.env
@@ -1 +1 @@
-NWNX_VERSION=0e77a56
+NWNX_VERSION=9865013
diff --git a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
index 0be8471b8..5ca94ba52 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.Tests/NWN.Anvil.Tests.csproj b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj
index ca71c8b66..09e7ea6ff 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/NwItemTests.cs b/NWN.Anvil.Tests/src/main/API/Objects/NwItemTests.cs
index 2a5087b1e..d726aa7c1 100644
--- a/NWN.Anvil.Tests/src/main/API/Objects/NwItemTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/Objects/NwItemTests.cs
@@ -419,6 +419,90 @@ public void SerializeItemAppearancePreservesAppearance(string itemResRef)
Assert.That(item.Appearance.GetSimpleModel(), Is.EqualTo(model));
}
+ [Test(Description = "An item stored in an item container returns the correct possessing object.")]
+ public void ItemInContainerOnGroundReturnsCorrectPossessor()
+ {
+ Location startLocation = NwModule.Instance.StartingLocation;
+ NwItem? container = NwItem.Create(StandardResRef.Item.nw_it_contain006, startLocation);
+ NwItem? item = NwItem.Create(StandardResRef.Item.nw_it_gem013, startLocation);
+
+ Assert.That(container, Is.Not.Null, "Item nw_it_contain006 was null after creation.");
+ Assert.That(container!.IsValid, Is.True, "Item nw_it_contain006 was invalid after creation.");
+
+ createdTestObjects.Add(container);
+
+ Assert.That(item, Is.Not.Null, "Item nw_it_gem013 was null after creation.");
+ Assert.That(item!.IsValid, Is.True, "Item nw_it_gem013 was invalid after creation.");
+
+ createdTestObjects.Add(item);
+
+ container.AcquireItem(item);
+
+ Assert.That(item.Possessor, Is.EqualTo(container), "Item possessor does not match expected container.");
+ Assert.That(item.RootPossessor, Is.Null, "Root possessor is not null.");
+ }
+
+ [Test(Description = "An item stored in an item container on a creature returns the correct possessing object.")]
+ public void ItemInContainerOnCreatureReturnsCorrectPossessor()
+ {
+ Location startLocation = NwModule.Instance.StartingLocation;
+ NwItem? container = NwItem.Create(StandardResRef.Item.nw_it_contain006, startLocation);
+ NwItem? item = NwItem.Create(StandardResRef.Item.nw_it_gem013, startLocation);
+ NwCreature? creature = NwCreature.Create(StandardResRef.Creature.nw_bandit001, startLocation);
+
+ Assert.That(container, Is.Not.Null, "Item nw_it_contain006 was null after creation.");
+ Assert.That(container!.IsValid, Is.True, "Item nw_it_contain006 was invalid after creation.");
+
+ createdTestObjects.Add(container);
+
+ Assert.That(item, Is.Not.Null, "Item nw_it_gem013 was null after creation.");
+ Assert.That(item!.IsValid, Is.True, "Item nw_it_gem013 was invalid after creation.");
+
+ createdTestObjects.Add(item);
+
+ Assert.That(creature, Is.Not.Null, "Creature nw_bandit001 was null after creation.");
+ Assert.That(creature!.IsValid, Is.True, "Creature nw_bandit001 was invalid after creation.");
+
+ createdTestObjects.Add(creature);
+
+ container.AcquireItem(item);
+ creature.AcquireItem(container);
+
+ Assert.That(item.Possessor, Is.EqualTo(container), "Item possessor does not match expected container.");
+ Assert.That(item.RootPossessor, Is.EqualTo(creature), "Root possessor is not the expected creature.");
+ }
+
+ [Test(Description = "An item stored in an item container on a placeable returns the correct possessing object.")]
+ public void ItemInContainerOnPlaceableReturnsCorrectPossessor()
+ {
+ Location startLocation = NwModule.Instance.StartingLocation;
+ NwItem? container = NwItem.Create(StandardResRef.Item.nw_it_contain006, startLocation);
+ NwItem? item = NwItem.Create(StandardResRef.Item.nw_it_gem013, startLocation);
+ NwPlaceable? placeable = NwPlaceable.Create(StandardResRef.Placeable.plc_chest1, startLocation);
+
+ Assert.That(container, Is.Not.Null, "Item nw_it_contain006 was null after creation.");
+ Assert.That(container!.IsValid, Is.True, "Item nw_it_contain006 was invalid after creation.");
+
+ createdTestObjects.Add(container);
+
+ Assert.That(item, Is.Not.Null, "Item nw_it_gem013 was null after creation.");
+ Assert.That(item!.IsValid, Is.True, "Item nw_it_gem013 was invalid after creation.");
+
+ createdTestObjects.Add(item);
+
+ Assert.That(placeable, Is.Not.Null, "Placeable plc_chest1 was null after creation.");
+ Assert.That(placeable!.IsValid, Is.True, "Placeable plc_chest1 was invalid after creation.");
+
+ createdTestObjects.Add(placeable);
+ placeable.HasInventory = true;
+
+ container.AcquireItem(item);
+ placeable.AcquireItem(container);
+
+ Assert.That(item.Possessor, Is.EqualTo(container), "Item possessor does not match expected container.");
+ Assert.That(item.RootPossessor, Is.EqualTo(placeable), "Root possessor is not the expected placeable.");
+ }
+
[TearDown]
public void CleanupTestObjects()
{
diff --git a/NWN.Anvil/NWN.Anvil.csproj b/NWN.Anvil/NWN.Anvil.csproj
index 3347be2fa..18326def7 100644
--- a/NWN.Anvil/NWN.Anvil.csproj
+++ b/NWN.Anvil/NWN.Anvil.csproj
@@ -65,8 +65,8 @@
-
-
+
+
diff --git a/NWN.Anvil/src/main/API/Constants/AudioStreamIdentifier.cs b/NWN.Anvil/src/main/API/Constants/AudioStreamIdentifier.cs
new file mode 100644
index 000000000..431f09a68
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Constants/AudioStreamIdentifier.cs
@@ -0,0 +1,18 @@
+using NWN.Core;
+
+namespace Anvil.API
+{
+ public enum AudioStreamIdentifier
+ {
+ Identifier0 = NWScript.AUDIOSTREAM_IDENTIFIER_0,
+ Identifier1 = NWScript.AUDIOSTREAM_IDENTIFIER_1,
+ Identifier2 = NWScript.AUDIOSTREAM_IDENTIFIER_2,
+ Identifier3 = NWScript.AUDIOSTREAM_IDENTIFIER_3,
+ Identifier4 = NWScript.AUDIOSTREAM_IDENTIFIER_4,
+ Identifier5 = NWScript.AUDIOSTREAM_IDENTIFIER_5,
+ Identifier6 = NWScript.AUDIOSTREAM_IDENTIFIER_6,
+ Identifier7 = NWScript.AUDIOSTREAM_IDENTIFIER_7,
+ Identifier8 = NWScript.AUDIOSTREAM_IDENTIFIER_8,
+ Identifier9 = NWScript.AUDIOSTREAM_IDENTIFIER_9,
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Constants/EffectType.cs b/NWN.Anvil/src/main/API/Constants/EffectType.cs
index 7d285c858..56f14c99e 100644
--- a/NWN.Anvil/src/main/API/Constants/EffectType.cs
+++ b/NWN.Anvil/src/main/API/Constants/EffectType.cs
@@ -86,5 +86,17 @@ public enum EffectType
BonusFeat = NWScript.EFFECT_TYPE_BONUS_FEAT,
TimeStopImmunity = NWScript.EFFECT_TYPE_TIMESTOP_IMMUNITY,
ForceWalk = NWScript.EFFECT_TYPE_FORCE_WALK,
+ Appear = NWScript.EFFECT_TYPE_APPEAR,
+ CutsceneDominated = NWScript.EFFECT_TYPE_CUTSCENE_DOMINATED,
+ Damage = NWScript.EFFECT_TYPE_DAMAGE,
+ Death = NWScript.EFFECT_TYPE_DEATH,
+ Disappear = NWScript.EFFECT_TYPE_DISAPPEAR,
+ Heal = NWScript.EFFECT_TYPE_HEAL,
+ HitpointChangeWhenDying = NWScript.EFFECT_TYPE_HITPOINTCHANGEWHENDYING,
+ Knockdown = NWScript.EFFECT_TYPE_KNOCKDOWN,
+ ModifyAttacks = NWScript.EFFECT_TYPE_MODIFY_ATTACKS,
+ SummonCreature = NWScript.EFFECT_TYPE_SUMMON_CREATURE,
+ Taunt = NWScript.EFFECT_TYPE_TAUNT,
+ Wounding = NWScript.EFFECT_TYPE_WOUNDING,
}
}
diff --git a/NWN.Anvil/src/main/API/Constants/GuiEventType.cs b/NWN.Anvil/src/main/API/Constants/GuiEventType.cs
index 667557ab4..d6b511955 100644
--- a/NWN.Anvil/src/main/API/Constants/GuiEventType.cs
+++ b/NWN.Anvil/src/main/API/Constants/GuiEventType.cs
@@ -30,5 +30,7 @@ public enum GuiEventType
OptionsOpen = NWScript.GUIEVENT_OPTIONS_OPEN,
OptionsClose = NWScript.GUIEVENT_OPTIONS_CLOSE,
RadialOpen = NWScript.GUIEVENT_RADIAL_OPEN,
+ ChatlogPortraitClick = NWScript.GUIEVENT_CHATLOG_PORTRAIT_CLICK,
+ PlayerlistPlayerTell = NWScript.GUIEVENT_PLAYERLIST_PLAYER_TELL,
}
}
diff --git a/NWN.Anvil/src/main/API/Constants/PlayerDeviceProperty.cs b/NWN.Anvil/src/main/API/Constants/PlayerDeviceProperty.cs
index 92cf86795..64f70a20b 100644
--- a/NWN.Anvil/src/main/API/Constants/PlayerDeviceProperty.cs
+++ b/NWN.Anvil/src/main/API/Constants/PlayerDeviceProperty.cs
@@ -61,6 +61,9 @@ public sealed class PlayerDeviceProperty
public static readonly PlayerDeviceProperty UiSpellbookSortSpells = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_SPELLBOOK_SORT_SPELLS);
public static readonly PlayerDeviceProperty UiRadialSpellcastingAlwaysSubradial = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_RADIAL_SPELLCASTING_ALWAYS_SUBRADIAL);
public static readonly PlayerDeviceProperty UiRadialClassAbilitiesAlwaysSubradial = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_RADIAL_CLASS_ABILITIES_ALWAYS_SUBRADIAL);
+ public static readonly PlayerDeviceProperty UiDisplayLoadscreenHintsInChatlog = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_DISPLAY_LOADSCREEN_HINTS_IN_CHATLOG);
+ public static readonly PlayerDeviceProperty UiMouseScale = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_MOUSE_SCALE);
+ public static readonly PlayerDeviceProperty UiMouseScaleValue = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_UI_MOUSE_SCALE_VALUE);
public static readonly PlayerDeviceProperty CameraMode = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_CAMERA_MODE);
public static readonly PlayerDeviceProperty CameraEdgeTurning = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_CAMERA_EDGE_TURNING);
public static readonly PlayerDeviceProperty CameraDialogZoom = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_CAMERA_DIALOG_ZOOM);
diff --git a/NWN.Anvil/src/main/API/Constants/ResRefType.cs b/NWN.Anvil/src/main/API/Constants/ResRefType.cs
index 4ab4fe8de..52ac90c4b 100644
--- a/NWN.Anvil/src/main/API/Constants/ResRefType.cs
+++ b/NWN.Anvil/src/main/API/Constants/ResRefType.cs
@@ -1,3 +1,5 @@
+// ReSharper disable InconsistentNaming
+
namespace Anvil.API
{
public enum ResRefType
@@ -84,6 +86,11 @@ public enum ResRefType
TML = NWN.Native.API.ResRefType.TML,
SQ3 = NWN.Native.API.ResRefType.SQ3,
LOD = NWN.Native.API.ResRefType.LOD,
+ GIF = NWN.Native.API.ResRefType.GIF,
+ PNG = NWN.Native.API.ResRefType.PNG,
+ JPG = NWN.Native.API.ResRefType.JPG,
+ CAF = NWN.Native.API.ResRefType.CAF,
+ JUI = NWN.Native.API.ResRefType.JUI,
IDS = NWN.Native.API.ResRefType.IDS,
ERF = NWN.Native.API.ResRefType.ERF,
BIF = NWN.Native.API.ResRefType.BIF,
diff --git a/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs b/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
index 445f6f1aa..fac8dda2f 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/Effect.Create.cs
@@ -250,9 +250,10 @@ public static Effect DamageIncrease(int bonus, DamageType damageType = DamageTyp
/// The damage to remove from each attack.
/// The max enchantment/power bonus of the weapon this effect will resist.
/// The total amount of damage to absorb, before the effect is removed (0 = infinite).
- public static Effect DamageReduction(int amount, DamagePower damagePower, int totalAbsorb = 0)
+ /// Set to true to have this damage reduction effect only apply to ranged attacks.
+ public static Effect DamageReduction(int amount, DamagePower damagePower, int totalAbsorb = 0, bool rangedOnly = false)
{
- return NWScript.EffectDamageReduction(amount, (int)damagePower, totalAbsorb)!;
+ return NWScript.EffectDamageReduction(amount, (int)damagePower, totalAbsorb, rangedOnly.ToInt())!;
}
///
@@ -261,9 +262,10 @@ public static Effect DamageReduction(int amount, DamagePower damagePower, int to
/// The type of damage to resist.
/// The damage to remove from each attack.
/// The total amount of damage to absorb, before the effect is removed (0 = infinite).
- public static Effect DamageResistance(DamageType damageType, int amount, int totalAbsorb = 0)
+ /// Set to true to have this damage resistance effect only apply to ranged attacks.
+ public static Effect DamageResistance(DamageType damageType, int amount, int totalAbsorb = 0, bool rangedOnly = false)
{
- return NWScript.EffectDamageResistance((int)damageType, amount, totalAbsorb)!;
+ return NWScript.EffectDamageResistance((int)damageType, amount, totalAbsorb, rangedOnly.ToInt())!;
}
///
@@ -895,5 +897,14 @@ public static Effect VisualEffect(VisualEffectTableEntry visualEffect, bool miss
{
return NWScript.EffectVisualEffect(visualEffect.RowIndex, missEffect.ToInt(), fScale, vTranslate, vRotate)!;
}
+
+ ///
+ /// Creates an effect that gives a creature with melee/ranged/touched attacks a bonus to hit.
+ ///
+ /// The additional attack bonus.
+ public static Effect EnemyAttackBonus(int bonus)
+ {
+ return NWScript.EffectEnemyAttackBonus(bonus)!;
+ }
}
}
diff --git a/NWN.Anvil/src/main/API/EngineStructures/Effect.cs b/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
index dca88f965..b29af1836 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/Effect.cs
@@ -31,7 +31,7 @@ public EffectDuration DurationType
///
/// Gets the type of this effect.
///
- public EffectType EffectType => (EffectType)NWScript.GetEffectType(this);
+ public EffectType EffectType => (EffectType)NWScript.GetEffectType(this, true.ToInt());
///
/// Gets or sets the subtype of this effect.
diff --git a/NWN.Anvil/src/main/API/EngineStructures/SQLQuery.cs b/NWN.Anvil/src/main/API/EngineStructures/SQLQuery.cs
index cde85c0ab..7ccbb2bb3 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/SQLQuery.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/SQLQuery.cs
@@ -64,6 +64,23 @@ public IEnumerable Results
}
}
+ ///
+ /// Gets the name of the columns declared as a part of this query.
+ ///
+ public string[] Columns
+ {
+ get
+ {
+ string[] columns = new string[NWScript.SqlGetColumnCount(this)];
+ for (int i = 0; i < columns.Length; i++)
+ {
+ columns[i] = NWScript.SqlGetColumnName(this, i);
+ }
+
+ return columns;
+ }
+ }
+
protected override int StructureId => NWScript.ENGINE_STRUCTURE_SQLQUERY;
public static implicit operator SQLQuery(IntPtr intPtr)
diff --git a/NWN.Anvil/src/main/API/EngineStructures/SQLResult.cs b/NWN.Anvil/src/main/API/EngineStructures/SQLResult.cs
index 3ce2e5831..9618c8ffc 100644
--- a/NWN.Anvil/src/main/API/EngineStructures/SQLResult.cs
+++ b/NWN.Anvil/src/main/API/EngineStructures/SQLResult.cs
@@ -1,3 +1,4 @@
+using System;
using System.Numerics;
using NWN.Core;
@@ -25,6 +26,16 @@ public float GetFloat(int columnIndex)
return NWScript.SqlGetFloat(query, columnIndex);
}
+ ///
+ /// Gets the float result for the specified column.
+ ///
+ /// The name of the column to fetch the result.
+ /// The float result. Returns 0.0f on an error.
+ public float GetFloat(string columnName)
+ {
+ return NWScript.SqlGetFloat(query, Array.IndexOf(query.Columns, columnName));
+ }
+
///
/// Gets the int result for the specified column.
///
@@ -35,6 +46,16 @@ public int GetInt(int columnIndex)
return NWScript.SqlGetInt(query, columnIndex);
}
+ ///
+ /// Gets the int result for the specified column.
+ ///
+ /// The name of the column to fetch the result.
+ /// The int result. Returns 0.0f on an error.
+ public float GetInt(string columnName)
+ {
+ return NWScript.SqlGetInt(query, Array.IndexOf(query.Columns, columnName));
+ }
+
///
/// Gets the serialized object result for the specified column, and spawns the object at the specified location and inventory.
///
@@ -47,6 +68,18 @@ public int GetInt(int columnIndex)
return NWScript.SqlGetObject(query, columnIndex, spawnLocation, targetInventory).ToNwObject();
}
+ ///
+ /// Gets the serialized object result for the specified column, and spawns the object at the specified location and inventory.
+ ///
+ /// The name of the column to fetch the result.
+ /// The location to spawn the object.
+ /// (Items only) The target inventory for the item.
+ /// The deserialized object. Returns null on an error.
+ public T? GetObject(string columnName, Location spawnLocation, NwGameObject? targetInventory = null) where T : NwObject
+ {
+ return NWScript.SqlGetObject(query, Array.IndexOf(query.Columns, columnName), spawnLocation, targetInventory).ToNwObject();
+ }
+
///
/// Gets the string result for the specified column.
///
@@ -57,6 +90,16 @@ public string GetString(int columnIndex)
return NWScript.SqlGetString(query, columnIndex);
}
+ ///
+ /// Gets the string result for the specified column.
+ ///
+ /// The name of the column to fetch the result.
+ /// The string result. Returns "" on an error.
+ public string GetString(string columnName)
+ {
+ return NWScript.SqlGetString(query, Array.IndexOf(query.Columns, columnName));
+ }
+
///
/// Gets the result for the specified column.
///
@@ -66,5 +109,15 @@ public Vector3 GetVector3(int columnIndex)
{
return NWScript.SqlGetVector(query, columnIndex);
}
+
+ ///
+ /// Gets the result for the specified column.
+ ///
+ /// The name of the column to fetch the result.
+ /// The result. Returns on an error.
+ public Vector3 GetVector3(string columnName)
+ {
+ return NWScript.SqlGetVector(query, Array.IndexOf(query.Columns, columnName));
+ }
}
}
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 f92e3a521..4a699f5d0 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
@@ -33,8 +33,15 @@ public sealed class OnPlayerGuiEvent : IEvent
/// Gets the object data associated with this GUI event.
///
///
- /// : The selected chat channel. Does not indicate the actual used channel. 0 = Shout, 1 = Whisper, 2 = Talk, 3 = Party, 4 = DM
- /// : The
+ /// : The waypoint the map note is attached to.
+ /// : The owner of the character sheet.
+ /// : The owner of the character sheet.
+ /// : The player that was clicked.
+ /// : The creature that was clicked.
+ /// : For , the owner of the character sheet. For GUIPanel.Examine*, the object being examined.
+ /// : The object being examined.
+ /// : The owner of the portrait that was clicked.
+ /// : The selected player.
///
public NwObject EventObject { get; } = NWScript.GetLastGuiEventObject().ToNwObject()!;
diff --git a/NWN.Anvil/src/main/API/Objects/CreatureSpellAbility.cs b/NWN.Anvil/src/main/API/Objects/CreatureSpellAbility.cs
new file mode 100644
index 000000000..0c1f797b6
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Objects/CreatureSpellAbility.cs
@@ -0,0 +1,42 @@
+using NWN.Core;
+
+namespace Anvil.API
+{
+ ///
+ /// Represents a spell-like ability on a creature.
+ ///
+ public sealed class CreatureSpellAbility
+ {
+ private readonly NwCreature creature;
+
+ ///
+ /// Get the internal index of this spell ability.
+ ///
+ public int Index { get; }
+
+ ///
+ /// Gets the associated spell cast when using this spell ability.
+ ///
+ public NwSpell Spell => NwSpell.FromSpellId(NWScript.GetSpellAbilitySpell(creature, Index))!;
+
+ ///
+ /// Gets the caster level of the spell ability.
+ ///
+ public int CasterLevel => NWScript.GetSpellAbilityCasterLevel(creature, Index);
+
+ ///
+ /// Gets or sets if this spell ability is ready and can be used.
+ ///
+ public bool Ready
+ {
+ get => NWScript.GetSpellAbilityReady(creature, Index).ToBool();
+ set => NWScript.SetSpellAbilityReady(creature, Index, value.ToInt());
+ }
+
+ internal CreatureSpellAbility(NwCreature creature, int index)
+ {
+ this.creature = creature;
+ Index = index;
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Objects/NwArea.cs b/NWN.Anvil/src/main/API/Objects/NwArea.cs
index 79147154e..bd41aa4d7 100644
--- a/NWN.Anvil/src/main/API/Objects/NwArea.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwArea.cs
@@ -945,6 +945,50 @@ public void SetTiles(List data, SettleFlags flags = SettleFlags.Recomp
}
}
+ ///
+ /// Set to true to disable the inaccessible tile border in this area. Requires clients to reload the area to take effect.
+ ///
+ /// True to disable tile borders, false to enable.
+ public void SetAreaTileBorderDisabled(bool disabled)
+ {
+ NWScript.SetAreaTileBorderDisabled(this, disabled.ToInt());
+ }
+
+ ///
+ /// Sets a grass override for a specific material in this area.
+ ///
+ /// The material to override.
+ /// The replacement grass texture
+ /// The density of the grass.
+ /// The height of the grass.
+ /// The ambient color of the grass. Alpha channel is ignored.
+ /// The diffuse color of the grass. Alpha channel is ignored.
+ public void SetAreaGrassOverride(SurfaceMaterialTableEntry material, string texture, float density, float height, Color ambientColor, Color diffuseColor)
+ {
+ Vector3 ambientColorVec = new Vector3(ambientColor.RedF, ambientColor.GreenF, ambientColor.BlueF);
+ Vector3 diffuseColorVec = new Vector3(diffuseColor.RedF, diffuseColor.GreenF, diffuseColor.BlueF);
+
+ NWScript.SetAreaGrassOverride(this, material.RowIndex, texture, density, height, ambientColorVec, diffuseColorVec);
+ }
+
+ ///
+ /// Remove a grass override from this area that was set with .
+ ///
+ /// The modified material to reset.
+ public void RemoveAreaGrassOverride(SurfaceMaterialTableEntry material)
+ {
+ NWScript.RemoveAreaGrassOverride(this, material.RowIndex);
+ }
+
+ ///
+ /// Set if the default grass of this area should be disabled.
+ ///
+ /// The new disabled state.
+ public void SetAreaDefaultGrassDisabled(bool disabled)
+ {
+ NWScript.SetAreaDefaultGrassDisabled(this, disabled.ToInt());
+ }
+
///
/// Gets a light color in this area.
///
diff --git a/NWN.Anvil/src/main/API/Objects/NwCreature.cs b/NWN.Anvil/src/main/API/Objects/NwCreature.cs
index 6d685b046..7991da289 100644
--- a/NWN.Anvil/src/main/API/Objects/NwCreature.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwCreature.cs
@@ -560,11 +560,6 @@ public bool IsRangedWeaponEquipped
///
public SpecialAttack LastSpecialAttackType => (SpecialAttack)NWScript.GetLastAttackType(this);
- ///
- /// Gets the caster level of the last spell this creature casted.
- ///
- public int LastSpellCasterLevel => NWScript.GetCasterLevel(this);
-
///
/// Gets the last trap detected by this creature.
///
@@ -786,6 +781,20 @@ public ushort SoundSet
set => Creature.m_nSoundSet = value;
}
+ ///
+ /// Gets a list of spell abilities usable by this creature.
+ ///
+ public IEnumerable SpellAbilities
+ {
+ get
+ {
+ for (int i = 0; i < NWScript.GetSpellAbilityCount(this); i++)
+ {
+ yield return new CreatureSpellAbility(this, i);
+ }
+ }
+ }
+
///
/// Gets the special abilities available to this creature.
///
diff --git a/NWN.Anvil/src/main/API/Objects/NwDoor.cs b/NWN.Anvil/src/main/API/Objects/NwDoor.cs
index 2d90a590b..ec62e569e 100644
--- a/NWN.Anvil/src/main/API/Objects/NwDoor.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwDoor.cs
@@ -130,7 +130,7 @@ public DoorOpenState DoorOpenState
public override NwDoor Clone(Location location, string? newTag = null, bool copyLocalState = true)
{
- return NWScript.CopyObject(this, location, sNewTag: newTag ?? string.Empty, bCopyLocalState: copyLocalState.ToInt()).ToNwObject()!;
+ return CloneInternal(location, newTag, copyLocalState);
}
///
diff --git a/NWN.Anvil/src/main/API/Objects/NwEncounter.cs b/NWN.Anvil/src/main/API/Objects/NwEncounter.cs
index e942a6c64..7e73533d8 100644
--- a/NWN.Anvil/src/main/API/Objects/NwEncounter.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwEncounter.cs
@@ -222,7 +222,7 @@ public int Spawns
public override NwEncounter Clone(Location location, string? newTag = null, bool copyLocalState = true)
{
- throw new NotSupportedException("Encounter objects may not be cloned.");
+ return CloneInternal(location, newTag, copyLocalState);
}
public override void Destroy()
diff --git a/NWN.Anvil/src/main/API/Objects/NwGameObject.cs b/NWN.Anvil/src/main/API/Objects/NwGameObject.cs
index a4fa326e5..6f5d7076a 100644
--- a/NWN.Anvil/src/main/API/Objects/NwGameObject.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwGameObject.cs
@@ -57,6 +57,17 @@ public IEnumerable ActiveEffects
///
public NwArea? Area => GameObject.GetArea().ToNwObject();
+ ///
+ /// Gets the caster level of this object.
+ ///
+ ///
+ /// A creature will return the caster level of their currently cast spell or ability, or the item's caster level if an item was used.
+ /// A placeable will return an automatic caster level: floor(10, (spell innate level * 2) - 1)
+ /// An Area of Effect object will return the caster level that was used to create the Area of Effect.
+ /// Otherwise, returns 0
+ ///
+ public int CasterLevel => NWScript.GetCasterLevel(this);
+
///
/// Gets or sets the highlight color of this object.
///
@@ -247,15 +258,17 @@ public ObjectUiDiscovery UiDiscoveryFlags
///
/// The spell to cast.
/// The target for the spell.
- /// Metamagic that should be applied to the spell.
- /// If true, this object doesn't have to be able to cast the spell.
+ /// Metamagic that should be applied to the spell. If class is specified, cannot be .
+ /// If true, this object doesn't have to be able to cast the spell. Ignored if class is specified.
/// Specifies the spell level if the spell is to be cast as a domain spell.
/// The type of projectile path to use for this spell.
/// If true, the spell is cast immediately.
- public async Task ActionCastSpellAt(NwSpell spell, NwGameObject target, MetaMagic metaMagic = MetaMagic.Any, bool cheat = false, int domainLevel = 0, ProjectilePathType projectilePathType = ProjectilePathType.Default, bool instant = false)
+ /// If specified, the spell will be cast using that class specifically. Null will use spell abilities instead.
+ /// If true, the creature will attempt to cast the given spell spontaneously. Requires class parameter is set to a valid class with spontaneous cast spells.
+ public async Task ActionCastSpellAt(NwSpell spell, NwGameObject target, MetaMagic metaMagic = MetaMagic.Any, bool cheat = false, int domainLevel = 0, ProjectilePathType projectilePathType = ProjectilePathType.Default, bool instant = false, NwClass? spellClass = null, bool spontaneousCast = false)
{
await WaitForObjectContext();
- NWScript.ActionCastSpellAtObject(spell.Id, target, (int)metaMagic, cheat.ToInt(), domainLevel, (int)projectilePathType, instant.ToInt());
+ NWScript.ActionCastSpellAtObject(spell.Id, target, (int)metaMagic, cheat.ToInt(), domainLevel, (int)projectilePathType, instant.ToInt(), spellClass?.Id ?? -1, spontaneousCast.ToInt());
}
///
@@ -267,10 +280,13 @@ public async Task ActionCastSpellAt(NwSpell spell, NwGameObject target, MetaMagi
/// If true, this object doesn't have to be able to cast the spell.
/// The type of projectile path to use for this spell.
/// If true, the spell is cast immediately.
- public async Task ActionCastSpellAt(NwSpell spell, Location target, MetaMagic metaMagic = MetaMagic.Any, bool cheat = false, ProjectilePathType projectilePathType = ProjectilePathType.Default, bool instant = false)
+ /// If specified, the spell will be cast using that class specifically. Null will use spell abilities instead.
+ /// If true, the creature will attempt to cast the given spell spontaneously. Requires class parameter is set to a valid class with spontaneous cast spells.
+ /// Specifies the spell level if the spell is to be cast as a domain spell.
+ public async Task ActionCastSpellAt(NwSpell spell, Location target, MetaMagic metaMagic = MetaMagic.Any, bool cheat = false, ProjectilePathType projectilePathType = ProjectilePathType.Default, bool instant = false, NwClass? spellClass = null, bool spontaneousCast = false, int domainLevel = 0)
{
await WaitForObjectContext();
- NWScript.ActionCastSpellAtLocation(spell.Id, target, (int)metaMagic, cheat.ToInt(), (int)projectilePathType, instant.ToInt());
+ NWScript.ActionCastSpellAtLocation(spell.Id, target, (int)metaMagic, cheat.ToInt(), (int)projectilePathType, instant.ToInt(), spellClass?.Id ?? -1, spontaneousCast.ToInt(), domainLevel);
}
///
diff --git a/NWN.Anvil/src/main/API/Objects/NwItem.cs b/NWN.Anvil/src/main/API/Objects/NwItem.cs
index 934383015..036550fe2 100644
--- a/NWN.Anvil/src/main/API/Objects/NwItem.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwItem.cs
@@ -196,7 +196,7 @@ public bool Pickpocketable
/// Gets the GameObject that has this item in its inventory. Returns null if it is on the ground, or not in any inventory.
///
/// This can be a creature, a placeable, or an item container (e.g magic bag). Use to get the root possessor of this item.
- public NwGameObject? Possessor => NWScript.GetItemPossessor(this).ToNwObject();
+ public NwGameObject? Possessor => NWScript.GetItemPossessor(this, true.ToInt()).ToNwObject();
///
/// Gets or sets the number of stacked items attached to this item.
@@ -230,19 +230,7 @@ public string UnidentifiedDescription
/// Gets the root possessor of this item.
///
/// If this item is in a container, this is the creature/placeable holding the container that holds this item. Otherwise, this returns the same object as .
- public NwGameObject? RootPossessor
- {
- get
- {
- NwGameObject? possessor = Possessor;
- if (possessor is NwItem item)
- {
- return item.Possessor as NwCreature;
- }
-
- return possessor as NwCreature;
- }
- }
+ public NwGameObject? RootPossessor => NWScript.GetItemPossessor(this, false.ToInt()).ToNwObject();
///
/// Gets or sets the weight of this item, in pounds.
diff --git a/NWN.Anvil/src/main/API/Objects/NwPlayer.cs b/NWN.Anvil/src/main/API/Objects/NwPlayer.cs
index 93a8e7ea9..36b2af184 100644
--- a/NWN.Anvil/src/main/API/Objects/NwPlayer.cs
+++ b/NWN.Anvil/src/main/API/Objects/NwPlayer.cs
@@ -816,9 +816,10 @@ public void FadeToBlack(float fadeSpeed)
///
/// The message to display.
/// If true, shows the floating message to all players in the same party.
- public void FloatingTextString(string message, bool broadcastToParty = true)
+ /// If true, the floating text will be shown in the player's chat window.
+ public void FloatingTextString(string message, bool broadcastToParty = true, bool chatWindow = true)
{
- NWScript.FloatingTextStringOnCreature(message, ControlledCreature, broadcastToParty.ToInt());
+ NWScript.FloatingTextStringOnCreature(message, ControlledCreature, broadcastToParty.ToInt(), chatWindow.ToInt());
}
///
@@ -826,9 +827,10 @@ public void FloatingTextString(string message, bool broadcastToParty = true)
///
/// The string ref index to use.
/// If true, shows the floating message to all players in the same party.
- public void FloatingTextStrRef(int strRef, bool broadcastToParty = true)
+ /// If true, the floating text will be shown in the player's chat window.
+ public void FloatingTextStrRef(int strRef, bool broadcastToParty = true, bool chatWindow = true)
{
- NWScript.FloatingTextStrRefOnCreature(strRef, ControlledCreature, broadcastToParty.ToInt());
+ NWScript.FloatingTextStrRefOnCreature(strRef, ControlledCreature, broadcastToParty.ToInt(), chatWindow.ToInt());
}
///
@@ -1530,6 +1532,63 @@ public void ShowVisualEffect(VfxType effectType, Vector3 position)
message.SendServerToPlayerArea_VisualEffect(Player, (ushort)effectType, position.ToNativeVector(), visualTransformData);
}
+ ///
+ /// Assign one of the available audio streams to play a specific file. This mechanism can be used
+ /// to replace regular music playback, and synchronize it between clients.
+ ///
+ /// The audio stream/channel to use for playing audio.
+ /// The audio resref to play.
+ /// If the audio should loop.
+ /// An optional fade in time for the audio.
+ /// The offset to seek in the audio track. If this is greater than the length of the track, it will loop back to the start.
+ /// The volume to set on the audio stream.
+ public void StartAudioStream(AudioStreamIdentifier streamIdentifier, string resRef, bool looping = false, TimeSpan fadeTime = default, float seekOffset = -1f, float volume = 1f)
+ {
+ NWScript.StartAudioStream(ControlledCreature, (int)streamIdentifier, resRef, looping.ToInt(), (float)fadeTime.TotalSeconds, seekOffset, volume);
+ }
+
+ ///
+ /// Stops the audio stream playing on the specified identifier.
+ ///
+ /// The stream identifier to stop.
+ /// The fade out time.
+ public void StopAudioStream(AudioStreamIdentifier streamIdentifier, TimeSpan fadeTime = default)
+ {
+ NWScript.StopAudioStream(ControlledCreature, (int)streamIdentifier, (float)fadeTime.TotalSeconds);
+ }
+
+ ///
+ /// Pauses/Unpauses the audio stream on the specified identifier.
+ ///
+ /// The stream identifier to pause/unpause.
+ /// The new pause state.
+ /// The time to fade out/fade in before pausing/unpausing the stream.
+ public void SetAudioStreamPaused(AudioStreamIdentifier streamIdentifier, bool paused, TimeSpan fadeTime = default)
+ {
+ NWScript.SetAudioStreamPaused(ControlledCreature, (int)streamIdentifier, paused.ToInt(), (float)fadeTime.TotalSeconds);
+ }
+
+ ///
+ /// Changes the volume of the audio stream with the specified identifier.
+ ///
+ /// The stream identifier to receive the new volume level.
+ /// The new volume level (0.0-1.0)
+ /// The time to fade in to the new volume level.
+ public void SetAudioStreamVolume(AudioStreamIdentifier streamIdentifier, float volume = 1.0f, TimeSpan fadeTime = default)
+ {
+ NWScript.SetAudioStreamVolume(ControlledCreature, (int)streamIdentifier, volume, (float)fadeTime.TotalSeconds);
+ }
+
+ ///
+ /// Seeks the audio stream with the specified identifier to a new position on the currently playing audio track.
+ ///
+ /// The stream identifier to update.
+ /// The new position on the audio track
+ public void SeekAudioStream(AudioStreamIdentifier streamIdentifier, float seekOffset)
+ {
+ NWScript.SeekAudioStream(ControlledCreature, (int)streamIdentifier, seekOffset);
+ }
+
///
/// Removes any current fading effects or black screen from the monitor of the player.
///
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 5c44db631..123e44441 100644
--- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.Factory.cs
@@ -148,6 +148,7 @@ private void LoadTables()
DamageLevelTable = GetTable("damagelevels.2da")!; // arrays.m_pDamageLevelTable does not exist in nwserver.
ExpTable = GetTable("exptable.2da")!;
SkillItemCostTable = GetTable(arrays.GetSkillVsItemCostTable());
+ SurfaceMaterialTable = GetTable(arrays.GetSurfaceMaterialTable());
}
private void OnReloadAll(void* pRules)
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
index 1d7d6b778..2fb51c86a 100644
--- a/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/NwGameTables.cs
@@ -147,6 +147,11 @@ public static partial class NwGameTables
///
public static TwoDimArray SkillItemCostTable { get; private set; } = null!;
+ ///
+ /// Gets surface material table (surfacemat.2da)
+ ///
+ public static TwoDimArray SurfaceMaterialTable { get; private set; } = null!;
+
///
/// Gets the visual effect table (visualeffects.2da)
///
diff --git a/NWN.Anvil/src/main/API/TwoDimArray/Tables/SurfaceMaterialTableEntry.cs b/NWN.Anvil/src/main/API/TwoDimArray/Tables/SurfaceMaterialTableEntry.cs
new file mode 100644
index 000000000..deeb33263
--- /dev/null
+++ b/NWN.Anvil/src/main/API/TwoDimArray/Tables/SurfaceMaterialTableEntry.cs
@@ -0,0 +1,71 @@
+namespace Anvil.API
+{
+ public sealed class SurfaceMaterialTableEntry : ITwoDimArrayEntry
+ {
+ private const int MaxActions = 8;
+
+ ///
+ /// Gets the human readable name of the surface material. Otherwise unused.
+ ///
+ public string? Label { get; private set; }
+
+ ///
+ /// Gets if the player may walk on this part of the walkmesh.
+ ///
+ public bool? Walk { get; private set; }
+
+ public bool? WalkCheck { get; private set; }
+
+ ///
+ /// Gets if the material blocks Line of Sight. For instance Grass will but Transparent will not.
+ ///
+ public bool? LineOfSight { get; private set; }
+
+ public string? Sound { get; private set; }
+
+ ///
+ /// Gets the column name prefix for the footsteps table which defines the noise made when walking on the surface.
+ ///
+ public string? Name { get; private set; }
+
+ ///
+ /// Gets if this material is considered water.
+ ///
+ public bool? IsWater { get; private set; }
+
+ ///
+ /// Gets the effect to place at a creature's feet when walking through this material.
+ ///
+ public string? Visual { get; private set; }
+
+ ///
+ /// Gets the list of string references used to show actions in the radial menu when right clicking this material.
+ ///
+ public string?[] ActionStrRefs { get; } = new string?[MaxActions];
+
+ ///
+ /// Gets the list of icons used to show actions in the radial menu when right clicking this material.
+ ///
+ public string?[] ActionIcons { get; } = new string?[MaxActions];
+
+ public int RowIndex { get; init; }
+
+ public void InterpretEntry(TwoDimArrayEntry entry)
+ {
+ Label = entry.GetString("Label");
+ Walk = entry.GetBool("Walk");
+ WalkCheck = entry.GetBool("WalkCheck");
+ LineOfSight = entry.GetBool("LineOfSight");
+ Sound = entry.GetString("Sound");
+ Name = entry.GetString("Name");
+ IsWater = entry.GetBool("IsWater");
+ Visual = entry.GetString("Visual");
+
+ for (int i = 0; i < MaxActions; i++)
+ {
+ ActionStrRefs[i] = entry.GetString($"Act{i}_Strref");
+ ActionIcons[i] = entry.GetString($"Act{i}_Icon");
+ }
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Variables/Campaign/CampaignVariableObject.cs b/NWN.Anvil/src/main/API/Variables/Campaign/CampaignVariableObject.cs
index 5c459cc6b..2833cd87c 100644
--- a/NWN.Anvil/src/main/API/Variables/Campaign/CampaignVariableObject.cs
+++ b/NWN.Anvil/src/main/API/Variables/Campaign/CampaignVariableObject.cs
@@ -3,7 +3,7 @@
namespace Anvil.API
{
- public sealed class CampaignVariableObject : CampaignVariable where T : NwObject
+ public sealed class CampaignVariableObject : CampaignVariable where T : NwGameObject
{
public override T Value
{
diff --git a/docs/NWN.Anvil.Samples.csproj b/docs/NWN.Anvil.Samples.csproj
index efef41753..15ba5a30d 100644
--- a/docs/NWN.Anvil.Samples.csproj
+++ b/docs/NWN.Anvil.Samples.csproj
@@ -30,7 +30,7 @@
-
+