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