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