diff --git a/.env b/.env
index 9b57de63c..7ed132ebd 100644
--- a/.env
+++ b/.env
@@ -1,2 +1,2 @@
NWN_VERSION=8193.34
-NWNX_VERSION=fe195ec
+NWNX_VERSION=2692ecb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6f9b548f..5905b8aaf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,44 @@ 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.23
+https://github.com/nwn-dotnet/Anvil/compare/v8193.34.22...v8193.34.23
+
+### Added
+- Added `ANVIL_ENCODING` environment variable for specifying a custom encoding when converting native strings from nwserver.
+- Added `EncodingService` for changing the server encoding at runtime.
+- NUI: Added StrRef support with `NuiBindStrRef` and `NuiValueStrRef` types.
+- Events: Added `OnDoorSetOpenState` event.
+- Events: Added `OnObjectUse` event.
+- Extensions: Added `TryParseObject` extension for parsing object ID strings.
+- NwCreature: Added `GetInitiativeModifier`,`SetInitiativeModifier`,`ClearInitiativeModifier` methods.
+- NwCreature: Added `IsDMAvatar` property.
+- NwCreature: Added `IsFlanking` method.
+- NwDoor: Added `DoorOpenState` property.
+- NwRuleset: Added NwDomain ruleset table and replaced constant usages with table references.
+- NwServer: Added `IsActivePaused` property.
+- NwServer: Added `IsTimestopPaused` property.
+
+### Package Updates
+- Microsoft.CodeAnalysis.CSharp: 4.3.1 -> 4.4.0
+- NWN.Core: 8193.34.7 -> 8193.34.10
+- NWN.Native: 8193.34.4 -> 8193.34.5
+- LightInject: 6.6.1 -> 6.6.3
+- Newtonsoft.Json: 13.0.1 -> 13.0.2
+- NLog: 5.0.5 -> 5.1.1
+- Paket.Core: 7.1.5 -> 7.2.0
+- NWNX: fe195ec -> 2692ecb
+
+### Changed
+- Events: `OnSpellAction` Domain and Feat is now nullable.
+- Events: `OnSpellInterrupt` Domain and Feat is now nullable.
+- Events: `OnSpellSlotMemorize` Domain is now nullable.
+- `System.Random` usages now use the `System.Random.Shared` instance, instead of individual instances.
+
+### Fixed
+- Fixed an issue where a GameObject or Player could become stuck in a hash-based collection when it became invalid.
+- NwPlayer: `IsDM` now correctly returns true when a DM is possessing a creature. Use `ControlledCreature.IsDM` for the prior behaviour.
+
## 8193.34.22
https://github.com/nwn-dotnet/Anvil/compare/v8193.34.21...v8193.34.22
diff --git a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
index f87d7a193..7f1630a1d 100644
--- a/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
+++ b/NWN.Anvil.TestRunner/NWN.Anvil.TestRunner.csproj
@@ -56,7 +56,7 @@
-
+
diff --git a/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj b/NWN.Anvil.Tests/NWN.Anvil.Tests.csproj
index 9f1e2b22c..17fce78d6 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/Nui/Bindings/NuiBindTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs
index c4c6956b1..d6fd0c784 100644
--- a/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiBindTests.cs
@@ -13,6 +13,13 @@ public void SerializeNuiBindStringReturnsValidJsonStructure()
Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""bind"":""test""}"));
}
+ [Test(Description = "Serializing a NuiBind creates a valid JSON structure.")]
+ public void SerializeNuiBindStrRefReturnsValidJsonStructure()
+ {
+ NuiBindStrRef test = new NuiBindStrRef("test");
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""bind"":""test""}"));
+ }
+
[Test(Description = "Serializing a NuiBind creates a valid JSON structure.")]
public void SerializeNuiBindNuiRectReturnsValidJsonStructure()
{
@@ -20,14 +27,21 @@ public void SerializeNuiBindNuiRectReturnsValidJsonStructure()
Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"{""bind"":""test""}"));
}
- [Test(Description = "Deerializing a NuiBind creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiBind creates a valid value/object.")]
public void DeserializeNuiBindStringReturnsValidJsonStructure()
{
NuiBind? test = JsonUtility.FromJson>(@"{""bind"":""test""}");
Assert.That(test?.Key, Is.EqualTo("test"));
}
- [Test(Description = "Deerializing a NuiBind creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiBind creates a valid value/object.")]
+ public void DeserializeNuiBindStrRefReturnsValidJsonStructure()
+ {
+ NuiBind? test = JsonUtility.FromJson>(@"{""bind"":""test""}");
+ Assert.That(test?.Key, Is.EqualTo("test"));
+ }
+
+ [Test(Description = "Deerializing a NuiBind creates a valid value/object.")]
public void DeserializeNuiBindNuiRectReturnsValidJsonStructure()
{
NuiBind? test = JsonUtility.FromJson>(@"{""bind"":""test""}");
diff --git a/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs
index ee44d0bb7..28a324bae 100644
--- a/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/Nui/Bindings/NuiValueTests.cs
@@ -17,6 +17,16 @@ public void SerializeNuiValueStringReturnsValidJsonStructure(string value, strin
Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
}
+ [Test(Description = "Serializing a NuiValueStrRef creates a valid JSON structure.")]
+ [TestCase(0u, @"{""strref"":0}")]
+ [TestCase(null, @"null")]
+ [TestCase(1000u, @"{""strref"":1000}")]
+ public void SerializeNuiValueStrRefReturnsValidJsonStructure(uint? value, string expected)
+ {
+ NuiValueStrRef test = new NuiValueStrRef(value != null ? new StrRef(value.Value) : null);
+ Assert.That(JsonUtility.ToJson(test), Is.EqualTo(expected));
+ }
+
[Test(Description = "Serializing a NuiValue creates a valid JSON structure.")]
[TestCase(0, @"0")]
[TestCase(-0, @"0")]
@@ -91,7 +101,7 @@ public void SerializeNuiValueIntListReturnsValidJsonStructure()
Assert.That(JsonUtility.ToJson(test), Is.EqualTo(@"[1,2,3]"));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
[TestCase("test", @"""test""")]
[TestCase(null, @"null")]
[TestCase("", @"""""")]
@@ -101,7 +111,17 @@ public void DeserializeNuiValueStringReturnsValidJsonStructure(string expected,
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValueStrRef creates a valid value/object.")]
+ [TestCase(0u, @"{""strref"":0}")]
+ [TestCase(null, @"null")]
+ [TestCase(1000u, @"{""strref"":1000}")]
+ public void DeserializeNuiValueStrRefReturnsValidJsonStructure(uint? expected, string serialized)
+ {
+ NuiValueStrRef? test = JsonUtility.FromJson(serialized);
+ Assert.That(test?.Value?.Id, Is.EqualTo(expected));
+ }
+
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
[TestCase(0, @"0")]
[TestCase(-0, @"0")]
[TestCase(10, @"10")]
@@ -114,7 +134,7 @@ public void DeserializeNuiValueIntReturnsValidJsonStructure(int expected, string
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
[TestCase(0, @"0")]
[TestCase(-0, @"0")]
[TestCase(10, @"10")]
@@ -128,7 +148,7 @@ public void DeserializeNuiValueNullableIntReturnsValidJsonStructure(int? expecte
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
[TestCase(0f, @"0.0")]
[TestCase(0.1f, @"0.1")]
[TestCase(0.125f, @"0.125")]
@@ -144,7 +164,7 @@ public void DeserializeNuiValueFloatReturnsValidJsonStructure(float expected, st
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
[TestCase(0f, @"0.0")]
[TestCase(0.1f, @"0.1")]
[TestCase(0.125f, @"0.125")]
@@ -161,7 +181,7 @@ public void DeserializeNuiValueFloatNullableReturnsValidJsonStructure(float? exp
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deerializing a NuiValue creates a valid JSON structure.")]
+ [Test(Description = "Deerializing a NuiValue creates a valid value/object.")]
public void DeserializeNuiValueNuiRectReturnsValidJsonStructure()
{
NuiValue? test = JsonUtility.FromJson>(@"{""h"":20.0,""w"":30.11,""x"":100.0,""y"":50.251}");
@@ -170,7 +190,7 @@ public void DeserializeNuiValueNuiRectReturnsValidJsonStructure()
Assert.That(test?.Value, Is.EqualTo(expected));
}
- [Test(Description = "Deserializing a NuiValue> creates a valid JSON structure.")]
+ [Test(Description = "Deserializing a NuiValue> creates a valid value/object.")]
public void DeserializeNuiValueIntListReturnsValidJsonStructure()
{
NuiValue>? test = JsonUtility.FromJson>>(@"[1,2,3]");
diff --git a/NWN.Anvil.Tests/src/main/API/TwoDimArray/TwoDimArrayTests.cs b/NWN.Anvil.Tests/src/main/API/TwoDimArray/TwoDimArrayTests.cs
index 0cc6193a2..f4fc2b68c 100644
--- a/NWN.Anvil.Tests/src/main/API/TwoDimArray/TwoDimArrayTests.cs
+++ b/NWN.Anvil.Tests/src/main/API/TwoDimArray/TwoDimArrayTests.cs
@@ -22,12 +22,12 @@ public void Custom2daReturnsValidValues()
@"2DA V2.0
LABEL TESTSTR TESTINT TESTFLOAT
-0 Test0 ""Test 0"" 0 0.0f
+0 Test0 ""Test 0"" 0 0.0f
1 Test1 ""Test 1"" 0x1 1.0f
2 Test2 ""Test 2"" 0x00000002 2.0f";
string resourceName = "testtemp.2da";
- ResourceManager.WriteTempResource(resourceName, StringHelper.Cp1252Encoding.GetBytes(twoDimArray));
+ ResourceManager.WriteTempResource(resourceName, StringHelper.Encoding.GetBytes(twoDimArray));
createdTempResources.Add(resourceName);
TwoDimArray array = NwGameTables.GetTable(resourceName);
diff --git a/NWN.Anvil.sln.DotSettings b/NWN.Anvil.sln.DotSettings
index 91206e138..3d60d7236 100644
--- a/NWN.Anvil.sln.DotSettings
+++ b/NWN.Anvil.sln.DotSettings
@@ -111,6 +111,7 @@
WARNING
SUGGESTION
WARNING
+ DO_NOT_SHOW
SUGGESTION
DO_NOT_SHOW
DO_NOT_SHOW
diff --git a/NWN.Anvil/NWN.Anvil.csproj b/NWN.Anvil/NWN.Anvil.csproj
index 21df3bc0e..55f00da67 100644
--- a/NWN.Anvil/NWN.Anvil.csproj
+++ b/NWN.Anvil/NWN.Anvil.csproj
@@ -57,12 +57,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/NWN.Anvil/src/main/API/Color.cs b/NWN.Anvil/src/main/API/Color.cs
index 09aad8a25..bb30ab6a4 100644
--- a/NWN.Anvil/src/main/API/Color.cs
+++ b/NWN.Anvil/src/main/API/Color.cs
@@ -154,7 +154,7 @@ public string ToColorToken()
{
const byte tokenMinVal = 1;
ReadOnlySpan tokenBytes = stackalloc[] { Math.Max(Red, tokenMinVal), Math.Max(Green, tokenMinVal), Math.Max(Blue, tokenMinVal) };
- return StringHelper.Cp1252Encoding.GetString(tokenBytes);
+ return StringHelper.Encoding.GetString(tokenBytes);
}
[Obsolete("Use Color.ToRGBA() instead.")]
diff --git a/NWN.Anvil/src/main/API/Constants/DoorOpenState.cs b/NWN.Anvil/src/main/API/Constants/DoorOpenState.cs
new file mode 100644
index 000000000..bb61f7a80
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Constants/DoorOpenState.cs
@@ -0,0 +1,25 @@
+namespace Anvil.API
+{
+ public enum DoorOpenState
+ {
+ ///
+ /// The closed state of the door.
+ ///
+ Closed = 0,
+
+ ///
+ /// The forward open state of the door. This is the direction of the arrow as shown in the toolset.
+ ///
+ OpenForward = 1,
+
+ ///
+ /// The backward open state of the door. This is the opposite direction of the arrow as shown in the toolset.
+ ///
+ OpenBackward = 2,
+
+ ///
+ /// The destroyed state of the door.
+ ///
+ Destroyed = 3,
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnDoorSetOpenState.cs b/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnDoorSetOpenState.cs
new file mode 100644
index 000000000..1e32b044b
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnDoorSetOpenState.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Runtime.InteropServices;
+using Anvil.API.Events;
+using Anvil.Services;
+using NWN.Native.API;
+
+namespace Anvil.API.Events
+{
+ ///
+ /// Called when a door's open state is changed (open/closed/destroyed).
+ ///
+ public sealed class OnDoorSetOpenState : IEvent
+ {
+ ///
+ /// The door that is being open/closed.
+ ///
+ public NwDoor Door { get; private init; } = null!;
+
+ ///
+ /// The new open state of the door.
+ ///
+ public DoorOpenState OpenState { get; set; }
+
+ ///
+ /// Gets or sets if the door state should not be changed.
+ ///
+ public bool PreventStateChange { get; set; }
+
+ NwObject IEvent.Context => Door;
+
+ public sealed unsafe class Factory : HookEventFactory
+ {
+ private static FunctionHook Hook { get; set; } = null!;
+
+ private delegate void SetOpenStateHook(void* pDoor, byte nOpenState);
+
+ protected override IDisposable[] RequestHooks()
+ {
+ delegate* unmanaged pHook = &OnSetOpenState;
+ Hook = HookService.RequestHook(pHook, FunctionsLinux._ZN8CNWSDoor12SetOpenStateEh, HookOrder.Early);
+ return new IDisposable[] { Hook };
+ }
+
+ [UnmanagedCallersOnly]
+ private static void OnSetOpenState(void* pDoor, byte nOpenState)
+ {
+ NwDoor? door = CNWSDoor.FromPointer(pDoor).ToNwObject();
+ if (door == null)
+ {
+ Hook.CallOriginal(pDoor, nOpenState);
+ return;
+ }
+
+ OnDoorSetOpenState eventData = ProcessEvent(EventCallbackType.Before, new OnDoorSetOpenState
+ {
+ Door = door,
+ OpenState = (DoorOpenState)nOpenState,
+ });
+
+ if (!eventData.PreventStateChange)
+ {
+ Hook.CallOriginal(pDoor, (byte)eventData.OpenState);
+ }
+
+ ProcessEvent(EventCallbackType.After, eventData);
+ }
+ }
+ }
+}
+
+namespace Anvil.API
+{
+ public sealed partial class NwDoor
+ {
+ ///
+ public event Action OnDoorSetOpenState
+ {
+ add => EventService.Subscribe(this, value);
+ remove => EventService.Unsubscribe(this, value);
+ }
+ }
+
+ public sealed partial class NwModule
+ {
+ ///
+ public event Action OnDoorSetOpenState
+ {
+ add => EventService.SubscribeAll(value);
+ remove => EventService.UnsubscribeAll(value);
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnObjectUse.cs b/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnObjectUse.cs
new file mode 100644
index 000000000..30177f826
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Events/Native/ObjectEvents/OnObjectUse.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Runtime.InteropServices;
+using Anvil.API.Events;
+using Anvil.Services;
+using NWN.Native.API;
+
+namespace Anvil.API.Events
+{
+ ///
+ /// Called when a creature is about to use an object.
+ ///
+ public sealed class OnObjectUse : IEvent
+ {
+ ///
+ /// The object that is being used.
+ ///
+ public NwGameObject Object { get; private init; } = null!;
+
+ ///
+ /// The creature using the object.
+ ///
+ public NwCreature UsedBy { get; private init; } = null!;
+
+ ///
+ /// Gets or sets if usage of the object should be prevented.
+ ///
+ public bool PreventObjectUse { get; set; }
+
+ NwObject IEvent.Context => UsedBy;
+
+ public sealed unsafe class Factory : HookEventFactory
+ {
+ private static FunctionHook Hook { get; set; } = null!;
+
+ private delegate int AddUseObjectActionHook(void* pObject, uint oidObjectToUse);
+
+ protected override IDisposable[] RequestHooks()
+ {
+ delegate* unmanaged pHook = &OnAddUseObjectAction;
+ Hook = HookService.RequestHook(pHook, FunctionsLinux._ZN10CNWSObject18AddUseObjectActionEj, HookOrder.Early);
+ return new IDisposable[] { Hook };
+ }
+
+ [UnmanagedCallersOnly]
+ private static int OnAddUseObjectAction(void* pObject, uint oidObjectToUse)
+ {
+ NwCreature? usedBy = CNWSObject.FromPointer(pObject).ToNwObjectSafe();
+ NwGameObject? gameObject = oidObjectToUse.ToNwObjectSafe();
+
+ if (usedBy == null || gameObject == null)
+ {
+ return Hook.CallOriginal(pObject, oidObjectToUse);
+ }
+
+ OnObjectUse eventData = ProcessEvent(EventCallbackType.Before, new OnObjectUse
+ {
+ UsedBy = usedBy,
+ Object = gameObject,
+ });
+
+ int retVal = eventData.PreventObjectUse ? false.ToInt() : Hook.CallOriginal(pObject, oidObjectToUse);
+ ProcessEvent(EventCallbackType.After, eventData);
+
+ return retVal;
+ }
+ }
+ }
+}
+
+namespace Anvil.API
+{
+ public sealed partial class NwCreature
+ {
+ ///
+ public event Action OnObjectUse
+ {
+ add => EventService.Subscribe(this, value);
+ remove => EventService.Unsubscribe(this, value);
+ }
+ }
+
+ public sealed partial class NwModule
+ {
+ ///
+ public event Action OnObjectUse
+ {
+ add => EventService.SubscribeAll(value);
+ remove => EventService.UnsubscribeAll(value);
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellAction.cs b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellAction.cs
index 6f1c3ee10..85de4d29c 100644
--- a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellAction.cs
+++ b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellAction.cs
@@ -15,9 +15,9 @@ public sealed class OnSpellAction : IEvent
public int ClassIndex { get; private init; }
- public Domain Domain { get; private init; }
+ public NwDomain? Domain { get; private init; }
- public NwFeat Feat { get; private init; } = null!;
+ public NwFeat? Feat { get; private init; }
public bool IsAreaTarget { get; private init; }
@@ -69,7 +69,7 @@ private static int OnAddCastSpellActions(void* pCreature, uint nSpellId, int nMu
Caster = creature.ToNwObject()!,
Spell = NwSpell.FromSpellId((int)nSpellId)!,
ClassIndex = nMultiClass,
- Domain = (Domain)nDomainLevel,
+ Domain = NwDomain.FromDomainId(nDomainLevel),
MetaMagic = (MetaMagic)nMetaType,
IsSpontaneous = bSpontaneousCast.ToBool(),
TargetPosition = vTargetLocation,
@@ -78,7 +78,7 @@ private static int OnAddCastSpellActions(void* pCreature, uint nSpellId, int nMu
IsFake = bFake.ToBool(),
ProjectilePath = (ProjectilePathType)nProjectilePathType,
IsInstant = bInstant.ToBool(),
- Feat = NwFeat.FromFeatId(nFeat)!,
+ Feat = NwFeat.FromFeatId(nFeat),
CasterLevel = nCasterLevel,
};
diff --git a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellInterrupt.cs b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellInterrupt.cs
index 51d1dd4de..5b92f2159 100644
--- a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellInterrupt.cs
+++ b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellInterrupt.cs
@@ -10,9 +10,10 @@ public sealed class OnSpellInterrupt : IEvent
{
public int ClassIndex { get; private init; }
- public Domain Domain { get; private init; }
+ public NwDomain? Domain { get; private init; }
+
+ public NwFeat? Feat { get; private init; }
- public NwFeat Feat { get; private init; } = null!;
public NwGameObject InterruptedCaster { get; private init; } = null!;
public MetaMagic MetaMagic { get; private init; }
@@ -58,8 +59,8 @@ private static int OnEffectApplied(void* pEffectListHandler, void* pObject, void
InterruptedCaster = gameObject.ToNwObject()!,
Spell = NwSpell.FromSpellId((int)gameObject.m_nLastSpellId)!,
ClassIndex = gameObject.m_nLastSpellCastMulticlass,
- Feat = NwFeat.FromFeatId(gameObject.m_nLastSpellCastFeat)!,
- Domain = (Domain)gameObject.m_nLastDomainLevel,
+ Feat = NwFeat.FromFeatId(gameObject.m_nLastSpellCastFeat),
+ Domain = NwDomain.FromDomainId(gameObject.m_nLastDomainLevel),
Spontaneous = gameObject.m_bLastSpellCastSpontaneous.ToBool(),
MetaMagic = (MetaMagic)gameObject.m_nLastSpellCastMetaType,
});
diff --git a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellSlotMemorize.cs b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellSlotMemorize.cs
index 049e38297..8b1981a21 100644
--- a/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellSlotMemorize.cs
+++ b/NWN.Anvil/src/main/API/Events/Native/SpellEvents/OnSpellSlotMemorize.cs
@@ -12,7 +12,7 @@ public sealed class OnSpellSlotMemorize : IEvent
public NwCreature Creature { get; private init; } = null!;
- public Domain Domain { get; private init; }
+ public NwDomain? Domain { get; private init; }
public bool FromClient { get; private init; }
@@ -51,7 +51,7 @@ private static int OnSetMemorizedSpellSlot(void* pCreatureStats, byte nMultiClas
ClassIndex = nMultiClass,
SlotIndex = nSpellSlot,
Spell = NwSpell.FromSpellId((int)nSpellId)!,
- Domain = (Domain)nDomainLevel,
+ Domain = NwDomain.FromDomainId(nDomainLevel),
MetaMagic = (MetaMagic)nMetaType,
FromClient = bFromClient.ToBool(),
});
diff --git a/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs b/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
index 78449f65f..fa62261e6 100644
--- a/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
+++ b/NWN.Anvil/src/main/API/Extensions/StringExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
@@ -108,6 +109,42 @@ public static bool ParseIntBool(this string intBoolString, bool defaultValue)
return uint.Parse(objectIdString, NumberStyles.HexNumber).ToNwObject();
}
+ ///
+ /// Tries to resolve the specified GameObject ID string to an object. A return value
+ /// indicates whether the conversion succeeded or failed.
+ /// This is the temporary ID created from . See to parse persistent UUIDs.
+ ///
+ /// The object ID string to parse.
+ /// When this method returns, contains the object referenced by
+ /// the number contained in objectIdString, if the conversion succeeded, or null if
+ /// the conversion failed. The conversion fails if the s parameter is null or System.String.Empty,
+ /// is not in a format compliant with style, or represents an invalid object reference.
+ /// true if objectIdString was converted successfully; otherwise, false.
+ public static bool TryParseObject(this string objectIdString, [NotNullWhen(true)] out NwObject? result)
+ {
+ if (uint.TryParse(objectIdString, NumberStyles.HexNumber, null, out uint res) && res.ToNwObject() is {} obj)
+ {
+ result = obj;
+ return true;
+ }
+
+ result = null;
+ return false;
+ }
+
+ ///
+ public static bool TryParseObject(this string objectIdString, [NotNullWhen(true)] out T? result) where T : NwObject
+ {
+ if (uint.TryParse(objectIdString, NumberStyles.HexNumber, null, out uint res) && res.ToNwObject() is {} obj)
+ {
+ result = obj;
+ return true;
+ }
+
+ result = null;
+ return false;
+ }
+
public static string ReadBlock(this StringReader stringReader, int length)
{
char[] retVal = new char[length];
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiBindStrRef.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiBindStrRef.cs
new file mode 100644
index 000000000..df79f0638
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiBindStrRef.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using NWN.Core;
+
+namespace Anvil.API
+{
+ public sealed class NuiBindStrRef : NuiProperty
+ {
+ [JsonConstructor]
+ public NuiBindStrRef(string key)
+ {
+ Key = key;
+ }
+
+ [JsonProperty("bind")]
+ public string Key { get; init; }
+
+ ///
+ /// Queries the specified player for the value of this binding.
+ ///
+ /// The player to query.
+ /// The associated UI token.
+ /// The current value of the binding.
+ public StrRef? GetBindValue(NwPlayer player, int uiToken)
+ {
+ return JsonUtility.FromJson(NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key));
+ }
+
+ ///
+ /// Queries the specified player for the array of values assigned to this binding.
+ ///
+ /// The player to query.
+ /// The associated UI token.
+ /// The current values of the binding.
+ public List? GetBindValues(NwPlayer player, int uiToken)
+ {
+ return JsonUtility.FromJson>(NWScript.NuiGetBind(player.ControlledCreature, uiToken, Key));
+ }
+
+ ///
+ /// Assigns a value to the binding for the specified player.
+ ///
+ /// The player whose binding will be updated.
+ /// The unique UI token to be updated.
+ /// The new value to assign.
+ public void SetBindValue(NwPlayer player, int uiToken, StrRef? value)
+ {
+ NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, JsonUtility.ToJsonStructure(value));
+ }
+
+ ///
+ /// Assigns an array of values to the binding for the specified player.
+ ///
+ /// The player whose binding will be updated.
+ /// The unique UI token to be updated.
+ /// The new value to assign.
+ public void SetBindValues(NwPlayer player, int uiToken, IEnumerable values)
+ {
+ NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, JsonUtility.ToJsonStructure(values));
+ }
+
+ ///
+ /// Marks this property as watched/un-watched.
+ /// A watched bind will invoke the NUI script event every time its value changes.
+ ///
+ /// The player whose binding will be updated.
+ /// The unique UI token to be updated.
+ /// True if the value should be watched, false if it should not be watched.
+ public void SetBindWatch(NwPlayer player, int uiToken, bool watch)
+ {
+ NWScript.NuiSetBindWatch(player.ControlledCreature, uiToken, Key, watch.ToInt());
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRef.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRef.cs
new file mode 100644
index 000000000..b1fe98a6b
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRef.cs
@@ -0,0 +1,25 @@
+using Newtonsoft.Json;
+
+namespace Anvil.API
+{
+ [JsonConverter(typeof(NuiValueStrRefConverter))]
+ public sealed class NuiValueStrRef : NuiProperty
+ {
+ public NuiValueStrRef(StrRef? value)
+ {
+ Value = value;
+ }
+
+ internal NuiValueStrRef() {}
+
+ ///
+ /// Gets the value of this property.
+ ///
+ public StrRef? Value { get; init; }
+
+ public static implicit operator StrRef?(NuiValueStrRef? value)
+ {
+ return value?.Value;
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRefConverter.cs b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRefConverter.cs
new file mode 100644
index 000000000..77310b399
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Nui/Bindings/NuiValueStrRefConverter.cs
@@ -0,0 +1,26 @@
+using System;
+using Newtonsoft.Json;
+
+namespace Anvil.API
+{
+ public sealed class NuiValueStrRefConverter : JsonConverter
+ {
+ public override NuiValueStrRef? ReadJson(JsonReader reader, Type objectType, NuiValueStrRef? existingValue, bool hasExistingValue, JsonSerializer serializer)
+ {
+ StrRef? value = serializer.Deserialize(reader);
+ return value != null ? new NuiValueStrRef(value) : null;
+ }
+
+ public override void WriteJson(JsonWriter writer, NuiValueStrRef? value, JsonSerializer serializer)
+ {
+ if (value?.Value != null)
+ {
+ serializer.Serialize(writer, value.Value);
+ }
+ else
+ {
+ writer.WriteNull();
+ }
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Object/NwCreature.cs b/NWN.Anvil/src/main/API/Object/NwCreature.cs
index d03dd082c..4b07073ce 100644
--- a/NWN.Anvil/src/main/API/Object/NwCreature.cs
+++ b/NWN.Anvil/src/main/API/Object/NwCreature.cs
@@ -25,6 +25,9 @@ public sealed partial class NwCreature : NwGameObject
[Inject]
private static Lazy CreatureWalkRateCapService { get; set; } = null!;
+ [Inject]
+ private static Lazy InitiativeModifierService { get; set; } = null!;
+
[Inject]
private static Lazy DamageLevelOverrideService { get; set; } = null!;
@@ -454,6 +457,12 @@ public bool Immortal
///
public bool IsDMPossessed => NWScript.GetIsDMPossessed(this).ToBool();
+ ///
+ /// Gets a value indicating whether this creature is a DM avatar character.
+ /// This returns false for NPC creatures possessed by DMs.
+ ///
+ public bool IsDMAvatar => Creature.m_pStats.GetIsDM().ToBool();
+
///
/// Gets a value indicating whether this creature was spawned from an encounter.
///
@@ -1333,6 +1342,14 @@ public void ClearDamageLevelOverride()
DamageLevelOverrideService.Value.ClearDamageLevelOverride(this);
}
+ ///
+ /// Clears the modifier that is set for the creature's initiative.
+ ///
+ public void ClearInitiativeModifier()
+ {
+ InitiativeModifierService.Value.ClearInitiativeModifier(this);
+ }
+
public override NwCreature Clone(Location location, string? newTag = null, bool copyLocalState = true)
{
return CloneInternal(location, newTag, copyLocalState);
@@ -1508,7 +1525,7 @@ public int GetBaseSavingThrow(SavingThrow savingThrow)
///
/// The class with domains. Defaults to if not specified.
/// An enumeration of this creature's domains.
- public IEnumerable GetClassDomains(NwClass? nwClass = default)
+ public IEnumerable GetClassDomains(NwClass? nwClass = default)
{
nwClass ??= NwClass.FromClassType(ClassType.Cleric)!;
@@ -1520,7 +1537,7 @@ public IEnumerable GetClassDomains(NwClass? nwClass = default)
for (i = 1, current = NWScript.GetDomain(this, i, classT); current != error; i++, current = NWScript.GetDomain(this, i, classT))
{
- yield return (Domain)current;
+ yield return NwDomain.FromDomainId(current)!;
}
}
@@ -1589,6 +1606,14 @@ public byte GetFeatTotalUses(NwFeat feat)
return Creature.m_pStats.GetFeatTotalUses(feat.Id);
}
+ ///
+ /// Gets the modifier that is set for the creature's initiative.
+ ///
+ public int? GetInitiativeModifier()
+ {
+ return InitiativeModifierService.Value.GetInitiativeModifier(this);
+ }
+
///
/// Gets the item that is equipped in the specified inventory slot.
///
@@ -1903,6 +1928,16 @@ public bool IsEnemy(NwCreature target)
return NWScript.GetIsEnemy(target, this).ToBool();
}
+ ///
+ /// Gets if this creature is flanking the specified target.
+ ///
+ /// The target creature to check for flanking status.
+ /// True if the creature is flanking the target, otherwise false.
+ public bool IsFlanking(NwCreature target)
+ {
+ return Creature.GetFlanked(target.Creature).ToBool();
+ }
+
///
/// Gets a value indicating whether this creature considers the target as a enemy.
///
@@ -2214,6 +2249,14 @@ public void SetDamageLevelOverride(DamageLevelEntry damageLevel)
DamageLevelOverrideService.Value.SetDamageLevelOverride(this, damageLevel);
}
+ ///
+ /// Sets the modifier that is set for the creature's initiative.
+ ///
+ public void SetInitiativeModifier(int modifier)
+ {
+ InitiativeModifierService.Value.SetInitiativeModifier(this, modifier);
+ }
+
///
/// Sets the remaining uses available for the specified feat.
/// Cannot exceed the creature's total/max uses of the feat.
diff --git a/NWN.Anvil/src/main/API/Object/NwDoor.cs b/NWN.Anvil/src/main/API/Object/NwDoor.cs
index 6452dee8e..7177047f8 100644
--- a/NWN.Anvil/src/main/API/Object/NwDoor.cs
+++ b/NWN.Anvil/src/main/API/Object/NwDoor.cs
@@ -42,6 +42,17 @@ public override bool KeyAutoRemoved
set => Door.m_bAutoRemoveKey = value.ToInt();
}
+ ///
+ /// Gets or sets the open state for this door.
+ ///
+ /// Changing this property will not fire the door's OnOpen/OnClose event.
+ /// Use the OnDoorSetOpenState event to handle state changes to the door.
+ public DoorOpenState DoorOpenState
+ {
+ get => (DoorOpenState)Door.GetOpenState();
+ set => Door.SetOpenState((byte)value);
+ }
+
///
/// Creates a door at the specified location.
///
diff --git a/NWN.Anvil/src/main/API/Object/NwObject.cs b/NWN.Anvil/src/main/API/Object/NwObject.cs
index f49865161..5ad68d64b 100644
--- a/NWN.Anvil/src/main/API/Object/NwObject.cs
+++ b/NWN.Anvil/src/main/API/Object/NwObject.cs
@@ -260,7 +260,7 @@ public string GetEventScript(EventScriptType eventType)
public override int GetHashCode()
{
- return (int)ObjectId;
+ return unchecked((int)ObjectId);
}
///
diff --git a/NWN.Anvil/src/main/API/Object/NwPlayer.cs b/NWN.Anvil/src/main/API/Object/NwPlayer.cs
index e4f5575f5..d669f89fb 100644
--- a/NWN.Anvil/src/main/API/Object/NwPlayer.cs
+++ b/NWN.Anvil/src/main/API/Object/NwPlayer.cs
@@ -115,7 +115,7 @@ public float CutsceneCameraMoveRate
///
/// Gets a value indicating whether the player is a Dungeon Master.
///
- public bool IsDM => ControlledCreature?.Creature.m_pStats.GetIsDM().ToBool() == true;
+ public bool IsDM => player.GetIsDM().ToBool();
///
/// Gets if this player is in cursor targeting mode.
@@ -925,7 +925,7 @@ public int GetDeviceProperty(PlayerDeviceProperty property)
public override int GetHashCode()
{
- return Player.Pointer.GetHashCode();
+ return player.Pointer.GetHashCode();
}
///
diff --git a/NWN.Anvil/src/main/API/Ruleset/NwDomain.cs b/NWN.Anvil/src/main/API/Ruleset/NwDomain.cs
new file mode 100644
index 000000000..cb168976e
--- /dev/null
+++ b/NWN.Anvil/src/main/API/Ruleset/NwDomain.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Linq;
+using NWN.Native.API;
+
+namespace Anvil.API
+{
+ ///
+ /// A selectable domain type used by classes with the property (e.g. clerics)
+ ///
+ public sealed class NwDomain
+ {
+ private readonly CNWDomain domainInfo;
+
+ internal NwDomain(byte domainId, CNWDomain domainInfo)
+ {
+ Id = domainId;
+ this.domainInfo = domainInfo;
+ }
+
+ ///
+ /// Gets if the feat provides an active property, e.g. the Death domain's "Negative Plane Avatar" ability.
+ ///
+ public bool CastableFeat => domainInfo.m_bCastableFeat.ToBool();
+
+ ///
+ /// Gets the description for this domain, as shown in the character generation and level up screens.
+ ///
+ public StrRef Description => new StrRef(domainInfo.m_nDescriptionStrref);
+
+ ///
+ /// Gets the associated type for this domain.
+ ///
+ public Domain DomainType => (Domain)Id;
+
+ ///
+ /// Gets the feat that is granted when selecting this domain.
+ ///
+ public NwFeat? GrantedFeat => NwFeat.FromFeatId(domainInfo.m_nGrantedFeat);
+
+ ///
+ /// Gets the icon resref to use for this domain.
+ ///
+ public string Icon => domainInfo.m_cIcon.ToString();
+
+ ///
+ /// Gets the ID of this domain.
+ ///
+ public byte Id { get; }
+
+ ///
+ /// Gets if this is a correctly configured domain.
+ ///
+ public bool IsValidDomain => domainInfo.m_bValidDomain.ToBool();
+
+ ///
+ /// Gets the name of this domain.
+ ///
+ public StrRef Name => new StrRef(domainInfo.m_nNameStrref);
+
+ ///
+ /// Gets a list of spells specific to this domain.
+ ///
+ ///
+ /// This property returns a fixed size list of 10, matching the number of spell levels (cantrips + 9 levels)
+ /// Levels that do not grant a spell for a domain return a null in that slot.
+ ///
+ public IReadOnlyList Spells => domainInfo.m_lstSpells.Select(spellId => NwSpell.FromSpellId((int)spellId)).ToArray();
+
+ ///
+ /// Resolves a from a .
+ ///
+ /// The domain type to resolve.
+ /// The associated instance. Null if the domain type is invalid.
+ public static NwDomain? FromDomainType(Domain domainType)
+ {
+ return NwRuleset.Domains.ElementAtOrDefault((int)domainType);
+ }
+
+ public static implicit operator NwDomain?(Domain domainType)
+ {
+ return NwRuleset.Domains.ElementAtOrDefault((int)domainType);
+ }
+
+ ///
+ /// Resolves a from a domain id.
+ ///
+ /// The id of the domain to resolve.
+ /// The associated instance. Null if the domain id is invalid.
+ public static NwDomain? FromDomainId(int domainId)
+ {
+ return NwRuleset.Domains.ElementAtOrDefault(domainId);
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/API/Ruleset/NwRuleset.cs b/NWN.Anvil/src/main/API/Ruleset/NwRuleset.cs
index fc0a28d64..b494ef527 100644
--- a/NWN.Anvil/src/main/API/Ruleset/NwRuleset.cs
+++ b/NWN.Anvil/src/main/API/Ruleset/NwRuleset.cs
@@ -21,6 +21,11 @@ public static class NwRuleset
///
public static IReadOnlyList Classes { get; private set; } = null!;
+ ///
+ /// Gets a list of all domains defined in the module's ruleset.
+ ///
+ public static IReadOnlyList Domains { get; private set; } = null!;
+
///
/// Gets a list of all feats defined in the module's ruleset.
///
@@ -125,6 +130,7 @@ private static void LoadRules()
Feats = LoadFeats(CNWFeatArray.FromPointer(rules.m_lstFeats), rules.m_nNumFeats);
BaseItems = LoadBaseItems(rules.m_pBaseItemArray);
Spells = LoadSpells(rules.m_pSpellArray);
+ Domains = LoadDomains(CNWDomainArray.FromPointer(rules.m_lstDomains), rules.m_nNumDomains);
}
private static IReadOnlyList LoadSkills(CNWSkillArray skillArray, int count)
@@ -149,6 +155,17 @@ private static IReadOnlyList LoadSpells(CNWSpellArray spellArray)
return retVal;
}
+ private static IReadOnlyList LoadDomains(CNWDomainArray domainArray, int count)
+ {
+ NwDomain[] retVal = new NwDomain[count];
+ for (int i = 0; i < retVal.Length; i++)
+ {
+ retVal[i] = new NwDomain((byte)i, domainArray.GetItem(i));
+ }
+
+ return retVal;
+ }
+
private void OnReloadAll(void* pRules)
{
if (reloadAllHook != null)
diff --git a/NWN.Anvil/src/main/API/Server/NwServer.cs b/NWN.Anvil/src/main/API/Server/NwServer.cs
index 22f92a76e..8625cf403 100644
--- a/NWN.Anvil/src/main/API/Server/NwServer.cs
+++ b/NWN.Anvil/src/main/API/Server/NwServer.cs
@@ -32,6 +32,20 @@ public string DMPassword
set => netLayer.SetGameMasterPassword(new CExoString(value));
}
+ ///
+ /// Gets or sets if the server is "active" paused - same as if a player requested pause.
+ ///
+ public bool IsActivePaused
+ {
+ get => server.GetPauseState(2).ToBool();
+ set => server.SetPauseState(2, value.ToInt());
+ }
+
+ ///
+ /// Gets if the server is paused as a result of .
+ ///
+ public bool IsTimestopPaused => server.GetPauseState(1).ToBool();
+
///
/// Gets or sets the current player password.
///
diff --git a/NWN.Anvil/src/main/API/Tlk/StrRef.cs b/NWN.Anvil/src/main/API/Tlk/StrRef.cs
index 1b301b57b..12fa28159 100644
--- a/NWN.Anvil/src/main/API/Tlk/StrRef.cs
+++ b/NWN.Anvil/src/main/API/Tlk/StrRef.cs
@@ -1,5 +1,6 @@
using Anvil.Internal;
using Anvil.Services;
+using Newtonsoft.Json;
using NWN.Native.API;
namespace Anvil.API
@@ -17,8 +18,10 @@ public readonly struct StrRef
///
/// Gets the index/key for this StrRef.
///
+ [JsonProperty("strref")]
public readonly uint Id;
+ [JsonConstructor]
public StrRef(uint stringId)
{
Id = stringId;
@@ -29,11 +32,13 @@ public StrRef(int stringId) : this((uint)stringId) {}
///
/// Gets the index/key for this StrRef relative to the module's custom talk table. (-16777216)
///
+ [JsonIgnore]
public uint CustomId => Id - CustomTlkOffset;
///
/// Gets or sets a string override that this StrRef should return instead of the tlk file definition.
///
+ [JsonIgnore]
public string? Override
{
get => TlkTable.GetTlkOverride(this);
diff --git a/NWN.Anvil/src/main/API/Variable/InternalVariables.cs b/NWN.Anvil/src/main/API/Variable/InternalVariables.cs
index 758e603ed..e7f3908db 100644
--- a/NWN.Anvil/src/main/API/Variable/InternalVariables.cs
+++ b/NWN.Anvil/src/main/API/Variable/InternalVariables.cs
@@ -11,6 +11,7 @@ namespace Anvil.API
internal static class InternalVariables
{
public static InternalVariableBool AlwaysWalk(NwObject creature) => creature.GetObjectVariable("ALWAYS_WALK");
+ public static InternalVariableInt InitiativeMod(NwObject creature) => creature.GetObjectVariable("INITIATIVE_MOD");
public static InternalVariableInt DamageLevelOverride(NwCreature creature) => creature.GetObjectVariable("DAMAGE_LEVEL");
public static InternalVariableEnum GlobalVisibilityOverride(NwObject gameObject) => gameObject.GetObjectVariable>("VISIBILITY_OVERRIDE");
public static InternalVariableEnum PlayerVisibilityOverride(NwPlayer player, NwObject targetGameObject) => player.ControlledCreature!.GetObjectVariable>("VISIBILITY_OVERRIDE" + targetGameObject.ObjectId);
diff --git a/NWN.Anvil/src/main/Internal/EnvironmentConfig.cs b/NWN.Anvil/src/main/Internal/EnvironmentConfig.cs
index 3fb04f9d4..f56653575 100644
--- a/NWN.Anvil/src/main/Internal/EnvironmentConfig.cs
+++ b/NWN.Anvil/src/main/Internal/EnvironmentConfig.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using Anvil.Services;
namespace Anvil.Internal
@@ -10,7 +11,8 @@ public static class EnvironmentConfig
{
private static readonly string[] VariablePrefixes = { "ANVIL_", "NWM_" };
- public static readonly string AnvilHome = GetAnvilVariableString("HOME", "./anvil")!;
+ public static readonly string AnvilHome = GetAnvilVariableString("HOME", "./anvil");
+ public static readonly string Encoding = GetAnvilVariableString("ENCODING", "windows-1252");
public static readonly LogMode LogMode = GetAnvilVariableEnum("LOG_MODE", LogMode.Default);
public static readonly bool NativePrelinkEnabled = GetAnvilVariableBool("PRELINK_ENABLED", true);
public static readonly bool PreventStartNoPlugin = GetAnvilVariableBool("PREVENT_START_NO_PLUGIN");
@@ -24,16 +26,17 @@ static EnvironmentConfig()
private static bool GetAnvilVariableBool(string key, bool defaultValue = false)
{
- string? value = GetAnvilVariableString(key, defaultValue.ToString());
- return value!.Equals("true", StringComparison.OrdinalIgnoreCase);
+ string value = GetAnvilVariableString(key, defaultValue.ToString());
+ return value.Equals("true", StringComparison.OrdinalIgnoreCase);
}
private static T GetAnvilVariableEnum(string key, T defaultValue = default) where T : struct, Enum
{
- string? value = GetAnvilVariableString(key, defaultValue.ToString());
+ string value = GetAnvilVariableString(key, defaultValue.ToString());
return Enum.TryParse(value, out T result) ? result : defaultValue;
}
+ [return: NotNullIfNotNull("defaultValue")]
private static string? GetAnvilVariableString(string key, string? defaultValue = null)
{
foreach (string prefix in VariablePrefixes)
diff --git a/NWN.Anvil/src/main/Services/API/Creature/InitiativeModifierService.cs b/NWN.Anvil/src/main/Services/API/Creature/InitiativeModifierService.cs
new file mode 100644
index 000000000..805d1c93f
--- /dev/null
+++ b/NWN.Anvil/src/main/Services/API/Creature/InitiativeModifierService.cs
@@ -0,0 +1,117 @@
+using Anvil.API;
+using Anvil.Internal;
+using NWN.Native.API;
+using Feat = Anvil.API.Feat;
+
+namespace Anvil.Services
+{
+ [ServiceBinding(typeof(InitiativeModifierService))]
+ [ServiceBindingOptions(InternalBindingPriority.API, Lazy = true)]
+ public sealed unsafe class InitiativeModifierService
+ {
+ private readonly FunctionHook initiativeModifierHook;
+
+ public InitiativeModifierService(HookService hookService)
+ {
+ initiativeModifierHook = hookService.RequestHook(OnResolveInitiative, FunctionsLinux._ZN12CNWSCreature17ResolveInitiativeEv, HookOrder.Late);
+ }
+
+ private delegate void InitiativeModifier(void* pObject);
+
+ ///
+ /// Clears any modifier that is set for the creature's initiative.
+ ///
+ public void ClearInitiativeModifier(NwCreature creature)
+ {
+ InternalVariables.InitiativeMod(creature).Delete();
+ }
+
+ ///
+ /// Gets the modifier that is set for the creature's initiative.
+ ///
+ public int? GetInitiativeModifier(NwCreature creature)
+ {
+ InternalVariableInt initiativeModifier = InternalVariables.InitiativeMod(creature);
+ if (initiativeModifier.HasValue)
+ {
+ return initiativeModifier.Value;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Sets the modifier that is set for the creature's initiative.
+ ///
+ public void SetInitiativeModifier(NwCreature creature, int mod)
+ {
+ InternalVariables.InitiativeMod(creature).Value = mod;
+ }
+
+ private void OnResolveInitiative(void* pCreature)
+ {
+ NwCreature? creature = CNWSCreature.FromPointer(pCreature).ToNwObject();
+ if (creature == null)
+ {
+ initiativeModifierHook.CallOriginal(pCreature);
+ return;
+ }
+
+ InternalVariableInt initMod = InternalVariables.InitiativeMod(creature);
+ if (initMod.HasNothing)
+ {
+ initiativeModifierHook.CallOriginal(pCreature);
+ return;
+ }
+
+ CNWSCreature cCreature = creature.Creature;
+
+ if (cCreature.m_bInitiativeExpired.ToBool())
+ {
+ CNWSCreatureStats? pStats = cCreature.m_pStats;
+ CNWRules rules = NWNXLib.Rules()!;
+
+ ushort diceRoll = rules.RollDice(1, 20);
+ int mod = pStats.GetDEXMod(0);
+ if (pStats.HasFeat((ushort)Feat.EpicSuperiorInitiative).ToBool())
+ {
+ mod += rules.GetRulesetIntEntry("EPIC_SUPERIOR_INITIATIVE_BONUS".ToExoString(), 8);
+ }
+
+ else if (pStats.HasFeat((ushort)Feat.ImprovedInitiative).ToBool())
+ {
+ mod += rules.GetRulesetIntEntry("IMPROVED_INITIATIVE_BONUS".ToExoString(), 4);
+ }
+
+ if (pStats.HasFeat((ushort)Feat.Blooded).ToBool())
+ {
+ mod += rules.GetRulesetIntEntry("BLOODED_INITIATIVE_BONUS".ToExoString(), 2);
+ }
+
+ if (pStats.HasFeat((ushort)Feat.Thug).ToBool())
+ {
+ mod += rules.GetRulesetIntEntry("THUG_INITIATIVE_BONUS".ToExoString(), 2);
+ }
+
+ // Add creature bonus
+ mod += initMod.Value;
+
+ cCreature.m_nInitiativeRoll = unchecked((byte)(diceRoll + mod));
+ if (creature.IsPlayerControlled(out NwPlayer? player))
+ {
+ CNWCCMessageData messageData = new CNWCCMessageData();
+ messageData.SetObjectID(0, cCreature.m_idSelf);
+ messageData.SetInteger(0, diceRoll);
+ messageData.SetInteger(1, mod);
+ CNWSMessage? message = LowLevel.ServerExoApp.GetNWSMessage();
+ if (message != null)
+ {
+ message.SendServerToPlayerCCMessage(player.Player.m_nPlayerID, (byte)MessageClientSideMsgMinor.Initiative, messageData, null);
+ }
+ }
+
+ cCreature.m_bInitiativeExpired = false.ToInt();
+ }
+ }
+ }
+}
diff --git a/NWN.Anvil/src/main/Services/Core/EncodingService.cs b/NWN.Anvil/src/main/Services/Core/EncodingService.cs
new file mode 100644
index 000000000..6d1cabd74
--- /dev/null
+++ b/NWN.Anvil/src/main/Services/Core/EncodingService.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Text;
+using Anvil.Internal;
+using NWN.Core;
+using NWN.Native.API;
+
+namespace Anvil.Services
+{
+ ///
+ /// Manages string conversion for NWN.Core, NWN.Native and the StringHelper utility class.
+ ///
+ public sealed class EncodingService : ICoreService
+ {
+ ///
+ /// Gets or sets the encoding used by anvil to convert strings.
+ ///
+ public Encoding Encoding
+ {
+ get => NWNCore.Encoding;
+ set
+ {
+ if (value == null)
+ {
+ throw new NullReferenceException("Encoding must not be null.");
+ }
+
+ NWNCore.Encoding = value;
+ StringHelper.Encoding = value;
+ }
+ }
+
+ void ICoreService.Init()
+ {
+ Encoding = Encoding.GetEncoding(EnvironmentConfig.Encoding);
+ }
+
+ void ICoreService.Load() {}
+
+ void ICoreService.Shutdown() {}
+
+ void ICoreService.Start() {}
+
+ void ICoreService.Unload() {}
+ }
+}
diff --git a/NWN.Anvil/src/main/Services/Resources/ResourceManager.cs b/NWN.Anvil/src/main/Services/Resources/ResourceManager.cs
index f554e6bf4..05fd784b7 100644
--- a/NWN.Anvil/src/main/Services/Resources/ResourceManager.cs
+++ b/NWN.Anvil/src/main/Services/Resources/ResourceManager.cs
@@ -150,7 +150,7 @@ public IEnumerable FindResourcesOfType(ResRefType type, bool moduleOnly
{
case ResRefType.NSS:
string? source = GetNSSContents(name.ToExoString());
- return source != null ? StringHelper.Cp1252Encoding.GetBytes(source) : null;
+ return source != null ? StringHelper.Encoding.GetBytes(source) : null;
case ResRefType.NCS:
return null;
default:
@@ -174,7 +174,7 @@ public IEnumerable FindResourcesOfType(ResRefType type, bool moduleOnly
return null;
default:
byte[]? data = GetStandardResourceData(name, type);
- return data != null ? StringHelper.Cp1252Encoding.GetString(data) : null;
+ return data != null ? StringHelper.Encoding.GetString(data) : null;
}
}
@@ -208,7 +208,7 @@ public void WriteTempResource(string resourceName, byte[] data)
/// The text to populate in the resource.
public void WriteTempResource(string resourceName, string text)
{
- WriteTempResource(resourceName, StringHelper.Cp1252Encoding.GetBytes(text));
+ WriteTempResource(resourceName, StringHelper.Encoding.GetBytes(text));
}
void IDisposable.Dispose()
diff --git a/NWN.Anvil/src/main/Services/Resources/ResourceNameGenerator.cs b/NWN.Anvil/src/main/Services/Resources/ResourceNameGenerator.cs
index 2f336f1a3..668cdc65d 100644
--- a/NWN.Anvil/src/main/Services/Resources/ResourceNameGenerator.cs
+++ b/NWN.Anvil/src/main/Services/Resources/ResourceNameGenerator.cs
@@ -8,7 +8,6 @@ internal static class ResourceNameGenerator
{
private const string ValidChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- private static readonly Random Random = new Random();
private static readonly StringBuilder StringBuilder = new StringBuilder(ScriptConstants.MaxScriptNameSize);
public static string Create()
@@ -16,7 +15,7 @@ public static string Create()
StringBuilder.Clear();
for (int i = 0; i < ScriptConstants.MaxScriptNameSize; i++)
{
- StringBuilder.Append(ValidChars[Random.Next(ValidChars.Length)]);
+ StringBuilder.Append(ValidChars[Random.Shared.Next(ValidChars.Length)]);
}
return StringBuilder.ToString();
diff --git a/NWN.Anvil/src/main/Services/Services/AnvilServiceManager.cs b/NWN.Anvil/src/main/Services/Services/AnvilServiceManager.cs
index b17372c46..63244025b 100644
--- a/NWN.Anvil/src/main/Services/Services/AnvilServiceManager.cs
+++ b/NWN.Anvil/src/main/Services/Services/AnvilServiceManager.cs
@@ -230,6 +230,7 @@ private void InstallCoreContainer()
RegisterCoreService();
RegisterCoreService();
RegisterCoreService();
+ RegisterCoreService();
}
private void RegisterCoreService() where T : ICoreService
diff --git a/docs/NWN.Anvil.Samples.csproj b/docs/NWN.Anvil.Samples.csproj
index b5fe9fdf8..3c7c6b7c6 100644
--- a/docs/NWN.Anvil.Samples.csproj
+++ b/docs/NWN.Anvil.Samples.csproj
@@ -30,7 +30,7 @@
-
+