From dc0ed1b5db619a99973c65f6a616fdbc97bc195e Mon Sep 17 00:00:00 2001 From: Jorteck Date: Mon, 13 Sep 2021 23:35:56 +0200 Subject: [PATCH 01/16] Refactor server lifecycle handling. Fix ObjectStorage breaking hot reload (#330) * Refactor server lifecycle handling. Use "ON_DESTROY_SERVER_AFTER" signal to prevent hooks from being disposed too early. * Fix analysis checks. * Remove dtor. * Update to 8193.30 * Fix compile errors. * Ensure validation event is run in a script context. * Cleanup. * Update NWN.Core to 8193.30.7. * Fix analysis warnings. Co-authored-by: urothis --- NWN.Anvil.csproj | 4 +- NWN.Anvil.csproj.DotSettings | 1 + .../Native/ClientEvents/OnClientConnect.cs | 2 +- .../Anvil/API/Object/CreatureLevelInfo.cs | 4 +- src/main/Anvil/API/Object/NwArea.cs | 20 +++--- src/main/Anvil/API/Object/NwCreature.cs | 35 +++++----- src/main/Anvil/API/Object/NwFaction.cs | 6 +- src/main/Anvil/API/Object/NwItem.cs | 2 +- src/main/Anvil/API/Object/NwPlaceable.cs | 2 +- src/main/Anvil/API/Object/NwPlayer.cs | 28 ++++---- src/main/Anvil/API/Object/NwStore.cs | 11 ++-- src/main/Anvil/API/Scripts/ScriptParams.cs | 10 +-- src/main/Anvil/API/Utils/VirtualMachine.cs | 24 ++++++- src/main/Anvil/AnvilCore.cs | 64 ++++++++++--------- src/main/Anvil/Internal/CoreInteropHandler.cs | 27 ++++---- src/main/Anvil/Internal/ICoreSignalHandler.cs | 9 --- .../Internal/IServerLifeCycleEventHandler.cs | 9 +++ src/main/Anvil/Plugins/ITypeLoader.cs | 4 +- src/main/Anvil/Plugins/PluginLoader.cs | 3 +- .../ELC/EnforceLegalCharacterService.cs | 41 +++++++----- .../Anvil/Services/Hooking/HookService.cs | 20 ++---- .../ObjectStorage/ObjectStorageService.cs | 31 ++++----- .../Services/AnvilContainerFactory.cs | 39 +++++++---- .../{ => LifeCycle}/IInitializable.cs | 0 .../Services/LifeCycle/ILateDisposable.cs | 15 +++++ .../Services/LifeCycle/LifeCycleEvent.cs | 10 +++ .../Anvil/Services/Services/ServiceManager.cs | 50 ++++++++++----- 27 files changed, 275 insertions(+), 196 deletions(-) delete mode 100644 src/main/Anvil/Internal/ICoreSignalHandler.cs create mode 100644 src/main/Anvil/Internal/IServerLifeCycleEventHandler.cs rename src/main/Anvil/Services/Services/{ => LifeCycle}/IInitializable.cs (100%) create mode 100644 src/main/Anvil/Services/Services/LifeCycle/ILateDisposable.cs create mode 100644 src/main/Anvil/Services/Services/LifeCycle/LifeCycleEvent.cs diff --git a/NWN.Anvil.csproj b/NWN.Anvil.csproj index 3f780f944..5ead3bbbd 100644 --- a/NWN.Anvil.csproj +++ b/NWN.Anvil.csproj @@ -38,8 +38,8 @@ - - + + diff --git a/NWN.Anvil.csproj.DotSettings b/NWN.Anvil.csproj.DotSettings index 0413aa056..9a02301d4 100644 --- a/NWN.Anvil.csproj.DotSettings +++ b/NWN.Anvil.csproj.DotSettings @@ -24,6 +24,7 @@ True True True + True True True True diff --git a/src/main/Anvil/API/Events/Native/ClientEvents/OnClientConnect.cs b/src/main/Anvil/API/Events/Native/ClientEvents/OnClientConnect.cs index 4a60edfa8..2d12ff28d 100644 --- a/src/main/Anvil/API/Events/Native/ClientEvents/OnClientConnect.cs +++ b/src/main/Anvil/API/Events/Native/ClientEvents/OnClientConnect.cs @@ -72,7 +72,7 @@ private static int OnSendServerToPlayerCharList(void* pMessage, void* pPlayer) OnClientConnect eventData = ProcessEvent(new OnClientConnect { PlayerName = playerInfo.m_sPlayerName.ToString(), - CDKey = playerInfo.m_lstKeys._OpIndex(0).ToString(), + CDKey = playerInfo.m_lstKeys[0].ToString(), DM = playerInfo.m_bGameMasterPrivileges.ToBool(), IP = ipAddress, }); diff --git a/src/main/Anvil/API/Object/CreatureLevelInfo.cs b/src/main/Anvil/API/Object/CreatureLevelInfo.cs index b4168323b..86d8d3264 100644 --- a/src/main/Anvil/API/Object/CreatureLevelInfo.cs +++ b/src/main/Anvil/API/Object/CreatureLevelInfo.cs @@ -25,7 +25,7 @@ public IReadOnlyList Feats for (int i = 0; i < feats.Length; i++) { - feats[i] = (Feat)levelStats.m_lstFeats.element[i]; + feats[i] = (Feat)levelStats.m_lstFeats[i]; } return feats; @@ -37,7 +37,7 @@ public IReadOnlyList Feats /// public int FeatCount { - get => levelStats.m_lstFeats.num; + get => levelStats.m_lstFeats.Count; } /// diff --git a/src/main/Anvil/API/Object/NwArea.cs b/src/main/Anvil/API/Object/NwArea.cs index ba7ca870e..bd6468c30 100644 --- a/src/main/Anvil/API/Object/NwArea.cs +++ b/src/main/Anvil/API/Object/NwArea.cs @@ -421,16 +421,16 @@ public byte[] SerializeGIT(ObjectTypes objectFilter = ObjectTypes.All, ICollecti { return NativeUtils.SerializeGff("GIT", "V3.2", (resGff, resStruct) => { - CExoArrayListObjectId creatures = new CExoArrayListObjectId(); - CExoArrayListObjectId items = new CExoArrayListObjectId(); - CExoArrayListObjectId doors = new CExoArrayListObjectId(); - CExoArrayListObjectId triggers = new CExoArrayListObjectId(); - CExoArrayListObjectId encounters = new CExoArrayListObjectId(); - CExoArrayListObjectId waypoints = new CExoArrayListObjectId(); - CExoArrayListObjectId sounds = new CExoArrayListObjectId(); - CExoArrayListObjectId placeables = new CExoArrayListObjectId(); - CExoArrayListObjectId stores = new CExoArrayListObjectId(); - CExoArrayListObjectId aoes = new CExoArrayListObjectId(); + CExoArrayListUInt32 creatures = new CExoArrayListUInt32(); + CExoArrayListUInt32 items = new CExoArrayListUInt32(); + CExoArrayListUInt32 doors = new CExoArrayListUInt32(); + CExoArrayListUInt32 triggers = new CExoArrayListUInt32(); + CExoArrayListUInt32 encounters = new CExoArrayListUInt32(); + CExoArrayListUInt32 waypoints = new CExoArrayListUInt32(); + CExoArrayListUInt32 sounds = new CExoArrayListUInt32(); + CExoArrayListUInt32 placeables = new CExoArrayListUInt32(); + CExoArrayListUInt32 stores = new CExoArrayListUInt32(); + CExoArrayListUInt32 aoes = new CExoArrayListUInt32(); foreach (NwGameObject gameObject in Objects) { diff --git a/src/main/Anvil/API/Object/NwCreature.cs b/src/main/Anvil/API/Object/NwCreature.cs index 5ee6358f5..bc686506e 100644 --- a/src/main/Anvil/API/Object/NwCreature.cs +++ b/src/main/Anvil/API/Object/NwCreature.cs @@ -828,16 +828,16 @@ public IReadOnlyList ClassInfo /// /// Gets an enumerable containing information about this creature's levels (feats, skills, class taken, etc). /// - public unsafe IReadOnlyList LevelInfo + public IReadOnlyList LevelInfo { get { - int statCount = Creature.m_pStats.m_lstLevelStats.num; + int statCount = Creature.m_pStats.m_lstLevelStats.Count; List retVal = new List(statCount); for (int i = 0; i < statCount; i++) { - CNWLevelStats levelStats = CNWLevelStats.FromPointer(*Creature.m_pStats.m_lstLevelStats._OpIndex(i)); + CNWLevelStats levelStats = Creature.m_pStats.m_lstLevelStats[i]; retVal.Add(new CreatureLevelInfo(this, levelStats)); } @@ -848,7 +848,7 @@ public unsafe IReadOnlyList LevelInfo /// /// Gets the feats known by this character. /// - public unsafe IReadOnlyList Feats + public IReadOnlyList Feats { get { @@ -856,7 +856,7 @@ public unsafe IReadOnlyList Feats for (int i = 0; i < feats.Length; i++) { - feats[i] = (Feat)Creature.m_pStats.m_lstFeats.element[i]; + feats[i] = (Feat)Creature.m_pStats.m_lstFeats[i]; } return feats; @@ -868,7 +868,7 @@ public unsafe IReadOnlyList Feats /// public int FeatCount { - get => Creature.m_pStats.m_lstFeats.num; + get => Creature.m_pStats.m_lstFeats.Count; } /// @@ -1963,14 +1963,14 @@ public void AddFeat(Feat feat) /// /// The feat to give. /// The level the feat was gained. - public unsafe void AddFeat(Feat feat, int level) + public void AddFeat(Feat feat, int level) { - if (level == 0 || level > Creature.m_pStats.m_lstLevelStats.num) + if (level == 0 || level > Creature.m_pStats.m_lstLevelStats.Count) { throw new ArgumentOutOfRangeException(nameof(level), "Level must be from 1 to the creature's max level."); } - CNWLevelStats levelStats = CNWLevelStats.FromPointer(*Creature.m_pStats.m_lstLevelStats._OpIndex(level - 1)); + CNWLevelStats levelStats = Creature.m_pStats.m_lstLevelStats[level - 1]; levelStats.AddFeat((ushort)feat); Creature.m_pStats.AddFeat((ushort)feat); @@ -2030,9 +2030,8 @@ public IReadOnlyList SpecialAbilities List retVal = new List(); CExoArrayListCNWSStatsSpellLikeAbility specialAbilities = Creature.m_pStats.m_pSpellLikeAbilityList; - for (int i = 0; i < specialAbilities.num; i++) + foreach (CNWSStats_SpellLikeAbility ability in specialAbilities) { - CNWSStats_SpellLikeAbility ability = specialAbilities._OpIndex(i); if (ability.m_nSpellId != ~0u) { retVal.Add(new SpecialAbility((Spell)ability.m_nSpellId, ability.m_nCasterLevel, ability.m_bReadied.ToBool())); @@ -2065,9 +2064,9 @@ public void AddSpecialAbility(SpecialAbility ability) public void RemoveSpecialAbilityAt(int index) { CExoArrayListCNWSStatsSpellLikeAbility specialAbilities = Creature.m_pStats.m_pSpellLikeAbilityList; - if (index < specialAbilities.num) + if (index < specialAbilities.Count) { - specialAbilities._OpIndex(index).m_nSpellId = ~0u; + specialAbilities[index].m_nSpellId = ~0u; } } @@ -2079,9 +2078,9 @@ public void RemoveSpecialAbilityAt(int index) public void SetSpecialAbilityAt(int index, SpecialAbility ability) { CExoArrayListCNWSStatsSpellLikeAbility specialAbilities = Creature.m_pStats.m_pSpellLikeAbilityList; - if (index < specialAbilities.num) + if (index < specialAbilities.Count) { - CNWSStats_SpellLikeAbility specialAbility = specialAbilities._OpIndex(index); + CNWSStats_SpellLikeAbility specialAbility = specialAbilities[index]; specialAbility.m_nSpellId = (uint)ability.Spell; specialAbility.m_bReadied = ability.Ready.ToInt(); specialAbility.m_nCasterLevel = ability.CasterLevel; @@ -2187,14 +2186,14 @@ public void RestoreAllSpells() /// /// The level to lookup. /// A object containing level info. - public unsafe CreatureLevelInfo GetLevelStats(int level) + public CreatureLevelInfo GetLevelStats(int level) { - if (level == 0 || level > Creature.m_pStats.m_lstLevelStats.num) + if (level == 0 || level > Creature.m_pStats.m_lstLevelStats.Count) { throw new ArgumentOutOfRangeException(nameof(level), "Level must be from 1 to the creature's max level."); } - CNWLevelStats levelStats = CNWLevelStats.FromPointer(*Creature.m_pStats.m_lstLevelStats._OpIndex(level - 1)); + CNWLevelStats levelStats = Creature.m_pStats.m_lstLevelStats[level - 1]; return new CreatureLevelInfo(this, levelStats); } diff --git a/src/main/Anvil/API/Object/NwFaction.cs b/src/main/Anvil/API/Object/NwFaction.cs index f0d79fb44..bd1b246e6 100644 --- a/src/main/Anvil/API/Object/NwFaction.cs +++ b/src/main/Anvil/API/Object/NwFaction.cs @@ -95,13 +95,13 @@ public NwPlayer Leader /// @note This can be a very costly operation when used on large NPC factions. /// /// All creatures in this faction. - public unsafe List GetMembers() + public List GetMembers() { List members = new List(); - for (int i = 0; i < faction.m_listFactionMembers.num; i++) + for (int i = 0; i < faction.m_listFactionMembers.Count; i++) { - NwCreature member = (*faction.m_listFactionMembers._OpIndex(i)).ToNwObjectSafe(); + NwCreature member = faction.m_listFactionMembers[i].ToNwObjectSafe(); if (member != null) { members.Add(member); diff --git a/src/main/Anvil/API/Object/NwItem.cs b/src/main/Anvil/API/Object/NwItem.cs index df8f9ac13..45abfd501 100644 --- a/src/main/Anvil/API/Object/NwItem.cs +++ b/src/main/Anvil/API/Object/NwItem.cs @@ -412,7 +412,7 @@ public static NwItem Deserialize(byte[] serialized) } item = new CNWSItem(Invalid); - if (item.LoadItem(resGff, resStruct).ToBool()) + if (item.LoadItem(resGff, resStruct, false.ToInt()).ToBool()) { GC.SuppressFinalize(item); return true; diff --git a/src/main/Anvil/API/Object/NwPlaceable.cs b/src/main/Anvil/API/Object/NwPlaceable.cs index 6113a116a..eef742269 100644 --- a/src/main/Anvil/API/Object/NwPlaceable.cs +++ b/src/main/Anvil/API/Object/NwPlaceable.cs @@ -231,7 +231,7 @@ public static NwPlaceable Deserialize(byte[] serialized) } placeable = new CNWSPlaceable(Invalid); - if (placeable.LoadPlaceable(resGff, resStruct, null).ToBool()) + if (placeable.LoadPlaceable(resGff, resStruct, false.ToInt(), null).ToBool()) { placeable.LoadObjectState(resGff, resStruct); GC.SuppressFinalize(placeable); diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index 12e38ff06..8c84eb852 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -950,9 +950,9 @@ public JournalEntry GetJournalEntry(string questTag) CNWSJournal journal = creature.Creature.GetJournal(); CExoArrayListSJournalEntry entries = journal.m_lstEntries; - for (int i = entries.num - 1; i >= 0; i--) + for (int i = entries.Count - 1; i >= 0; i--) { - SJournalEntry entry = entries._OpIndex(i); + SJournalEntry entry = entries[i]; if (entry.szPlot_Id.ToString() == questTag) { return new JournalEntry @@ -1015,15 +1015,15 @@ public int AddCustomJournalEntry(JournalEntry entryData, bool silentUpdate = fal }; int overwrite = -1; - if (entries.num > 0) + if (entries.Count > 0) { - for (int i = entries.num - 1; i >= 0; i--) + for (int i = entries.Count - 1; i >= 0; i--) { - SJournalEntry entry = entries._OpIndex(i); + SJournalEntry entry = entries[i]; if (entry.szPlot_Id.ToString() == entryData.QuestTag) { - entries.DelIndex(i); - entries.Insert(entry, i); + entries.RemoveAt(i); + entries.Insert(i, entry); overwrite = i; break; } @@ -1035,7 +1035,7 @@ public int AddCustomJournalEntry(JournalEntry entryData, bool silentUpdate = fal journal.m_lstEntries.Add(newJournal); } - retVal = journal.m_lstEntries.num; + retVal = journal.m_lstEntries.Count; message.SendServerToPlayerJournalAddQuest(Player, newJournal.szPlot_Id, @@ -1069,10 +1069,10 @@ public unsafe byte[] GetAreaExplorationState(NwArea area) return null; } - uint* oidArea = creature.Creature.m_oidAutoMapAreaList.element; - for (int i = 0; i < creature.Creature.m_oidAutoMapAreaList.num; i++, oidArea++) + for (int i = 0; i < creature.Creature.m_oidAutoMapAreaList.Count; i++) { - if (*oidArea != area) + uint oidArea = creature.Creature.m_oidAutoMapAreaList[i]; + if (oidArea != area) { continue; } @@ -1104,10 +1104,10 @@ public unsafe void SetAreaExplorationState(NwArea area, byte[] newState) return; } - uint* oidArea = creature.Creature.m_oidAutoMapAreaList.element; - for (int i = 0; i < creature.Creature.m_oidAutoMapAreaList.num; i++, oidArea++) + for (int i = 0; i < creature.Creature.m_oidAutoMapAreaList.Count; i++) { - if (*oidArea != area) + uint oidArea = creature.Creature.m_oidAutoMapAreaList[i]; + if (oidArea != area) { continue; } diff --git a/src/main/Anvil/API/Object/NwStore.cs b/src/main/Anvil/API/Object/NwStore.cs index 106afbd96..33fb028de 100644 --- a/src/main/Anvil/API/Object/NwStore.cs +++ b/src/main/Anvil/API/Object/NwStore.cs @@ -62,23 +62,22 @@ public IEnumerable Items /// public int CustomerCount { - get => Store.m_aCurrentCustomers.num; + get => Store.m_aCurrentCustomers.Count; } /// /// Gets the current customers of this store. /// - public unsafe IReadOnlyList CurrentCustomers + public IReadOnlyList CurrentCustomers { get { List customers = new List(); CExoArrayListCStoreCustomerPtr customersPtr = Store.m_aCurrentCustomers; - for (int i = 0; i < customersPtr.num; i++) + foreach (CStoreCustomer storeCustomer in customersPtr) { - void** customerPtr = customersPtr._OpIndex(i); - NwCreature customer = CStoreCustomer.FromPointer(*customerPtr).m_oidObject.ToNwObjectSafe(); + NwCreature customer = storeCustomer.m_oidObject.ToNwObjectSafe(); if (customer != null) { customers.Add(customer); @@ -133,7 +132,7 @@ public static NwStore Deserialize(byte[] serialized) } store = new CNWSStore(Invalid); - if (store.LoadStore(resGff, resStruct, null).ToBool()) + if (store.LoadStore(resGff, resStruct, false.ToInt(), null).ToBool()) { store.LoadObjectState(resGff, resStruct); GC.SuppressFinalize(store); diff --git a/src/main/Anvil/API/Scripts/ScriptParams.cs b/src/main/Anvil/API/Scripts/ScriptParams.cs index 28e0cbc2e..b08021b7d 100644 --- a/src/main/Anvil/API/Scripts/ScriptParams.cs +++ b/src/main/Anvil/API/Scripts/ScriptParams.cs @@ -1,3 +1,4 @@ +using Anvil.Services; using NWN.Core; using NWN.Native.API; @@ -5,7 +6,8 @@ namespace Anvil.API { public sealed class ScriptParams { - private static readonly CVirtualMachine VirtualMachine = NWNXLib.VirtualMachine(); + [Inject] + private static VirtualMachine VirtualMachine { get; set; } /// /// Gets the specified parameter value. @@ -24,15 +26,13 @@ public string this[string paramName] /// true if the specified parameter is set, otherwise false. public bool IsSet(string paramName) { - if (VirtualMachine.m_nRecursionLevel < 0) + if (VirtualMachine.RecursionLevel < 0) { return false; } - CExoArrayListScriptParam scriptParams = VirtualMachine.m_lScriptParams.GetItem(VirtualMachine.m_nRecursionLevel); - for (int i = 0; i < scriptParams.num; i++) + foreach (ScriptParam scriptParam in VirtualMachine.GetCurrentContextScriptParams()) { - ScriptParam scriptParam = scriptParams._OpIndex(i); if (scriptParam.key.ToString() == paramName) { return true; diff --git a/src/main/Anvil/API/Utils/VirtualMachine.cs b/src/main/Anvil/API/Utils/VirtualMachine.cs index 8f1b18395..fdaffb7e5 100644 --- a/src/main/Anvil/API/Utils/VirtualMachine.cs +++ b/src/main/Anvil/API/Utils/VirtualMachine.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; using Anvil.Services; using NLog; using NWN.Core; @@ -14,6 +17,7 @@ public sealed class VirtualMachine { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private readonly int mainThreadId = Thread.CurrentThread.ManagedThreadId; private readonly CVirtualMachine virtualMachine = NWNXLib.VirtualMachine(); public uint InstructionsExecuted @@ -40,9 +44,17 @@ public EventScriptType CurrentRunningEvent set => virtualMachine.m_pVirtualMachineScript[0].m_nScriptEventID = (int)value; } + public int RecursionLevel + { + get => virtualMachine.m_nRecursionLevel; + } + + /// + /// Returns true if the current executing code is being executed on the main thread, and in a Virtual Machine script context. + /// public bool IsInScriptContext { - get => virtualMachine.m_nRecursionLevel >= 0; + get => Thread.CurrentThread.ManagedThreadId == mainThreadId && RecursionLevel >= 0; } public unsafe bool ScriptReturnValue @@ -140,6 +152,16 @@ public T ExecuteInScriptContext(System.Func action, uint objectId = NwObje } } + internal IEnumerable GetCurrentContextScriptParams() + { + if (IsInScriptContext) + { + return virtualMachine.m_lScriptParams.GetItem(RecursionLevel); + } + + return Enumerable.Empty(); + } + private int PushScriptContext(uint oid, int scriptEventId, bool valid) { CNWVirtualMachineCommands cmd = CNWVirtualMachineCommands.FromPointer(virtualMachine.m_pCmdImplementer.Pointer); diff --git a/src/main/Anvil/AnvilCore.cs b/src/main/Anvil/AnvilCore.cs index 3be72e6d3..28ca023c7 100644 --- a/src/main/Anvil/AnvilCore.cs +++ b/src/main/Anvil/AnvilCore.cs @@ -15,7 +15,7 @@ namespace Anvil /// Handles bootstrap and interop between %NWN, %NWN.Core and the %Anvil %API. The entry point of the implementing module should point to this class.
/// Until is called, all APIs are unavailable for usage. ///
- public sealed class AnvilCore : ICoreSignalHandler + public sealed class AnvilCore : IServerLifeCycleEventHandler { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -69,29 +69,46 @@ public static async void Reload() Log.Info("Reloading Anvil"); - instance.Shutdown(true); + instance.serviceManager.ShutdownServices(); + instance.serviceManager.ShutdownLateServices(); GC.Collect(); GC.WaitForPendingFinalizers(); - instance.typeLoader.Init(); - instance.Start(); + instance.InitServices(); } private AnvilCore() {} - void ICoreSignalHandler.OnStart() + void IServerLifeCycleEventHandler.HandleLifeCycleEvent(LifeCycleEvent eventType) { - Init(); - Start(); + HandleLifeCycleEvent(eventType); } - void ICoreSignalHandler.OnShutdown() + private void HandleLifeCycleEvent(LifeCycleEvent eventType) { - Shutdown(); + switch (eventType) + { + case LifeCycleEvent.ModuleLoad: + InitCore(); + InitServices(); + break; + case LifeCycleEvent.DestroyServer: + Log.Info("Server is shutting down..."); + serviceManager.ShutdownServices(); + break; + case LifeCycleEvent.DestroyServerAfter: + serviceManager.ShutdownLateServices(); + ShutdownCore(); + break; + case LifeCycleEvent.Unhandled: + break; + default: + throw new ArgumentOutOfRangeException(nameof(eventType), eventType, null); + } } - private void Init() + private void InitCore() { loggerManager.Init(); PrelinkNative(); @@ -106,39 +123,24 @@ private void Init() Assemblies.Core.GetName().Version, Assemblies.Native.GetName().Version); - Log.Info(".NET runtime is {NetVersion}, running on {OS}, installed at {NetInstallDir}", - RuntimeInformation.FrameworkDescription, - RuntimeInformation.OSDescription, - RuntimeEnvironment.GetRuntimeDirectory()); - - Log.Info("Server is running Neverwinter Nights {Version}", NwServer.Instance.ServerVersion); - CheckServerVersion(); - typeLoader.Init(); } - private void Start() + private void InitServices() { + typeLoader.Init(); serviceManager = new ServiceManager(typeLoader, containerFactory); - - serviceManager.RegisterCoreService(typeLoader); - serviceManager.RegisterCoreService(serviceManager); - serviceManager.Init(); interopHandler.Init(serviceManager.GetService(), serviceManager.GetService()); } - private void Shutdown(bool keepLoggerAlive = false) + private void ShutdownCore() { - serviceManager?.Dispose(); serviceManager = null; - typeLoader.Dispose(); - if (!keepLoggerAlive) - { - unhandledExceptionLogger.Dispose(); - loggerManager.Dispose(); - } + typeLoader.Dispose(); + unhandledExceptionLogger.Dispose(); + loggerManager.Dispose(); } private void CheckServerVersion() diff --git a/src/main/Anvil/Internal/CoreInteropHandler.cs b/src/main/Anvil/Internal/CoreInteropHandler.cs index 7108a856b..613e0f147 100644 --- a/src/main/Anvil/Internal/CoreInteropHandler.cs +++ b/src/main/Anvil/Internal/CoreInteropHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Anvil.Services; using NLog; using NWN.Core; @@ -12,7 +13,7 @@ internal sealed class CoreInteropHandler : ICoreFunctionHandler, ICoreEventHandl private readonly Stack scriptContexts = new Stack(); private readonly Dictionary closures = new Dictionary(); - private readonly ICoreSignalHandler signalHandler; + private readonly IServerLifeCycleEventHandler signalHandler; private ICoreRunScriptHandler scriptHandler; private ICoreLoopHandler loopHandler; @@ -24,7 +25,7 @@ uint ICoreFunctionHandler.ObjectSelf get => objectSelf; } - public CoreInteropHandler(ICoreSignalHandler signalHandler) + public CoreInteropHandler(IServerLifeCycleEventHandler signalHandler) { this.signalHandler = signalHandler; } @@ -37,18 +38,20 @@ public void Init(ICoreRunScriptHandler scriptHandler, ICoreLoopHandler loopHandl void ICoreEventHandler.OnSignal(string signal) { - switch (signal) + LifeCycleEvent eventType = signal switch { - case "ON_MODULE_LOAD_FINISH": - signalHandler.OnStart(); - break; - case "ON_DESTROY_SERVER": - signalHandler.OnShutdown(); - break; - default: - Log.Debug("Unhandled Signal: {Signal}", signal); - break; + "ON_MODULE_LOAD_FINISH" => LifeCycleEvent.ModuleLoad, + "ON_DESTROY_SERVER" => LifeCycleEvent.DestroyServer, + "ON_DESTROY_SERVER_AFTER" => LifeCycleEvent.DestroyServerAfter, + _ => LifeCycleEvent.Unhandled, + }; + + if (eventType == LifeCycleEvent.Unhandled) + { + Log.Debug("Unhandled Signal: {Signal}", signal); } + + signalHandler.HandleLifeCycleEvent(eventType); } void ICoreEventHandler.OnMainLoop(ulong frame) diff --git a/src/main/Anvil/Internal/ICoreSignalHandler.cs b/src/main/Anvil/Internal/ICoreSignalHandler.cs deleted file mode 100644 index a22d3686c..000000000 --- a/src/main/Anvil/Internal/ICoreSignalHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Anvil.Internal -{ - public interface ICoreSignalHandler - { - void OnStart(); - - void OnShutdown(); - } -} diff --git a/src/main/Anvil/Internal/IServerLifeCycleEventHandler.cs b/src/main/Anvil/Internal/IServerLifeCycleEventHandler.cs new file mode 100644 index 000000000..098922d34 --- /dev/null +++ b/src/main/Anvil/Internal/IServerLifeCycleEventHandler.cs @@ -0,0 +1,9 @@ +using Anvil.Services; + +namespace Anvil.Internal +{ + internal interface IServerLifeCycleEventHandler + { + void HandleLifeCycleEvent(LifeCycleEvent eventType); + } +} diff --git a/src/main/Anvil/Plugins/ITypeLoader.cs b/src/main/Anvil/Plugins/ITypeLoader.cs index a2f06a77e..33e8d382e 100644 --- a/src/main/Anvil/Plugins/ITypeLoader.cs +++ b/src/main/Anvil/Plugins/ITypeLoader.cs @@ -3,12 +3,14 @@ namespace Anvil.Plugins { - public interface ITypeLoader : IDisposable + public interface ITypeLoader { void Init(); IReadOnlyCollection LoadedTypes { get; } IReadOnlyCollection ResourcePaths { get; } + + void Dispose(); } } diff --git a/src/main/Anvil/Plugins/PluginLoader.cs b/src/main/Anvil/Plugins/PluginLoader.cs index fa308dfa0..cfa8a784f 100644 --- a/src/main/Anvil/Plugins/PluginLoader.cs +++ b/src/main/Anvil/Plugins/PluginLoader.cs @@ -210,12 +210,13 @@ private IReadOnlyCollection GetResourcePaths() return resourcePaths.AsReadOnly(); } - void IDisposable.Dispose() + void ITypeLoader.Dispose() { loadedAssemblies.Clear(); LoadedTypes = null; ResourcePaths = null; + Log.Info("Unloading plugins..."); foreach (Plugin plugin in plugins) { plugin.Dispose(); diff --git a/src/main/Anvil/Services/ELC/EnforceLegalCharacterService.cs b/src/main/Anvil/Services/ELC/EnforceLegalCharacterService.cs index 7b63c86a6..ab1cb4d0c 100644 --- a/src/main/Anvil/Services/ELC/EnforceLegalCharacterService.cs +++ b/src/main/Anvil/Services/ELC/EnforceLegalCharacterService.cs @@ -139,10 +139,7 @@ private int OnValidateCharacter(void* player, int* bFailedServerRestriction) // ********************************************************************************************************************** NwPlayer nwPlayer = pPlayer.ToNwPlayer(); - OnValidationBefore?.Invoke(new OnELCValidationBefore - { - Player = nwPlayer, - }); + InvokeValidationBeforeEvent(nwPlayer); // *** Server Restrictions ********************************************************************************************** CServerInfo pServerInfo = NWNXLib.AppManager().m_pServerExoApp.GetServerInfo(); @@ -921,9 +918,8 @@ int GetSkillPointAbilityAdjust() int nNumberBonusFeats = pClassLeveledUpIn.GetBonusFeats(nMultiClassLevel[nMultiClassLeveledUpIn]); // Add this level's gained feats to our own list - for (int nFeatIndex = 0; nFeatIndex < pLevelStats.m_lstFeats.num; nFeatIndex++) + foreach (ushort nFeat in pLevelStats.m_lstFeats) { - ushort nFeat = *pLevelStats.m_lstFeats._OpIndex(nFeatIndex); CNWFeat feat = nFeat < pRules.m_nNumFeats ? feats[nFeat] : null; if (feat == null) @@ -1485,7 +1481,7 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) for (byte nSpellLevel = 0; nSpellLevel < NumSpellLevels; nSpellLevel++) { - for (int nSpellIndex = 0; nSpellIndex < pLevelStats.m_pAddedKnownSpellList[nSpellLevel].num; nSpellIndex++) + for (int nSpellIndex = 0; nSpellIndex < pLevelStats.m_pAddedKnownSpellList[nSpellLevel].Count; nSpellIndex++) { // Can we add spells this level? if (pClassLeveledUpIn.m_bSpellbookRestricted.ToBool() && pClassLeveledUpIn.m_bNeedsToMemorizeSpells.ToBool()) @@ -1537,7 +1533,7 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) } } - uint nSpellID = pLevelStats.m_pAddedKnownSpellList[nSpellLevel].element[nSpellIndex]; + uint nSpellID = pLevelStats.m_pAddedKnownSpellList[nSpellLevel][nSpellIndex]; CNWSpell pSpell = pRules.m_pSpellArray.GetSpell((int)nSpellID); if (pSpell == null) @@ -1659,7 +1655,7 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) } // Check Bard/Sorc removed spells - for (int nSpellIndex = 0; nSpellIndex < pLevelStats.m_pRemovedKnownSpellList[nSpellLevel].num; nSpellIndex++) + for (int nSpellIndex = 0; nSpellIndex < pLevelStats.m_pRemovedKnownSpellList[nSpellLevel].Count; nSpellIndex++) { if (!pClassLeveledUpIn.m_bSpellbookRestricted.ToBool() || pClassLeveledUpIn.m_bNeedsToMemorizeSpells.ToBool() || nMultiClassLevel[nMultiClassLeveledUpIn] == 1 || @@ -1678,7 +1674,7 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) } } - uint nSpellID = pLevelStats.m_pRemovedKnownSpellList[nSpellLevel].element[nSpellIndex]; + uint nSpellID = pLevelStats.m_pRemovedKnownSpellList[nSpellLevel][nSpellIndex]; CNWSpell pSpell = pRules.m_pSpellArray.GetSpell((int)nSpellID); @@ -1834,7 +1830,7 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) // Final Feats Check // Check if our list of feats from LevelStats are the same as the feats the character has - for (int nFeatIndex = 0; nFeatIndex < pCreatureStats.m_lstFeats.num; nFeatIndex++) + foreach (ushort nFeat in pCreatureStats.m_lstFeats) { if (!listFeats.Any()) { @@ -1850,8 +1846,6 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) } } - ushort nFeat = pCreatureStats.m_lstFeats.element[nFeatIndex]; - if (!listFeats.Contains(nFeat)) { if (HandleValidationFailure(out int strRefFailure, new OnELCFeatValidationFailure @@ -1892,10 +1886,10 @@ int PrerequisitesFeatCheck(ushort nPrereqFeat) for (byte nLevel = 1; nLevel <= nCharacterLevel; nLevel++) { CNWLevelStats pLevelStats = pCreatureStats.GetLevelStats((byte)(nLevel - 1)); - nNumberOfFeats += pLevelStats.m_lstFeats.num; + nNumberOfFeats += pLevelStats.m_lstFeats.Count; } - if (pCreatureStats.m_lstFeats.num > nNumberOfFeats) + if (pCreatureStats.m_lstFeats.Count > nNumberOfFeats) { if (HandleValidationFailure(out int strRefFailure, new OnELCValidationFailure { @@ -1940,6 +1934,19 @@ private bool HandleValidationFailure(out int strRefFailure, OnELCValidationFailu return !eventData.IgnoreFailure; } + private void InvokeValidationBeforeEvent(NwPlayer player) + { + OnELCValidationBefore eventData = new OnELCValidationBefore + { + Player = player, + }; + + virtualMachine.ExecuteInScriptContext(() => + { + OnValidationBefore?.Invoke(eventData); + }); + } + private bool InvokeCustomCheck(NwPlayer player) { OnELCCustomCheck eventData = new OnELCCustomCheck @@ -1973,9 +1980,9 @@ private int[] GetStatBonusesFromFeats(CExoArrayListUInt16 lstFeats, bool subtrac int[] abilityMods = new int[6]; HashSet creatureFeats = new HashSet(); - for (int i = 0; i < lstFeats.num; i++) + foreach (ushort nFeat in lstFeats) { - creatureFeats.Add((Feat)(*lstFeats._OpIndex(i))); + creatureFeats.Add((Feat)nFeat); } int GetFeatCount(params Feat[] epicFeats) diff --git a/src/main/Anvil/Services/Hooking/HookService.cs b/src/main/Anvil/Services/Hooking/HookService.cs index 82ba4e22b..a649f819a 100644 --- a/src/main/Anvil/Services/Hooking/HookService.cs +++ b/src/main/Anvil/Services/Hooking/HookService.cs @@ -12,7 +12,7 @@ namespace Anvil.Services /// [ServiceBinding(typeof(HookService))] [ServiceBindingOptions(BindingOrder.API)] - public sealed unsafe class HookService : IDisposable + public sealed unsafe class HookService : ILateDisposable { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -24,20 +24,16 @@ public sealed unsafe class HookService : IDisposable /// The handler to be invoked when this function is called. Once hooked, the original function will not be called, and must be invoked manually via the returned object. /// The address of the native function. Use the constants available in . /// The execution order for this hook. See the constants in . - /// True if the hook should automatically be disposed during shutdown/reload. /// The delegate type that identically matches the native function signature. /// A wrapper object containing a delegate to the original function. The wrapped object can be disposed to release the hook. - public FunctionHook RequestHook(T handler, uint address, int order = HookOrder.Default, bool shutdownDispose = true) where T : Delegate + public FunctionHook RequestHook(T handler, uint address, int order = HookOrder.Default) where T : Delegate { Log.Debug("Requesting function hook for {HookType}, address {Address}", typeof(T).Name, $"0x{address:X}"); IntPtr managedFuncPtr = Marshal.GetFunctionPointerForDelegate(handler); IntPtr nativeFuncPtr = VM.RequestHook(new IntPtr(address), managedFuncPtr, order); FunctionHook hook = new FunctionHook(this, nativeFuncPtr, handler); - if (shutdownDispose) - { - hooks.Add(hook); - } + hooks.Add(hook); return hook; } @@ -48,19 +44,15 @@ public FunctionHook RequestHook(T handler, uint address, int order = HookO /// A delegate pointer (delegate*) to be invoked when the original game function is called. Once hooked, the original function will not be called, and must be invoked manually via the returned object. /// The address of the native function. Use the constants available in . /// The execution order for this hook. See the constants in . - /// True if the hook should automatically be disposed during shutdown/reload. /// The delegate type that identically matches the native function signature. /// A wrapper object containing a delegate to the original function. The wrapped object can be disposed to release the hook. - public FunctionHook RequestHook(void* handler, uint address, int order = HookOrder.Default, bool shutdownDispose = true) where T : Delegate + public FunctionHook RequestHook(void* handler, uint address, int order = HookOrder.Default) where T : Delegate { Log.Debug("Requesting function hook for {HookType}, address {Address}", typeof(T).Name, $"0x{address:X}"); IntPtr nativeFuncPtr = VM.RequestHook(new IntPtr(address), (IntPtr)handler, order); FunctionHook retVal = new FunctionHook(this, nativeFuncPtr); - if (shutdownDispose) - { - hooks.Add(retVal); - } + hooks.Add(retVal); return retVal; } @@ -70,7 +62,7 @@ internal void RemoveHook(FunctionHook hook) where T : Delegate hooks.Remove(hook); } - void IDisposable.Dispose() + void ILateDisposable.LateDispose() { foreach (IDisposable hook in hooks.ToList()) { diff --git a/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs b/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs index e8b225377..bc0322dc9 100644 --- a/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs +++ b/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs @@ -25,36 +25,29 @@ public sealed unsafe class ObjectStorageService private delegate int LoadFromGffHook(void* pUUID, void* pRes, void* pStruct); - private static FunctionHook objectDestructorHook; - private static FunctionHook areaDestructorHook; - private static FunctionHook eatTURDHook; - private static FunctionHook dropTURDHook; + private readonly FunctionHook objectDestructorHook; + private readonly FunctionHook areaDestructorHook; + private readonly FunctionHook eatTURDHook; + private readonly FunctionHook dropTURDHook; - private static FunctionHook saveToGffHook; - private static FunctionHook loadFromGffHook; + private readonly FunctionHook saveToGffHook; + private readonly FunctionHook loadFromGffHook; private readonly Dictionary objectStorage = new Dictionary(); public ObjectStorageService(HookService hookService) { - objectDestructorHook?.Dispose(); - areaDestructorHook?.Dispose(); - eatTURDHook?.Dispose(); - dropTURDHook?.Dispose(); - saveToGffHook?.Dispose(); - loadFromGffHook?.Dispose(); - - objectDestructorHook = hookService.RequestHook(OnObjectDestructor, FunctionsLinux._ZN10CNWSObjectD1Ev, HookOrder.VeryEarly, false); - areaDestructorHook = hookService.RequestHook(OnAreaDestructor, FunctionsLinux._ZN8CNWSAreaD1Ev, HookOrder.VeryEarly, false); - eatTURDHook = hookService.RequestHook(OnEatTURD, FunctionsLinux._ZN10CNWSPlayer7EatTURDEP14CNWSPlayerTURD, HookOrder.VeryEarly, false); - dropTURDHook = hookService.RequestHook(OnDropTURD, FunctionsLinux._ZN10CNWSPlayer8DropTURDEv, HookOrder.VeryEarly, false); + objectDestructorHook = hookService.RequestHook(OnObjectDestructor, FunctionsLinux._ZN10CNWSObjectD1Ev, HookOrder.VeryEarly); + areaDestructorHook = hookService.RequestHook(OnAreaDestructor, FunctionsLinux._ZN8CNWSAreaD1Ev, HookOrder.VeryEarly); + eatTURDHook = hookService.RequestHook(OnEatTURD, FunctionsLinux._ZN10CNWSPlayer7EatTURDEP14CNWSPlayerTURD, HookOrder.VeryEarly); + dropTURDHook = hookService.RequestHook(OnDropTURD, FunctionsLinux._ZN10CNWSPlayer8DropTURDEv, HookOrder.VeryEarly); // We want to prioritize our call first for serialization, so it gets called last in the CallOriginal call in NWNX. const int orderBeforeNWNX = HookOrder.VeryEarly - 1; const int orderAfterNWNX = HookOrder.VeryEarly + 1; - saveToGffHook = hookService.RequestHook(OnSaveToGff, FunctionsLinux._ZN8CNWSUUID9SaveToGffEP7CResGFFP10CResStruct, orderBeforeNWNX, false); - loadFromGffHook = hookService.RequestHook(OnLoadFromGff, FunctionsLinux._ZN8CNWSUUID11LoadFromGffEP7CResGFFP10CResStruct, orderAfterNWNX, false); + saveToGffHook = hookService.RequestHook(OnSaveToGff, FunctionsLinux._ZN8CNWSUUID9SaveToGffEP7CResGFFP10CResStruct, orderBeforeNWNX); + loadFromGffHook = hookService.RequestHook(OnLoadFromGff, FunctionsLinux._ZN8CNWSUUID11LoadFromGffEP7CResGFFP10CResStruct, orderAfterNWNX); } public ObjectStorage GetObjectStorage(NwObject gameObject) diff --git a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs index 287c1c86f..dc5e45ec1 100644 --- a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs +++ b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs @@ -74,23 +74,14 @@ private void RegisterBindings(Type bindTo, ServiceBindingAttribute[] newBindings string serviceName = GetServiceName(bindTo, options); PerContainerLifetime lifeTime = new PerContainerLifetime(); + RegisterExplicitBindings(bindTo, newBindings, serviceName, lifeTime); if (options is not { Lazy: true }) { - ServiceContainer.Register(typeof(object), bindTo, serviceName, lifeTime); - if (bindTo.IsAssignableTo(typeof(IInitializable))) - { - ServiceContainer.Register(typeof(IInitializable), bindTo, serviceName, lifeTime); - } - } - - foreach (ServiceBindingAttribute bindingInfo in newBindings) - { - ServiceContainer.Register(bindingInfo.BindFrom, bindTo, serviceName, lifeTime); - Log.Debug("Bind: {BindFrom} -> {BindTo}", bindingInfo.BindFrom.FullName, bindTo.FullName); + RegisterImplicitBindings(bindTo, serviceName, lifeTime); } - Log.Info("Registered service: {Service}", bindTo.FullName); + Log.Info("Registered service {Service}", bindTo.FullName); } private string GetServiceName(Type implementation, ServiceBindingOptionsAttribute options) @@ -99,6 +90,30 @@ private string GetServiceName(Type implementation, ServiceBindingOptionsAttribut return bindingOrder.ToString("D5") + implementation.FullName; } + private void RegisterImplicitBindings(Type bindTo, string serviceName, ILifetime lifeTime) + { + ServiceContainer.Register(typeof(object), bindTo, serviceName, lifeTime); + + if (bindTo.IsAssignableTo(typeof(IInitializable))) + { + ServiceContainer.Register(typeof(IInitializable), bindTo, serviceName, lifeTime); + } + + if (bindTo.IsAssignableTo(typeof(ILateDisposable))) + { + ServiceContainer.Register(typeof(ILateDisposable), bindTo, serviceName, lifeTime); + } + } + + private void RegisterExplicitBindings(Type bindTo, ServiceBindingAttribute[] newBindings, string serviceName, ILifetime lifeTime) + { + foreach (ServiceBindingAttribute bindingInfo in newBindings) + { + ServiceContainer.Register(bindingInfo.BindFrom, bindTo, serviceName, lifeTime); + Log.Debug("Bind {BindFrom} -> {BindTo}", bindingInfo.BindFrom.FullName, bindTo.FullName); + } + } + /// /// Override in a child class to specify additional bindings/overrides.
/// See https://www.lightinject.net/ for documentation. diff --git a/src/main/Anvil/Services/Services/IInitializable.cs b/src/main/Anvil/Services/Services/LifeCycle/IInitializable.cs similarity index 100% rename from src/main/Anvil/Services/Services/IInitializable.cs rename to src/main/Anvil/Services/Services/LifeCycle/IInitializable.cs diff --git a/src/main/Anvil/Services/Services/LifeCycle/ILateDisposable.cs b/src/main/Anvil/Services/Services/LifeCycle/ILateDisposable.cs new file mode 100644 index 000000000..1d7896885 --- /dev/null +++ b/src/main/Anvil/Services/Services/LifeCycle/ILateDisposable.cs @@ -0,0 +1,15 @@ +namespace Anvil.Services +{ + /// + /// Interface that is invoked after the server has been shutdown, and during reloads. + /// + /// + /// This interface should only be used to release function hooks, or to replicate data that was written during server shutdown (e.g. characters).
+ /// When this is called, the server instance has already been destroyed, and you cannot access any APIs.
+ /// Implement instead to receive a callback just before server shutdown. + ///
+ public interface ILateDisposable + { + void LateDispose(); + } +} diff --git a/src/main/Anvil/Services/Services/LifeCycle/LifeCycleEvent.cs b/src/main/Anvil/Services/Services/LifeCycle/LifeCycleEvent.cs new file mode 100644 index 000000000..51ec93cbc --- /dev/null +++ b/src/main/Anvil/Services/Services/LifeCycle/LifeCycleEvent.cs @@ -0,0 +1,10 @@ +namespace Anvil.Services +{ + internal enum LifeCycleEvent + { + Unhandled = 0, + ModuleLoad, + DestroyServer, + DestroyServerAfter, + } +} diff --git a/src/main/Anvil/Services/Services/ServiceManager.cs b/src/main/Anvil/Services/Services/ServiceManager.cs index 571281332..03b3f9d0d 100644 --- a/src/main/Anvil/Services/Services/ServiceManager.cs +++ b/src/main/Anvil/Services/Services/ServiceManager.cs @@ -1,20 +1,21 @@ +using System.Collections.Generic; +using System.Linq; using Anvil.Plugins; -using JetBrains.Annotations; using LightInject; using NLog; namespace Anvil.Services { [ServiceBindingOptions(BindingOrder.Core)] - public sealed class ServiceManager + internal sealed class ServiceManager { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - [UsedImplicitly] private readonly ITypeLoader typeLoader; - private readonly IContainerFactory containerFactory; - private readonly ServiceContainer serviceContainer; + + private ServiceContainer serviceContainer; + private List lateDisposables; internal ServiceManager(ITypeLoader typeLoader, IContainerFactory containerFactory) { @@ -26,25 +27,47 @@ internal ServiceManager(ITypeLoader typeLoader, IContainerFactory containerFacto serviceContainer = containerFactory.Setup(typeLoader); } - internal void RegisterCoreService(T instance) + public T GetService() where T : class { - containerFactory.RegisterCoreService(instance); + return serviceContainer.GetInstance(); } internal void Init() { + RegisterCoreService(typeLoader); + RegisterCoreService(this); + containerFactory.BuildContainer(); NotifyInitComplete(); } - ~ServiceManager() + internal void ShutdownServices() { - Dispose(); + if (serviceContainer == null) + { + return; + } + + Log.Info("Unloading services..."); + lateDisposables = serviceContainer.GetAllInstances().ToList(); + serviceContainer.Dispose(); + + serviceContainer = null; } - internal T GetService() where T : class + internal void ShutdownLateServices() { - return serviceContainer.GetInstance(); + foreach (ILateDisposable lateDisposable in lateDisposables) + { + lateDisposable.LateDispose(); + } + + lateDisposables = null; + } + + private void RegisterCoreService(T instance) + { + containerFactory.RegisterCoreService(instance); } private void NotifyInitComplete() @@ -54,10 +77,5 @@ private void NotifyInitComplete() initializable.Init(); } } - - internal void Dispose() - { - serviceContainer?.Dispose(); - } } } From fc4f5f6d1098244f5101768dd201504d452919f7 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Tue, 14 Sep 2021 23:33:07 +0200 Subject: [PATCH 02/16] Add new 8193.30 constants, script functions & events. (#366) * Add new 8193.30 API/script functions & events. * Use constant value for icons. * Remove duplicate property. * Remove redundant qualifier. * Player GUI event integer data. --- .../Anvil/API/Constants/ChatBarChannel.cs | 11 ++ src/main/Anvil/API/Constants/EffectIcon.cs | 138 ++++++++++++++++++ src/main/Anvil/API/Constants/EffectType.cs | 2 + .../Anvil/API/Constants/EventScriptType.cs | 2 + src/main/Anvil/API/Constants/GUIPanel.cs | 7 + src/main/Anvil/API/Constants/GuiEventType.cs | 22 +++ .../API/EngineStructure/Effect.Create.cs | 5 + .../Anvil/API/Events/Game/ModuleEvents.cs | 135 +++++++++++++++++ src/main/Anvil/API/Object/NwPlayer.cs | 12 ++ .../Variable/Local/LocalVariableCassowary.cs | 18 +++ 10 files changed, 352 insertions(+) create mode 100644 src/main/Anvil/API/Constants/ChatBarChannel.cs create mode 100644 src/main/Anvil/API/Constants/EffectIcon.cs create mode 100644 src/main/Anvil/API/Constants/GuiEventType.cs create mode 100644 src/main/Anvil/API/Variable/Local/LocalVariableCassowary.cs diff --git a/src/main/Anvil/API/Constants/ChatBarChannel.cs b/src/main/Anvil/API/Constants/ChatBarChannel.cs new file mode 100644 index 000000000..174d49825 --- /dev/null +++ b/src/main/Anvil/API/Constants/ChatBarChannel.cs @@ -0,0 +1,11 @@ +namespace Anvil.API +{ + public enum ChatBarChannel + { + Shout = 0, + Whisper = 1, + Talk = 2, + Party = 3, + DM = 4, + } +} diff --git a/src/main/Anvil/API/Constants/EffectIcon.cs b/src/main/Anvil/API/Constants/EffectIcon.cs new file mode 100644 index 000000000..69d506773 --- /dev/null +++ b/src/main/Anvil/API/Constants/EffectIcon.cs @@ -0,0 +1,138 @@ +using NWN.Core; + +namespace Anvil.API +{ + public enum EffectIcon + { + Invalid = NWScript.EFFECT_ICON_INVALID, + DamageResistance = NWScript.EFFECT_ICON_DAMAGE_RESISTANCE, + Regenerate = NWScript.EFFECT_ICON_REGENERATE, + DamageReduction = NWScript.EFFECT_ICON_DAMAGE_REDUCTION, + TemporaryHitpoints = NWScript.EFFECT_ICON_TEMPORARY_HITPOINTS, + Entangle = NWScript.EFFECT_ICON_ENTANGLE, + Invulnerable = NWScript.EFFECT_ICON_INVULNERABLE, + Deaf = NWScript.EFFECT_ICON_DEAF, + Fatigue = NWScript.EFFECT_ICON_FATIGUE, + Immunity = NWScript.EFFECT_ICON_IMMUNITY, + Blind = NWScript.EFFECT_ICON_BLIND, + EnemyAttackBonus = NWScript.EFFECT_ICON_ENEMY_ATTACK_BONUS, + Charmed = NWScript.EFFECT_ICON_CHARMED, + Confused = NWScript.EFFECT_ICON_CONFUSED, + Frightened = NWScript.EFFECT_ICON_FRIGHTENED, + Dominated = NWScript.EFFECT_ICON_DOMINATED, + Paralyze = NWScript.EFFECT_ICON_PARALYZE, + Dazed = NWScript.EFFECT_ICON_DAZED, + Stunned = NWScript.EFFECT_ICON_STUNNED, + Sleep = NWScript.EFFECT_ICON_SLEEP, + Poison = NWScript.EFFECT_ICON_POISON, + Disease = NWScript.EFFECT_ICON_DISEASE, + Curse = NWScript.EFFECT_ICON_CURSE, + Silence = NWScript.EFFECT_ICON_SILENCE, + Turned = NWScript.EFFECT_ICON_TURNED, + Haste = NWScript.EFFECT_ICON_HASTE, + Slow = NWScript.EFFECT_ICON_SLOW, + AbilityIncreaseStr = NWScript.EFFECT_ICON_ABILITY_INCREASE_STR, + AbilityDecreaseStr = NWScript.EFFECT_ICON_ABILITY_DECREASE_STR, + AttackIncrease = NWScript.EFFECT_ICON_ATTACK_INCREASE, + AttackDecrease = NWScript.EFFECT_ICON_ATTACK_DECREASE, + DamageIncrease = NWScript.EFFECT_ICON_DAMAGE_INCREASE, + DamageDecrease = NWScript.EFFECT_ICON_DAMAGE_DECREASE, + DamageImmunityIncrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_INCREASE, + DamageImmunityDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_DECREASE, + ACIncrease = NWScript.EFFECT_ICON_AC_INCREASE, + ACDecrease = NWScript.EFFECT_ICON_AC_DECREASE, + MovementSpeedIncrease = NWScript.EFFECT_ICON_MOVEMENT_SPEED_INCREASE, + MovementSpeedDecrease = NWScript.EFFECT_ICON_MOVEMENT_SPEED_DECREASE, + SavingThrowIncrease = NWScript.EFFECT_ICON_SAVING_THROW_INCREASE, + SavingThrowDecrease = NWScript.EFFECT_ICON_SAVING_THROW_DECREASE, + SpellResistanceIncrease = NWScript.EFFECT_ICON_SPELL_RESISTANCE_INCREASE, + SpellResistanceDecrease = NWScript.EFFECT_ICON_SPELL_RESISTANCE_DECREASE, + SkillIncrease = NWScript.EFFECT_ICON_SKILL_INCREASE, + SkillDecrease = NWScript.EFFECT_ICON_SKILL_DECREASE, + Invisibility = NWScript.EFFECT_ICON_INVISIBILITY, + ImprovedInvisibility = NWScript.EFFECT_ICON_IMPROVEDINVISIBILITY, + Darkness = NWScript.EFFECT_ICON_DARKNESS, + DispelMagical = NWScript.EFFECT_ICON_DISPELMAGICALL, + ElementalShield = NWScript.EFFECT_ICON_ELEMENTALSHIELD, + LevelDrain = NWScript.EFFECT_ICON_LEVELDRAIN, + Polymorph = NWScript.EFFECT_ICON_POLYMORPH, + Sanctuary = NWScript.EFFECT_ICON_SANCTUARY, + TrueSeeing = NWScript.EFFECT_ICON_TRUESEEING, + SeeInvisibility = NWScript.EFFECT_ICON_SEEINVISIBILITY, + TimeStop = NWScript.EFFECT_ICON_TIMESTOP, + Blindness = NWScript.EFFECT_ICON_BLINDNESS, + SpellLevelAbsorption = NWScript.EFFECT_ICON_SPELLLEVELABSORPTION, + DispelMagicBest = NWScript.EFFECT_ICON_DISPELMAGICBEST, + AbilityIncreaseDex = NWScript.EFFECT_ICON_ABILITY_INCREASE_DEX, + AbilityDecreaseDex = NWScript.EFFECT_ICON_ABILITY_DECREASE_DEX, + AbilityIncreaseCon = NWScript.EFFECT_ICON_ABILITY_INCREASE_CON, + AbilityDecreaseCon = NWScript.EFFECT_ICON_ABILITY_DECREASE_CON, + AbilityIncreaseInt = NWScript.EFFECT_ICON_ABILITY_INCREASE_INT, + AbilityDecreaseInt = NWScript.EFFECT_ICON_ABILITY_DECREASE_INT, + AbilityIncreaseWis = NWScript.EFFECT_ICON_ABILITY_INCREASE_WIS, + AbilityDecreaseWis = NWScript.EFFECT_ICON_ABILITY_DECREASE_WIS, + AbilityIncreaseCha = NWScript.EFFECT_ICON_ABILITY_INCREASE_CHA, + AbilityDecreaseCha = NWScript.EFFECT_ICON_ABILITY_DECREASE_CHA, + ImmunityAll = NWScript.EFFECT_ICON_IMMUNITY_ALL, + ImmunityMind = NWScript.EFFECT_ICON_IMMUNITY_MIND, + ImmunityPoison = NWScript.EFFECT_ICON_IMMUNITY_POISON, + ImmunityDisease = NWScript.EFFECT_ICON_IMMUNITY_DISEASE, + ImmunityFear = NWScript.EFFECT_ICON_IMMUNITY_FEAR, + ImmunityTrap = NWScript.EFFECT_ICON_IMMUNITY_TRAP, + ImmunityParalysis = NWScript.EFFECT_ICON_IMMUNITY_PARALYSIS, + ImmunityBlindness = NWScript.EFFECT_ICON_IMMUNITY_BLINDNESS, + ImmunityDeafness = NWScript.EFFECT_ICON_IMMUNITY_DEAFNESS, + ImmunitySlow = NWScript.EFFECT_ICON_IMMUNITY_SLOW, + ImmunityEntangle = NWScript.EFFECT_ICON_IMMUNITY_ENTANGLE, + ImmunitySilence = NWScript.EFFECT_ICON_IMMUNITY_SILENCE, + ImmunityStun = NWScript.EFFECT_ICON_IMMUNITY_STUN, + ImmunitySleep = NWScript.EFFECT_ICON_IMMUNITY_SLEEP, + ImmunityCharm = NWScript.EFFECT_ICON_IMMUNITY_CHARM, + ImmunityDominate = NWScript.EFFECT_ICON_IMMUNITY_DOMINATE, + ImmunityConfuse = NWScript.EFFECT_ICON_IMMUNITY_CONFUSE, + ImmunityCurse = NWScript.EFFECT_ICON_IMMUNITY_CURSE, + ImmunityDazed = NWScript.EFFECT_ICON_IMMUNITY_DAZED, + ImmunityAbilityDecrease = NWScript.EFFECT_ICON_IMMUNITY_ABILITY_DECREASE, + ImmunityAttackDecrease = NWScript.EFFECT_ICON_IMMUNITY_ATTACK_DECREASE, + ImmunityDamageDecrease = NWScript.EFFECT_ICON_IMMUNITY_DAMAGE_DECREASE, + ImmunityDamageImmunityDecrease = NWScript.EFFECT_ICON_IMMUNITY_DAMAGE_IMMUNITY_DECREASE, + ImmunityACDecrease = NWScript.EFFECT_ICON_IMMUNITY_AC_DECREASE, + ImmunityMovementSpeedDecrease = NWScript.EFFECT_ICON_IMMUNITY_MOVEMENT_SPEED_DECREASE, + ImmunitySavingThrowDecrease = NWScript.EFFECT_ICON_IMMUNITY_SAVING_THROW_DECREASE, + ImmunitySpellResistanceDecrease = NWScript.EFFECT_ICON_IMMUNITY_SPELL_RESISTANCE_DECREASE, + ImmunitySkillDecrease = NWScript.EFFECT_ICON_IMMUNITY_SKILL_DECREASE, + ImmunityKnockdown = NWScript.EFFECT_ICON_IMMUNITY_KNOCKDOWN, + ImmunityNegativeLevel = NWScript.EFFECT_ICON_IMMUNITY_NEGATIVE_LEVEL, + ImmunitySneakAttack = NWScript.EFFECT_ICON_IMMUNITY_SNEAK_ATTACK, + ImmunityCriticalHit = NWScript.EFFECT_ICON_IMMUNITY_CRITICAL_HIT, + ImmunityDeathMagic = NWScript.EFFECT_ICON_IMMUNITY_DEATH_MAGIC, + ReflexSaveIncreased = NWScript.EFFECT_ICON_REFLEX_SAVE_INCREASED, + FortSaveIncreased = NWScript.EFFECT_ICON_FORT_SAVE_INCREASED, + WillSaveIncreased = NWScript.EFFECT_ICON_WILL_SAVE_INCREASED, + Taunted = NWScript.EFFECT_ICON_TAUNTED, + SpellImmunity = NWScript.EFFECT_ICON_SPELLIMMUNITY, + Etherealness = NWScript.EFFECT_ICON_ETHEREALNESS, + Concealment = NWScript.EFFECT_ICON_CONCEALMENT, + Petrified = NWScript.EFFECT_ICON_PETRIFIED, + EffectSpellFailure = NWScript.EFFECT_ICON_EFFECT_SPELL_FAILURE, + DamageImmunityMagic = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_MAGIC, + DamageImmunityAcid = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_ACID, + DamageImmunityCold = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_COLD, + DamageImmunityDivine = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_DIVINE, + DamageImmunityElectrical = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_ELECTRICAL, + DamageImmunityFire = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_FIRE, + DamageImmunityNegative = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_NEGATIVE, + DamageImmunityPositive = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_POSITIVE, + DamageImmunitySonic = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_SONIC, + DamageImmunityMagicDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_MAGIC_DECREASE, + DamageImmunityAcidDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_ACID_DECREASE, + DamageImmunityColdDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_COLD_DECREASE, + DamageImmunityDivineDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_DIVINE_DECREASE, + DamageImmunityElectricalDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_ELECTRICAL_DECREASE, + DamageImmunityFireDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_FIRE_DECREASE, + DamageImmunityNegativeDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_NEGATIVE_DECREASE, + DamageImmunityPositiveDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_POSITIVE_DECREASE, + DamageImmunitySonicDecrease = NWScript.EFFECT_ICON_DAMAGE_IMMUNITY_SONIC_DECREASE, + Wounding = NWScript.EFFECT_ICON_WOUNDING, + } +} diff --git a/src/main/Anvil/API/Constants/EffectType.cs b/src/main/Anvil/API/Constants/EffectType.cs index 739cd9242..125e61619 100644 --- a/src/main/Anvil/API/Constants/EffectType.cs +++ b/src/main/Anvil/API/Constants/EffectType.cs @@ -80,5 +80,7 @@ public enum EffectType SpellFailure = NWScript.EFFECT_TYPE_SPELL_FAILURE, CutsceneGhost = NWScript.EFFECT_TYPE_CUTSCENEGHOST, CutsceneImmobilize = NWScript.EFFECT_TYPE_CUTSCENEIMMOBILIZE, + RunScript = NWScript.EFFECT_TYPE_RUNSCRIPT, + Icon = NWScript.EFFECT_TYPE_ICON, } } diff --git a/src/main/Anvil/API/Constants/EventScriptType.cs b/src/main/Anvil/API/Constants/EventScriptType.cs index 5d3623ff7..64a5d8f85 100644 --- a/src/main/Anvil/API/Constants/EventScriptType.cs +++ b/src/main/Anvil/API/Constants/EventScriptType.cs @@ -24,6 +24,8 @@ public enum EventScriptType ModuleOnUnequipItem = NWScript.EVENT_SCRIPT_MODULE_ON_UNEQUIP_ITEM, ModuleOnPlayerChat = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_CHAT, ModuleOnPlayerTarget = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_TARGET, + ModuleOnPlayerGuiEvent = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_GUIEVENT, + ModuleOnPlayerTileAction = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_TILE_ACTION, AreaOnHeartbeat = NWScript.EVENT_SCRIPT_AREA_ON_HEARTBEAT, AreaOnUserDefinedEvent = NWScript.EVENT_SCRIPT_AREA_ON_USER_DEFINED_EVENT, AreaOnEnter = NWScript.EVENT_SCRIPT_AREA_ON_ENTER, diff --git a/src/main/Anvil/API/Constants/GUIPanel.cs b/src/main/Anvil/API/Constants/GUIPanel.cs index cd79ef6fb..b61ba84c0 100644 --- a/src/main/Anvil/API/Constants/GUIPanel.cs +++ b/src/main/Anvil/API/Constants/GUIPanel.cs @@ -5,5 +5,12 @@ namespace Anvil.API public enum GUIPanel { Death = NWScript.GUI_PANEL_PLAYER_DEATH, + Minimap = NWScript.GUI_PANEL_MINIMAP, + Compass = NWScript.GUI_PANEL_COMPASS, + Inventory = NWScript.GUI_PANEL_INVENTORY, + PlayerList = NWScript.GUI_PANEL_PLAYERLIST, + Journal = NWScript.GUI_PANEL_JOURNAL, + SpellBook = NWScript.GUI_PANEL_SPELLBOOK, + CharacterSheet = NWScript.GUI_PANEL_CHARACTERSHEET, } } diff --git a/src/main/Anvil/API/Constants/GuiEventType.cs b/src/main/Anvil/API/Constants/GuiEventType.cs new file mode 100644 index 000000000..7617e0006 --- /dev/null +++ b/src/main/Anvil/API/Constants/GuiEventType.cs @@ -0,0 +1,22 @@ +using NWN.Core; + +namespace Anvil.API +{ + public enum GuiEventType + { + ChatBarFocus = NWScript.GUIEVENT_CHATBAR_FOCUS, + ChatBarUnFocus = NWScript.GUIEVENT_CHATBAR_UNFOCUS, + CharacterSheetSkillClick = NWScript.GUIEVENT_CHARACTERSHEET_SKILL_CLICK, + CharacterSheetFeatClick = NWScript.GUIEVENT_CHARACTERSHEET_FEAT_CLICK, + EffectIconClick = NWScript.GUIEVENT_EFFECTICON_CLICK, + DeathPanelWaitForHelpClick = NWScript.GUIEVENT_DEATHPANEL_WAITFORHELP_CLICK, + MinimapMapPinClick = NWScript.GUIEVENT_MINIMAP_MAPPIN_CLICK, + MinimapOpen = NWScript.GUIEVENT_MINIMAP_OPEN, + MinimapClose = NWScript.GUIEVENT_MINIMAP_CLOSE, + JournalOpen = NWScript.GUIEVENT_JOURNAL_OPEN, + JournalClose = NWScript.GUIEVENT_JOURNAL_CLOSE, + PlayerListPlayerClick = NWScript.GUIEVENT_PLAYERLIST_PLAYER_CLICK, + PartyBarPortraitClick = NWScript.GUIEVENT_PARTYBAR_PORTRAIT_CLICK, + DisabledPanelAttemptOpen = NWScript.GUIEVENT_DISABLED_PANEL_ATTEMPT_OPEN, + } +} diff --git a/src/main/Anvil/API/EngineStructure/Effect.Create.cs b/src/main/Anvil/API/EngineStructure/Effect.Create.cs index 62fdf77aa..8db51a13b 100644 --- a/src/main/Anvil/API/EngineStructure/Effect.Create.cs +++ b/src/main/Anvil/API/EngineStructure/Effect.Create.cs @@ -384,6 +384,11 @@ public static Effect HitPointChangeWhenDying(float hpChangePerRound) return NWScript.EffectHitPointChangeWhenDying(hpChangePerRound); } + public static Effect Icon(EffectIcon icon) + { + return NWScript.EffectIcon((int)icon); + } + public static Effect Immunity(ImmunityType immunityType) { return NWScript.EffectImmunity((int)immunityType); diff --git a/src/main/Anvil/API/Events/Game/ModuleEvents.cs b/src/main/Anvil/API/Events/Game/ModuleEvents.cs index 9f7d7af45..42763b95d 100644 --- a/src/main/Anvil/API/Events/Game/ModuleEvents.cs +++ b/src/main/Anvil/API/Events/Game/ModuleEvents.cs @@ -232,6 +232,106 @@ NwObject IEvent.Context } } + /// + /// Triggered when a player clicks on a particular GUI interface. + /// + [GameEvent(EventScriptType.ModuleOnPlayerGuiEvent)] + public sealed class OnPlayerGuiEvent : IEvent + { + /// + /// Gets the that triggered this event. + /// + public NwPlayer Player { get; } = NWScript.GetLastGuiEventPlayer().ToNwPlayer(); + + /// + /// Gets the that was triggered. + /// + public GuiEventType EventType { get; } = (GuiEventType)NWScript.GetLastGuiEventType(); + + /// + /// Gets the object data associated with this GUI event. + /// + /// + /// : The selected chat channel. Does not indicate the actual used channel. 0 = Shout, 1 = Whisper, 2 = Talk, 3 = Party, 4 = DM + /// : The + /// + public NwObject EventObject { get; } = NWScript.GetLastGuiEventObject().ToNwObject(); + + /// + /// Gets the chat bar channel that is selected. Only valid in and type events. + /// + public ChatBarChannel ChatBarChannel + { + get => (ChatBarChannel)integerEventData; + } + + /// + /// Gets the skill that was selected. Only valid in events. + /// + public Skill SkillSelection + { + get => (Skill)integerEventData; + } + + /// + /// Gets the feat that was selected. Only valid in events. + /// + public Feat FeatSelection + { + get => (Feat)integerEventData; + } + + /// + /// Gets the effect icon that was selected. Only valid in events. + /// + public EffectIcon EffectIcon + { + get => (EffectIcon)integerEventData; + } + + /// + /// Gets the GUI panel that attempted to be opened. Only valid in events. + /// + public GUIPanel OpenedPanel + { + get => (GUIPanel)integerEventData; + } + + private readonly int integerEventData = NWScript.GetLastGuiEventInteger(); + + NwObject IEvent.Context + { + get => Player.ControlledCreature; + } + } + + /// + /// Triggered when a player performs an action on an area tile. + /// + [GameEvent(EventScriptType.ModuleOnPlayerTileAction)] + public sealed class OnPlayerTileAction : IEvent + { + /// + /// Gets the that performed a tile action. + /// + public NwPlayer Player { get; } = NWScript.GetLastPlayerToDoTileAction().ToNwPlayer(); + + /// + /// Gets the position that was clicked. + /// + public Vector3 TargetPosition { get; } = NWScript.GetLastTileActionPosition(); + + /// + /// Gets the action ID (surfacemat.2da) that was selected by the player. + /// + public int ActionId { get; } = NWScript.GetLastTileActionId(); + + NwObject IEvent.Context + { + get => Player.ControlledCreature; + } + } + /// /// Triggered when a dies. /// @@ -494,6 +594,27 @@ public event Action OnPlayerChat remove => EventService.UnsubscribeAll(value); } + /// + public event Action OnPlayerTarget + { + add => EventService.SubscribeAll(new GameEventFactory.RegistrationData(this), value); + remove => EventService.UnsubscribeAll(value); + } + + /// + public event Action OnPlayerGuiEvent + { + add => EventService.SubscribeAll(new GameEventFactory.RegistrationData(this), value); + remove => EventService.UnsubscribeAll(value); + } + + /// + public event Action OnPlayerTileAction + { + add => EventService.SubscribeAll(new GameEventFactory.RegistrationData(this), value); + remove => EventService.UnsubscribeAll(value); + } + /// public event Action OnPlayerDeath { @@ -595,6 +716,20 @@ public event Action OnPlayerTarget remove => EventService.Unsubscribe(ControlledCreature, value); } + /// + public event Action OnPlayerGuiEvent + { + add => EventService.Subscribe(ControlledCreature, new GameEventFactory.RegistrationData(NwModule.Instance), value); + remove => EventService.Unsubscribe(ControlledCreature, value); + } + + /// + public event Action OnPlayerTileAction + { + add => EventService.Subscribe(ControlledCreature, new GameEventFactory.RegistrationData(NwModule.Instance), value); + remove => EventService.Unsubscribe(ControlledCreature, value); + } + /// public event Action OnPlayerDeath { diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index 8c84eb852..dcd708a39 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -665,6 +665,18 @@ public void LockCameraPitch(bool isLocked = true) NWScript.LockCameraPitch(ControlledCreature, isLocked.ToInt()); } + /// + /// Disable a specific gui panel for this player.
+ /// Will close the GUI panel if it is currently open.
+ /// Will fire a event for some panels if a player attempts to open them while disabled. + ///
+ /// The panel type to disable. + /// True to disable the panel, false to re-enable the panel. + public void SetGuiPanelDisabled(GUIPanel panel, bool disabled) + { + NWScript.SetGuiPanelDisabled(ControlledCreature, (int)panel, disabled.ToInt()); + } + /// /// Locks the player's camera distance to its current distance setting, /// or unlocks the player's camera distance. diff --git a/src/main/Anvil/API/Variable/Local/LocalVariableCassowary.cs b/src/main/Anvil/API/Variable/Local/LocalVariableCassowary.cs new file mode 100644 index 000000000..ffae11164 --- /dev/null +++ b/src/main/Anvil/API/Variable/Local/LocalVariableCassowary.cs @@ -0,0 +1,18 @@ +using NWN.Core; + +namespace Anvil.API +{ + public class LocalVariableCassowary : LocalVariable + { + public override Cassowary Value + { + get => NWScript.GetLocalCassowary(Object, Name); + set => NWScript.SetLocalCassowary(Object, Name, value); + } + + public override void Delete() + { + NWScript.DeleteLocalCassowary(Object, Name); + } + } +} From b90b272aba0de170b1fe2184ce2c5d0fac88310f Mon Sep 17 00:00:00 2001 From: Jorteck Date: Sat, 18 Sep 2021 00:56:29 +0200 Subject: [PATCH 03/16] Update build target to 8193.31. --- NWN.Anvil.csproj | 4 ++-- dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NWN.Anvil.csproj b/NWN.Anvil.csproj index 5ead3bbbd..79c1daa13 100644 --- a/NWN.Anvil.csproj +++ b/NWN.Anvil.csproj @@ -38,8 +38,8 @@ - - + + diff --git a/dockerfile b/dockerfile index 6f4eb258e..a5c18b45c 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,5 @@ # Configure nwserver to run with nwnx -FROM nwnxee/unified:01ef335 +FROM nwnxee/unified:da69de7 ARG BINARY_PATH COPY ${BINARY_PATH} /nwn/anvil/ From bdc54fa2d6697709d55a01b50a2c5b93051ee839 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Sat, 18 Sep 2021 00:56:45 +0200 Subject: [PATCH 04/16] Update CResRef.GetDataFromPointer() --- src/main/Anvil/API/Utils/NativeUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/Anvil/API/Utils/NativeUtils.cs b/src/main/Anvil/API/Utils/NativeUtils.cs index 3ee7f2ae2..e8a2a7626 100644 --- a/src/main/Anvil/API/Utils/NativeUtils.cs +++ b/src/main/Anvil/API/Utils/NativeUtils.cs @@ -137,7 +137,7 @@ public static bool DeserializeGff(byte[] serialized, Func Date: Wed, 22 Sep 2021 23:57:16 +0200 Subject: [PATCH 05/16] Update project references to use NWN/NWNX 8193.32. (#372) --- NWN.Anvil.csproj | 4 ++-- dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NWN.Anvil.csproj b/NWN.Anvil.csproj index 79c1daa13..01e4160e9 100644 --- a/NWN.Anvil.csproj +++ b/NWN.Anvil.csproj @@ -38,8 +38,8 @@ - - + + diff --git a/dockerfile b/dockerfile index a5c18b45c..51f93ca72 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,5 @@ # Configure nwserver to run with nwnx -FROM nwnxee/unified:da69de7 +FROM nwnxee/unified:5e0afbb ARG BINARY_PATH COPY ${BINARY_PATH} /nwn/anvil/ From 77bd1501e9dbb13c015fbc4fee919c84016f43ee Mon Sep 17 00:00:00 2001 From: Jorteck Date: Thu, 23 Sep 2021 00:01:02 +0200 Subject: [PATCH 06/16] Add Changelog & Project Credits (#373) * Add project credits. * Add changelog. * Add changelog link. --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 59 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..eda8520e1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog +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/). + +## Unreleased +https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD + +### Added +- Effect: Added `Effect.Icon()` factory method for creating Icon effects. +- ModuleEvents: Added `OnPlayerGuiEvent` and `OnPlayerTarget` events. +- GUIPanel: Added new constants published with NWN 8193.31 +- NwPlayer: Added `SetGuiPanelDisabled` for disabling built-in GUI elements. +- VirtualMachine: Added `RecursionLevel` property. +- LocalVariableCassowary: Added to support cassowary local variables. +- ILateDisposable: Added a new service event interface that is invoked after the server is destroyed. + +### Changed +- Refactored various internal usages of NWN.Native to use collection/list accessors for native types. +- VirtualMachine: `IsInScriptContext` now checks the current executing thread, and now only returns true while on the main thread and inside of a VM script context. +- HookService: Hooks are now returned/disposed after the server has been destroyed. + +### Deprecated +- N/A + +### Removed +- HookService: Removed the optional `shutdownDispose` parameter. + +### Fixed +- Fixed an issue where the `ObjectStorageService` would cause errors when performing hot reloads with `AnvilCore.Reload()` +- Fixed an issue where the PluginLoader would attempt to unload plugins too early during server shutdown/hot reload. +- Fixed an issue where the `EnforceLegalCharacterService` would call the `ELCValidationBefore` event outside of a script context. + +## 8193.26.3 +https://github.com/nwn-dotnet/Anvil/compare/v8193.26.2...v8193.26.3 + +### Fixed +- OnCreatureDamage: Fixed damage hook function returning void instead of an int. Resolves an issue where damage effects would persist on a character, and be applied again after login. + +## 8193.26.2 +https://github.com/nwn-dotnet/Anvil/compare/v8193.26.1...v8193.26.2 + +### Changed +- IEvent: Exposed "Context" property, allowing plugins to implement custom events. + +## 8193.26.1 +https://github.com/nwn-dotnet/Anvil/compare/v8193.26.0...v8193.26.1 + +### Fixed +- OnCreatureDamage: Fixed a server crash when a creature received damage from non-object sources. (EffectDamage from a module/area event, etc.) + +## 8193.26.0 +https://github.com/nwn-dotnet/Anvil/compare/ffd9cd6dd0d6626ebc325265a8a8b370dd74d66b...v8193.26.0 + +### Initial Release! diff --git a/README.md b/README.md index 39dbf7dff..7be94ea71 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Anvil is a C# framework for building behaviours and adding new functionalty to N Builders can add functionality like opening a store from a dialogue with a few lines of code, while plugin developers can leverage [NuGet](https://www.nuget.org/packages) to add new functionality with external packages and the [NWN.Native](https://github.com/nwn-dotnet/NWN.Native) library to completely rewrite existing game systems and mechanics. - Latest [Release](https://github.com/nwn-dotnet/Anvil/releases/latest) +- [Changelog](https://github.com/nwn-dotnet/Anvil/blob/master/CHANGELOG.md) ([Development](https://github.com/nwn-dotnet/Anvil/blob/development/CHANGELOG.md)) - View [Community Submitted Plugins](https://github.com/nwn-dotnet/Anvil/discussions/categories/plugins) - Join the community: [![Discord](https://img.shields.io/discord/714927668826472600?color=7289DA&label=Discord&logo=discord&logoColor=7289DA)](https://discord.gg/gKt495UBgS) @@ -127,3 +128,6 @@ Anvil is bundled with a core set of services that you can depend on in your own These services handle game events, task scheduling, and more! Examples of how to use these services can be found in the [Anvil docs](https://nwn-dotnet.github.io/Anvil/classAnvil_1_1Services_1_1ServiceBindingAttribute.html). You can also find a full list of the services bundled by Anvil [HERE](https://nwn-dotnet.github.io/Anvil/namespaceAnvil_1_1Services.html). + +## Credits +The Anvil Framework builds heavily on the foundations of the [NWNX:EE DotNET plugin](https://github.com/nwnxee/unified/tree/master/Plugins/DotNET) that was written by [Milos Tijanic](https://github.com/mtijanic "Milos Tijanic"), and derives several service implementations from plugins developed by the NWNX:EE team and its contributors. From 2ba214491dc09507c1412611e08a4d3e26aab804 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Thu, 23 Sep 2021 00:29:31 +0200 Subject: [PATCH 07/16] Runtime Script Handlers & Effect Action Functions (#367) * Implement Runtime Script Dispatch Service. * Add EffectRunScript script event. * Fix compile error. * Use handles as parameters in effect functions. Return ScriptCallbackHandle in RuntimeScriptDispatchService. * Rename RuntimeScriptDispatchService to ScriptHandleFactory. * Fix comment warning. * Updated changelog. --- CHANGELOG.md | 5 +- .../API/Constants/EffectRunScriptType.cs | 11 ++ .../API/EngineStructure/Effect.Create.cs | 37 ++++++ .../API/Events/Game/AreaOfEffectEvents.cs | 2 +- .../Anvil/API/Events/Game/GameEventFactory.cs | 2 + .../API/Events/Script/EffectRunScriptEvent.cs | 25 ++++ .../Anvil/API/Extensions/StringExtensions.cs | 19 ++- src/main/Anvil/API/Object/NwObject.cs | 3 +- ...e.cs => AttributeScriptDispatchService.cs} | 4 +- .../ScriptDispatch/IScriptDispatcher.cs | 2 + .../ScriptDispatch/ScriptCallbackHandle.cs | 43 +++++++ ...iceManager.cs => ScriptDispatchService.cs} | 7 +- .../ScriptDispatch/ScriptHandleFactory.cs | 108 ++++++++++++++++++ 13 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 src/main/Anvil/API/Constants/EffectRunScriptType.cs create mode 100644 src/main/Anvil/API/Events/Script/EffectRunScriptEvent.cs rename src/main/Anvil/Services/ScriptDispatch/{AttributeDispatchService.cs => AttributeScriptDispatchService.cs} (93%) create mode 100644 src/main/Anvil/Services/ScriptDispatch/ScriptCallbackHandle.cs rename src/main/Anvil/Services/ScriptDispatch/{DispatchServiceManager.cs => ScriptDispatchService.cs} (74%) create mode 100644 src/main/Anvil/Services/ScriptDispatch/ScriptHandleFactory.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index eda8520e1..59976e835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD ### Added - Effect: Added `Effect.Icon()` factory method for creating Icon effects. +- Effect: Added `Effect.RunAction()` factory methods for creating effects that invoke C# actions. +- ScriptHandleFactory: New service for dynamically creating function callbacks at runtime that are bound to script names. The returned handle is currently used for script parameters in effects. - ModuleEvents: Added `OnPlayerGuiEvent` and `OnPlayerTarget` events. - GUIPanel: Added new constants published with NWN 8193.31 - NwPlayer: Added `SetGuiPanelDisabled` for disabling built-in GUI elements. @@ -19,9 +21,10 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - Refactored various internal usages of NWN.Native to use collection/list accessors for native types. - VirtualMachine: `IsInScriptContext` now checks the current executing thread, and now only returns true while on the main thread and inside of a VM script context. - HookService: Hooks are now returned/disposed after the server has been destroyed. +- IScriptDispatcher: Custom Script Dispatchers must now define an execution order. This order is used when a script call is triggered from the VM, and determines which service/s implementing this interface get executed first. ### Deprecated -- N/A +- Effect: Deprecated `Effect.AreaOfEffect` that uses strings for the script handlers. Use the overload that uses `ScriptCallbackHandle` parameters instead. ### Removed - HookService: Removed the optional `shutdownDispose` parameter. diff --git a/src/main/Anvil/API/Constants/EffectRunScriptType.cs b/src/main/Anvil/API/Constants/EffectRunScriptType.cs new file mode 100644 index 000000000..b3b23a927 --- /dev/null +++ b/src/main/Anvil/API/Constants/EffectRunScriptType.cs @@ -0,0 +1,11 @@ +using NWN.Core; + +namespace Anvil.API +{ + public enum EffectRunScriptType + { + OnApplied = NWScript.RUNSCRIPT_EFFECT_SCRIPT_TYPE_ON_APPLIED, + OnRemoved = NWScript.RUNSCRIPT_EFFECT_SCRIPT_TYPE_ON_REMOVED, + OnInterval = NWScript.RUNSCRIPT_EFFECT_SCRIPT_TYPE_ON_INTERVAL, + } +} diff --git a/src/main/Anvil/API/EngineStructure/Effect.Create.cs b/src/main/Anvil/API/EngineStructure/Effect.Create.cs index 8db51a13b..d34e019d7 100644 --- a/src/main/Anvil/API/EngineStructure/Effect.Create.cs +++ b/src/main/Anvil/API/EngineStructure/Effect.Create.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Anvil.Services; using NWN.Core; namespace Anvil.API @@ -54,11 +55,29 @@ public static Effect Appear() return NWScript.EffectAppear(); } + [Obsolete("Use the overload with ScriptCallBackHandles instead.")] public static Effect AreaOfEffect(int areaEffectId, string onEnterScript = "", string heartbeatScript = "", string onExitScript = "") { return NWScript.EffectAreaOfEffect(areaEffectId, onEnterScript, heartbeatScript, onExitScript); } + /// + /// Creates an area of effect (AOE) effect. + /// + /// The persistent area visual effect to use for this effect. + /// The callback to invoke when something enters this area of effect. + /// The callback to invoke when something is inside the area of effect during a heartbeat (~6 seconds) + /// The callback to invoke when something leaves this area of effect. + /// The created effect. + public static Effect AreaOfEffect(PersistentVfxType vfxType, ScriptCallbackHandle onEnterHandle = null, ScriptCallbackHandle heartbeatHandle = null, ScriptCallbackHandle onExitHandle = null) + { + onEnterHandle?.AssertValid(); + heartbeatHandle?.AssertValid(); + onExitHandle?.AssertValid(); + + return NWScript.EffectAreaOfEffect((int)vfxType, onEnterHandle?.ScriptName ?? string.Empty, heartbeatHandle?.ScriptName ?? string.Empty, onExitHandle?.ScriptName ?? string.Empty); + } + /// /// Creates an effect that applies a penalty to an attack roll. /// @@ -459,6 +478,24 @@ public static Effect Resurrection() return NWScript.EffectResurrection(); } + /// + /// Creates a custom scripted effect. + /// + /// The callback to invoke when this effect is applied. + /// The callback to invoke when this effect is removed (via script, death, resting, or dispelling). + /// The callback to invoke per interval. + /// The interval in which to call onIntervalHandle. + /// Optional string of data saved with the effect, retrievable with Effect.StringParams[0]. + /// The created effect. + public static Effect RunAction(ScriptCallbackHandle onAppliedHandle = null, ScriptCallbackHandle onRemovedHandle = null, ScriptCallbackHandle onIntervalHandle = null, TimeSpan interval = default, string data = "") + { + onAppliedHandle?.AssertValid(); + onRemovedHandle?.AssertValid(); + onIntervalHandle?.AssertValid(); + + return NWScript.EffectRunScript(onAppliedHandle?.ScriptName ?? string.Empty, onRemovedHandle?.ScriptName ?? string.Empty, onIntervalHandle?.ScriptName ?? string.Empty, (float)interval.TotalSeconds, data); + } + public static Effect Sanctuary(int difficultyClass) { return NWScript.EffectSanctuary(difficultyClass); diff --git a/src/main/Anvil/API/Events/Game/AreaOfEffectEvents.cs b/src/main/Anvil/API/Events/Game/AreaOfEffectEvents.cs index abe602353..264f533f3 100644 --- a/src/main/Anvil/API/Events/Game/AreaOfEffectEvents.cs +++ b/src/main/Anvil/API/Events/Game/AreaOfEffectEvents.cs @@ -5,7 +5,7 @@ namespace Anvil.API.Events { /// - /// Events for effects created with . + /// Events for effects created with . /// public static class AreaOfEffectEvents { diff --git a/src/main/Anvil/API/Events/Game/GameEventFactory.cs b/src/main/Anvil/API/Events/Game/GameEventFactory.cs index 91401f3ae..967f38bf3 100644 --- a/src/main/Anvil/API/Events/Game/GameEventFactory.cs +++ b/src/main/Anvil/API/Events/Game/GameEventFactory.cs @@ -17,6 +17,8 @@ public sealed partial class GameEventFactory : IEventFactory EventService { get; init; } + public int ExecutionOrder { get; } = -10000; + // Caches private readonly Dictionary eventInfoCache = new Dictionary(); private readonly Dictionary> eventConstructorCache = new Dictionary>(); diff --git a/src/main/Anvil/API/Events/Script/EffectRunScriptEvent.cs b/src/main/Anvil/API/Events/Script/EffectRunScriptEvent.cs new file mode 100644 index 000000000..0907f5dbf --- /dev/null +++ b/src/main/Anvil/API/Events/Script/EffectRunScriptEvent.cs @@ -0,0 +1,25 @@ +using NWN.Core; + +namespace Anvil.API.Events +{ + public sealed class EffectRunScriptEvent : IEvent + { + public NwObject EffectTarget { get; } + + public Effect Effect { get; } + + public EffectRunScriptType EventType { get; } + + NwObject IEvent.Context + { + get => EffectTarget; + } + + public EffectRunScriptEvent() + { + EffectTarget = NWScript.OBJECT_SELF.ToNwObject(); + Effect = NWScript.GetLastRunScriptEffect(); + EventType = (EffectRunScriptType)NWScript.GetLastRunScriptEffectScriptType(); + } + } +} diff --git a/src/main/Anvil/API/Extensions/StringExtensions.cs b/src/main/Anvil/API/Extensions/StringExtensions.cs index 13be47757..aa60c7f11 100644 --- a/src/main/Anvil/API/Extensions/StringExtensions.cs +++ b/src/main/Anvil/API/Extensions/StringExtensions.cs @@ -126,7 +126,24 @@ public static byte[] ToByteArray(this string base64String) public static bool IsValidScriptName(this string scriptName) { - return scriptName != null && scriptName.Length <= ScriptConstants.MaxScriptNameSize; + if (string.IsNullOrEmpty(scriptName)) + { + return false; + } + + string lowerName = scriptName.ToLower(); + return lowerName != ScriptConstants.GameEventScriptName && lowerName != ScriptConstants.NWNXEventScriptName; + } + + public static bool IsReservedScriptName(this string scriptName) + { + if (string.IsNullOrEmpty(scriptName)) + { + return false; + } + + string lowerName = scriptName.ToLower(); + return lowerName is ScriptConstants.GameEventScriptName or ScriptConstants.NWNXEventScriptName; } public static string ReadUntilChar(this StringReader stringReader, char character) diff --git a/src/main/Anvil/API/Object/NwObject.cs b/src/main/Anvil/API/Object/NwObject.cs index 82e2351f1..3a4e25884 100644 --- a/src/main/Anvil/API/Object/NwObject.cs +++ b/src/main/Anvil/API/Object/NwObject.cs @@ -291,8 +291,7 @@ public void SetEventScript(EventScriptType eventType, string script) /// True if the event is locked and the script cannot be modified, otherwise false. public bool IsEventLocked(EventScriptType eventType) { - string current = GetEventScript(eventType); - return current is ScriptConstants.GameEventScriptName or ScriptConstants.NWNXEventScriptName; + return GetEventScript(eventType).IsReservedScriptName(); } /// diff --git a/src/main/Anvil/Services/ScriptDispatch/AttributeDispatchService.cs b/src/main/Anvil/Services/ScriptDispatch/AttributeScriptDispatchService.cs similarity index 93% rename from src/main/Anvil/Services/ScriptDispatch/AttributeDispatchService.cs rename to src/main/Anvil/Services/ScriptDispatch/AttributeScriptDispatchService.cs index 0de445f44..db475d6d9 100644 --- a/src/main/Anvil/Services/ScriptDispatch/AttributeDispatchService.cs +++ b/src/main/Anvil/Services/ScriptDispatch/AttributeScriptDispatchService.cs @@ -8,7 +8,7 @@ namespace Anvil.Services { [ServiceBinding(typeof(IScriptDispatcher))] [ServiceBinding(typeof(IInitializable))] - internal sealed class AttributeDispatchService : IScriptDispatcher, IInitializable + internal sealed class AttributeScriptDispatchService : IScriptDispatcher, IInitializable { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private const int StartCapacity = 2000; @@ -17,6 +17,8 @@ internal sealed class AttributeDispatchService : IScriptDispatcher, IInitializab [Inject] private Lazy> Services { get; init; } + public int ExecutionOrder { get; } = 10000; + private readonly Dictionary scriptHandlers = new Dictionary(StartCapacity); void IInitializable.Init() diff --git a/src/main/Anvil/Services/ScriptDispatch/IScriptDispatcher.cs b/src/main/Anvil/Services/ScriptDispatch/IScriptDispatcher.cs index 7f08d86f9..a0a03086c 100644 --- a/src/main/Anvil/Services/ScriptDispatch/IScriptDispatcher.cs +++ b/src/main/Anvil/Services/ScriptDispatch/IScriptDispatcher.cs @@ -5,6 +5,8 @@ namespace Anvil.Services /// public interface IScriptDispatcher { + public int ExecutionOrder { get; } + /// /// Called when the game would execute the specified script name. /// diff --git a/src/main/Anvil/Services/ScriptDispatch/ScriptCallbackHandle.cs b/src/main/Anvil/Services/ScriptDispatch/ScriptCallbackHandle.cs new file mode 100644 index 000000000..0452ab70f --- /dev/null +++ b/src/main/Anvil/Services/ScriptDispatch/ScriptCallbackHandle.cs @@ -0,0 +1,43 @@ +using System; +using Anvil.API; + +namespace Anvil.Services +{ + /// + /// Represents a handle for a script callback handle. + /// + public sealed class ScriptCallbackHandle : IDisposable + { + [Inject] + private static ScriptHandleFactory ScriptHandleFactory { get; set; } + + public readonly string ScriptName; + public bool IsValid { get; internal set; } + + private readonly Func callback; + + internal ScriptCallbackHandle(string scriptName, Func callback) + { + ScriptName = scriptName; + this.callback = callback; + } + + internal void AssertValid() + { + if (!IsValid) + { + throw new InvalidOperationException("Attempted to use invalid script callback handle."); + } + } + + internal ScriptHandleResult Invoke(CallInfo callInfo) + { + return callback(callInfo); + } + + public void Dispose() + { + ScriptHandleFactory.UnregisterScriptHandler(ScriptName); + } + } +} diff --git a/src/main/Anvil/Services/ScriptDispatch/DispatchServiceManager.cs b/src/main/Anvil/Services/ScriptDispatch/ScriptDispatchService.cs similarity index 74% rename from src/main/Anvil/Services/ScriptDispatch/DispatchServiceManager.cs rename to src/main/Anvil/Services/ScriptDispatch/ScriptDispatchService.cs index 043d01506..59a25e9b7 100644 --- a/src/main/Anvil/Services/ScriptDispatch/DispatchServiceManager.cs +++ b/src/main/Anvil/Services/ScriptDispatch/ScriptDispatchService.cs @@ -6,17 +6,18 @@ namespace Anvil.Services { - [ServiceBinding(typeof(DispatchServiceManager))] + [ServiceBinding(typeof(ScriptDispatchService))] [ServiceBinding(typeof(ICoreRunScriptHandler))] - internal class DispatchServiceManager : ICoreRunScriptHandler + internal class ScriptDispatchService : ICoreRunScriptHandler { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private readonly List dispatchers; - public DispatchServiceManager(IEnumerable dispatchers) + public ScriptDispatchService(IEnumerable dispatchers) { this.dispatchers = dispatchers.ToList(); + this.dispatchers.Sort((dispatcherA, dispatcherB) => dispatcherA.ExecutionOrder.CompareTo(dispatcherB.ExecutionOrder)); } public int OnRunScript(string script, uint oidSelf) diff --git a/src/main/Anvil/Services/ScriptDispatch/ScriptHandleFactory.cs b/src/main/Anvil/Services/ScriptDispatch/ScriptHandleFactory.cs new file mode 100644 index 000000000..7c58dd173 --- /dev/null +++ b/src/main/Anvil/Services/ScriptDispatch/ScriptHandleFactory.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using Anvil.API; + +namespace Anvil.Services +{ + /// + /// A service for registering C# functions as script handlers dynamically. + /// + [ServiceBinding(typeof(ScriptHandleFactory))] + [ServiceBinding(typeof(IScriptDispatcher))] + public sealed class ScriptHandleFactory : IScriptDispatcher + { + public int ExecutionOrder { get; } = 0; + + private readonly Dictionary activeHandlers = new Dictionary(); + + /// + /// Registers the specified action as a callback for the specified script name. + /// + /// The script name to be handled. + /// The function invoked when this script is called by the Virtual Machine. + /// A handle that can be disposed to remove the handler. + /// Thrown if the specified script name is internally used by Anvil. + /// Thrown if the specified script already has a handler defined. + public ScriptCallbackHandle RegisterScriptHandler(string scriptName, Func callback) + { + if (!scriptName.IsValidScriptName()) + { + throw new ArgumentException("The specified script name is not valid.", scriptName); + } + + if (activeHandlers.ContainsKey(scriptName)) + { + throw new InvalidOperationException($"A handler is already registered for script name {scriptName}"); + } + + ScriptCallbackHandle handle = new ScriptCallbackHandle(scriptName, callback); + activeHandlers.Add(scriptName, handle); + handle.IsValid = true; + + return handle; + } + + /// + /// Creates a unique script callback handle for the specified callback method.
+ /// The returned handle can be used for certain API functions that take a script name as a parameter. + ///
+ /// The callback function. + /// The callback handle. + public ScriptCallbackHandle CreateUniqueHandler(Func handler) + { + string scriptName; + + do + { + scriptName = ResourceNameGenerator.Create(); + } + while (IsScriptRegistered(scriptName)); + + ScriptCallbackHandle handle = RegisterScriptHandler(scriptName, handler); + return handle; + } + + /// + /// Unregisters any handler assigned to the specified script. + /// + /// The script name to unregister. + /// True if a handler was removed, false if nothing was removed. + public bool UnregisterScriptHandler(string scriptName) + { + if (activeHandlers.TryGetValue(scriptName, out ScriptCallbackHandle handle)) + { + handle.IsValid = false; + } + + return activeHandlers.Remove(scriptName); + } + + /// + /// Gets if the specified script name has a script handler already defined. + /// + /// The script name to query. + /// True if a handler already exists, otherwise false. + public bool IsScriptRegistered(string scriptName) + { + return activeHandlers.ContainsKey(scriptName); + } + + ScriptHandleResult IScriptDispatcher.ExecuteScript(string scriptName, uint oidSelf) + { + if (activeHandlers.TryGetValue(scriptName, out ScriptCallbackHandle handler)) + { + if (handler != null) + { + CallInfo callInfo = new CallInfo(scriptName, oidSelf.ToNwObject()); + return handler.Invoke(callInfo); + } + else + { + activeHandlers.Remove(scriptName); + } + } + + return ScriptHandleResult.NotHandled; + } + } +} From 67941e99dde3af834ec078e5598dfc7435a0256a Mon Sep 17 00:00:00 2001 From: Jorteck Date: Tue, 5 Oct 2021 20:19:18 +0200 Subject: [PATCH 08/16] Add Optional Plugin Dependency Support (#381) * Remove custom type loader support. Move plugin resource loading to new class. Add service plugin dependencies. * Update changelog. * Fix analysis warnings. --- CHANGELOG.md | 2 + src/main/Anvil/AnvilCore.cs | 18 ++-- src/main/Anvil/Plugins/ITypeLoader.cs | 16 ---- src/main/Anvil/Plugins/PluginLoadContext.cs | 8 +- .../{PluginLoader.cs => PluginManager.cs} | 46 ++++++---- .../Anvil/Plugins/PluginResourceManager.cs | 17 ++++ .../ResourceManager/ResourceManager.cs | 55 +++++------ .../Services/AnvilContainerFactory.cs | 91 ++++++++++++------- .../Services/Services/IContainerFactory.cs | 2 +- .../Services/Services/InjectionService.cs | 4 +- .../ServiceBindingOptionsAttribute.cs | 10 ++ .../Anvil/Services/Services/ServiceManager.cs | 10 +- 12 files changed, 159 insertions(+), 120 deletions(-) delete mode 100644 src/main/Anvil/Plugins/ITypeLoader.cs rename src/main/Anvil/Plugins/{PluginLoader.cs => PluginManager.cs} (94%) create mode 100644 src/main/Anvil/Plugins/PluginResourceManager.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 59976e835..ebdf1c448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - VirtualMachine: Added `RecursionLevel` property. - LocalVariableCassowary: Added to support cassowary local variables. - ILateDisposable: Added a new service event interface that is invoked after the server is destroyed. +- ServiceBindingOptions: Added `PluginDependencies` and `MissingPluginDependencies` properties for setting up services with optional plugin dependencies. ### Changed - Refactored various internal usages of NWN.Native to use collection/list accessors for native types. @@ -28,6 +29,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD ### Removed - HookService: Removed the optional `shutdownDispose` parameter. +- AnvilCore: Removed custom `ITypeLoader` support, and hardcoded references to the updated PluginManager. ### Fixed - Fixed an issue where the `ObjectStorageService` would cause errors when performing hot reloads with `AnvilCore.Reload()` diff --git a/src/main/Anvil/AnvilCore.cs b/src/main/Anvil/AnvilCore.cs index 28ca023c7..fe177edcb 100644 --- a/src/main/Anvil/AnvilCore.cs +++ b/src/main/Anvil/AnvilCore.cs @@ -13,7 +13,7 @@ namespace Anvil { /// /// Handles bootstrap and interop between %NWN, %NWN.Core and the %Anvil %API. The entry point of the implementing module should point to this class.
- /// Until is called, all APIs are unavailable for usage. + /// Until is called, all APIs are unavailable for usage. ///
public sealed class AnvilCore : IServerLifeCycleEventHandler { @@ -24,7 +24,7 @@ public sealed class AnvilCore : IServerLifeCycleEventHandler // Core Services private CoreInteropHandler interopHandler; private IContainerFactory containerFactory; - private ITypeLoader typeLoader; + private PluginManager pluginManager; private LoggerManager loggerManager; private UnhandledExceptionLogger unhandledExceptionLogger; private ServiceManager serviceManager; @@ -35,18 +35,16 @@ public sealed class AnvilCore : IServerLifeCycleEventHandler /// The NativeHandles pointer, provided by the NWNX bootstrap entry point. /// The size of the NativeHandles bootstrap structure, provided by the NWNX entry point. /// An optional custom binding installer to use instead of the default . - /// An optional type loader to use instead of the default . /// The init result code to return back to NWNX. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int Init(IntPtr arg, int argLength, IContainerFactory containerFactory = default, ITypeLoader typeLoader = default) + public static int Init(IntPtr arg, int argLength, IContainerFactory containerFactory = default) { - typeLoader ??= new PluginLoader(); containerFactory ??= new AnvilContainerFactory(); instance = new AnvilCore(); instance.interopHandler = new CoreInteropHandler(instance); instance.containerFactory = containerFactory; - instance.typeLoader = typeLoader; + instance.pluginManager = new PluginManager(); instance.loggerManager = new LoggerManager(); instance.unhandledExceptionLogger = new UnhandledExceptionLogger(); @@ -71,10 +69,12 @@ public static async void Reload() instance.serviceManager.ShutdownServices(); instance.serviceManager.ShutdownLateServices(); + instance.pluginManager.Unload(); GC.Collect(); GC.WaitForPendingFinalizers(); + instance.pluginManager.Load(); instance.InitServices(); } @@ -128,8 +128,8 @@ private void InitCore() private void InitServices() { - typeLoader.Init(); - serviceManager = new ServiceManager(typeLoader, containerFactory); + pluginManager.Load(); + serviceManager = new ServiceManager(pluginManager, containerFactory); serviceManager.Init(); interopHandler.Init(serviceManager.GetService(), serviceManager.GetService()); } @@ -138,7 +138,7 @@ private void ShutdownCore() { serviceManager = null; - typeLoader.Dispose(); + pluginManager.Unload(); unhandledExceptionLogger.Dispose(); loggerManager.Dispose(); } diff --git a/src/main/Anvil/Plugins/ITypeLoader.cs b/src/main/Anvil/Plugins/ITypeLoader.cs deleted file mode 100644 index 33e8d382e..000000000 --- a/src/main/Anvil/Plugins/ITypeLoader.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Anvil.Plugins -{ - public interface ITypeLoader - { - void Init(); - - IReadOnlyCollection LoadedTypes { get; } - - IReadOnlyCollection ResourcePaths { get; } - - void Dispose(); - } -} diff --git a/src/main/Anvil/Plugins/PluginLoadContext.cs b/src/main/Anvil/Plugins/PluginLoadContext.cs index 5846df9e7..caac5ce75 100644 --- a/src/main/Anvil/Plugins/PluginLoadContext.cs +++ b/src/main/Anvil/Plugins/PluginLoadContext.cs @@ -7,14 +7,14 @@ namespace Anvil.Plugins { internal sealed class PluginLoadContext : AssemblyLoadContext { - private readonly PluginLoader pluginLoader; + private readonly PluginManager pluginManager; private readonly string pluginName; private readonly AssemblyDependencyResolver resolver; - public PluginLoadContext(PluginLoader pluginLoader, string pluginPath, string pluginName) : base(EnvironmentConfig.ReloadEnabled) + public PluginLoadContext(PluginManager pluginManager, string pluginPath, string pluginName) : base(EnvironmentConfig.ReloadEnabled) { - this.pluginLoader = pluginLoader; + this.pluginManager = pluginManager; this.pluginName = pluginName; resolver = new AssemblyDependencyResolver(pluginPath); } @@ -28,7 +28,7 @@ protected override Assembly Load(AssemblyName assemblyName) } // Resolve the dependency with the bundled assemblies (NWN.Core/Anvil), then check if other plugins can provide the dependency. - Assembly assembly = pluginLoader.ResolveDependency(pluginName, assemblyName); + Assembly assembly = pluginManager.ResolveDependency(pluginName, assemblyName); if (assembly != null) { diff --git a/src/main/Anvil/Plugins/PluginLoader.cs b/src/main/Anvil/Plugins/PluginManager.cs similarity index 94% rename from src/main/Anvil/Plugins/PluginLoader.cs rename to src/main/Anvil/Plugins/PluginManager.cs index cfa8a784f..11953a211 100644 --- a/src/main/Anvil/Plugins/PluginLoader.cs +++ b/src/main/Anvil/Plugins/PluginManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Anvil.Internal; using Anvil.Services; @@ -12,20 +13,25 @@ namespace Anvil.Plugins /// Loads all available plugins and their types for service initialisation. ///
[ServiceBindingOptions(BindingOrder.Core)] - internal sealed class PluginLoader : ITypeLoader + public sealed class PluginManager { private const string PluginResourceDir = "resources"; private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - public IReadOnlyCollection LoadedTypes { get; private set; } + internal IReadOnlyCollection LoadedTypes { get; private set; } - public IReadOnlyCollection ResourcePaths { get; private set; } + internal IReadOnlyCollection ResourcePaths { get; private set; } private readonly HashSet loadedAssemblies = new HashSet(); private readonly List plugins = new List(); - public void Init() + public bool IsPluginLoaded(string pluginName) + { + return plugins.Any(plugin => plugin.AssemblyName.Name == pluginName); + } + + internal void Load() { LoadCore(); BootstrapPlugins(); @@ -35,6 +41,22 @@ public void Init() ResourcePaths = GetResourcePaths(); } + internal void Unload() + { + loadedAssemblies.Clear(); + LoadedTypes = null; + ResourcePaths = null; + + Log.Info("Unloading plugins..."); + foreach (Plugin plugin in plugins) + { + plugin.Dispose(); + Log.Info("Unloaded DotNET plugin {PluginName} - {PluginPath}", plugin.AssemblyName.Name, plugin.PluginPath); + } + + plugins.Clear(); + } + private void LoadCore() { foreach (Assembly assembly in Assemblies.AllAssemblies) @@ -209,21 +231,5 @@ private IReadOnlyCollection GetResourcePaths() return resourcePaths.AsReadOnly(); } - - void ITypeLoader.Dispose() - { - loadedAssemblies.Clear(); - LoadedTypes = null; - ResourcePaths = null; - - Log.Info("Unloading plugins..."); - foreach (Plugin plugin in plugins) - { - plugin.Dispose(); - Log.Info("Unloaded DotNET plugin {PluginName} - {PluginPath}", plugin.AssemblyName.Name, plugin.PluginPath); - } - - plugins.Clear(); - } } } diff --git a/src/main/Anvil/Plugins/PluginResourceManager.cs b/src/main/Anvil/Plugins/PluginResourceManager.cs new file mode 100644 index 000000000..c9045d3ab --- /dev/null +++ b/src/main/Anvil/Plugins/PluginResourceManager.cs @@ -0,0 +1,17 @@ +using Anvil.Services; + +namespace Anvil.Plugins +{ + [ServiceBinding(typeof(PluginResourceManager))] + [ServiceBindingOptions(BindingOrder.API)] + internal sealed class PluginResourceManager + { + public PluginResourceManager(PluginManager pluginManager, ResourceManager resourceManager) + { + foreach (string resourcePath in pluginManager.ResourcePaths) + { + resourceManager.CreateResourceDirectory(resourcePath); + } + } + } +} diff --git a/src/main/Anvil/Services/ResourceManager/ResourceManager.cs b/src/main/Anvil/Services/ResourceManager/ResourceManager.cs index 6ef6074e1..173d40ea5 100644 --- a/src/main/Anvil/Services/ResourceManager/ResourceManager.cs +++ b/src/main/Anvil/Services/ResourceManager/ResourceManager.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using Anvil.API; using Anvil.Internal; -using Anvil.Plugins; using NLog; using NWN.Native.API; using ResRefType = Anvil.API.ResRefType; @@ -13,6 +12,7 @@ namespace Anvil.Services { [ServiceBinding(typeof(ResourceManager))] + [ServiceBindingOptions(BindingOrder.API)] public sealed class ResourceManager : IDisposable { public const int MaxNameLength = 16; @@ -30,7 +30,7 @@ public sealed class ResourceManager : IDisposable private uint currentIndex; - public ResourceManager(ITypeLoader typeLoader) + public ResourceManager() { if (Directory.Exists(EnvironmentConfig.ResourcePath)) { @@ -38,11 +38,6 @@ public ResourceManager(ITypeLoader typeLoader) } tempAlias = CreateResourceDirectory(EnvironmentConfig.ResourcePath).ToExoString(); - - foreach (string resourcePath in typeLoader.ResourcePaths) - { - CreateResourceDirectory(resourcePath); - } } public void WriteTempResource(string resourceName, byte[] data) @@ -130,29 +125,7 @@ public unsafe string GetNSSContents(CExoString scriptName) return null; } - private unsafe byte[] GetStandardResourceData(string name, ushort type) - { - CResRef resRef = new CResRef(name); - if (!ResMan.Exists(resRef, type).ToBool()) - { - return null; - } - - CRes res = ResMan.GetResObject(resRef, type); - if (res == null) - { - return null; - } - - void* data = res.GetData(); - int size = res.GetSize(); - - byte[] retVal = new byte[res.m_nSize]; - Marshal.Copy((IntPtr)data, retVal, 0, size); - return retVal; - } - - private string CreateResourceDirectory(string path) + internal string CreateResourceDirectory(string path) { if (string.IsNullOrEmpty(path)) { @@ -175,6 +148,28 @@ private string CreateResourceDirectory(string path) return alias; } + private unsafe byte[] GetStandardResourceData(string name, ushort type) + { + CResRef resRef = new CResRef(name); + if (!ResMan.Exists(resRef, type).ToBool()) + { + return null; + } + + CRes res = ResMan.GetResObject(resRef, type); + if (res == null) + { + return null; + } + + void* data = res.GetData(); + int size = res.GetSize(); + + byte[] retVal = new byte[res.m_nSize]; + Marshal.Copy((IntPtr)data, retVal, 0, size); + return retVal; + } + void IDisposable.Dispose() { if (Directory.Exists(EnvironmentConfig.ResourcePath)) diff --git a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs index dc5e45ec1..85e766e30 100644 --- a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs +++ b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using Anvil.API; using Anvil.Plugins; @@ -14,12 +15,12 @@ public class AnvilContainerFactory : IContainerFactory { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - protected ITypeLoader TypeLoader; + protected PluginManager PluginManager; protected ServiceContainer ServiceContainer; - public ServiceContainer Setup(ITypeLoader typeLoader) + public ServiceContainer Setup(PluginManager pluginManager) { - TypeLoader = typeLoader; + PluginManager = pluginManager; ServiceContainer = new ServiceContainer(new ContainerOptions { EnablePropertyInjection = true, EnableVariance = false }); SetupInjectPropertySelector(); @@ -38,43 +39,47 @@ public void RegisterCoreService(T instance) public void BuildContainer() { - SearchForBindings(); + Log.Info("Loading services..."); + foreach (Type type in PluginManager.LoadedTypes) + { + TryRegisterType(type); + } + RegisterOverrides(); } - private void SetupInjectPropertySelector() - { - InjectPropertySelector propertySelector = new InjectPropertySelector(InjectPropertyTypes.InstanceOnly); - ServiceContainer.PropertyDependencySelector = new InjectPropertyDependencySelector(propertySelector); - } + /// + /// Override in a child class to specify additional bindings/overrides.
+ /// See https://www.lightinject.net/ for documentation. + ///
+ protected virtual void RegisterOverrides() {} - private void SearchForBindings() + private void TryRegisterType(Type type) { - Log.Info("Loading services..."); - - foreach (Type type in TypeLoader.LoadedTypes) + if (!type.IsClass || type.IsAbstract || type.ContainsGenericParameters) { - if (!type.IsClass || type.IsAbstract || type.ContainsGenericParameters) - { - continue; - } - - RegisterBindings(type, type.GetCustomAttributes()); + return; } - } - private void RegisterBindings(Type bindTo, ServiceBindingAttribute[] newBindings) - { - if (newBindings.Length == 0) + ServiceBindingAttribute[] bindings = type.GetCustomAttributes(); + if (bindings.Length == 0) { return; } - ServiceBindingOptionsAttribute options = bindTo.GetCustomAttribute(); + ServiceBindingOptionsAttribute options = type.GetCustomAttribute(); + if (IsServiceRequirementsMet(options)) + { + RegisterBindings(type, bindings, options); + } + } + + private void RegisterBindings(Type bindTo, ServiceBindingAttribute[] bindings, ServiceBindingOptionsAttribute options) + { string serviceName = GetServiceName(bindTo, options); PerContainerLifetime lifeTime = new PerContainerLifetime(); - RegisterExplicitBindings(bindTo, newBindings, serviceName, lifeTime); + RegisterExplicitBindings(bindTo, bindings, serviceName, lifeTime); if (options is not { Lazy: true }) { @@ -84,10 +89,24 @@ private void RegisterBindings(Type bindTo, ServiceBindingAttribute[] newBindings Log.Info("Registered service {Service}", bindTo.FullName); } - private string GetServiceName(Type implementation, ServiceBindingOptionsAttribute options) + private bool IsServiceRequirementsMet(ServiceBindingOptionsAttribute options) { - short bindingOrder = options?.Order ?? (short)BindingOrder.Default; - return bindingOrder.ToString("D5") + implementation.FullName; + if (options == null || options.PluginDependencies == null && options.MissingPluginDependencies == null) + { + return true; + } + + if (options.PluginDependencies != null && options.PluginDependencies.Any(dependency => !PluginManager.IsPluginLoaded(dependency))) + { + return false; + } + + if (options.MissingPluginDependencies != null && options.MissingPluginDependencies.Any(dependency => PluginManager.IsPluginLoaded(dependency))) + { + return false; + } + + return true; } private void RegisterImplicitBindings(Type bindTo, string serviceName, ILifetime lifeTime) @@ -114,10 +133,16 @@ private void RegisterExplicitBindings(Type bindTo, ServiceBindingAttribute[] new } } - /// - /// Override in a child class to specify additional bindings/overrides.
- /// See https://www.lightinject.net/ for documentation. - ///
- protected virtual void RegisterOverrides() {} + private void SetupInjectPropertySelector() + { + InjectPropertySelector propertySelector = new InjectPropertySelector(InjectPropertyTypes.InstanceOnly); + ServiceContainer.PropertyDependencySelector = new InjectPropertyDependencySelector(propertySelector); + } + + private string GetServiceName(Type implementation, ServiceBindingOptionsAttribute options) + { + short bindingOrder = options?.Order ?? (short)BindingOrder.Default; + return bindingOrder.ToString("D5") + implementation.FullName; + } } } diff --git a/src/main/Anvil/Services/Services/IContainerFactory.cs b/src/main/Anvil/Services/Services/IContainerFactory.cs index 9d72bb7a3..356b8e0a8 100644 --- a/src/main/Anvil/Services/Services/IContainerFactory.cs +++ b/src/main/Anvil/Services/Services/IContainerFactory.cs @@ -5,7 +5,7 @@ namespace Anvil.Services { public interface IContainerFactory { - ServiceContainer Setup(ITypeLoader typeLoader); + ServiceContainer Setup(PluginManager pluginManager); void RegisterCoreService(T instance); diff --git a/src/main/Anvil/Services/Services/InjectionService.cs b/src/main/Anvil/Services/Services/InjectionService.cs index a9c41ee99..5e846735b 100644 --- a/src/main/Anvil/Services/Services/InjectionService.cs +++ b/src/main/Anvil/Services/Services/InjectionService.cs @@ -12,10 +12,10 @@ public sealed class InjectionService { private readonly IServiceContainer container; - public InjectionService(IServiceContainer container, ITypeLoader typeLoader) + public InjectionService(IServiceContainer container, PluginManager pluginManager) { this.container = container; - InjectStaticProperties(typeLoader.LoadedTypes); + InjectStaticProperties(pluginManager.LoadedTypes); } /// diff --git a/src/main/Anvil/Services/Services/ServiceBindingOptionsAttribute.cs b/src/main/Anvil/Services/Services/ServiceBindingOptionsAttribute.cs index 2da9e1fe4..acd256483 100644 --- a/src/main/Anvil/Services/Services/ServiceBindingOptionsAttribute.cs +++ b/src/main/Anvil/Services/Services/ServiceBindingOptionsAttribute.cs @@ -32,6 +32,16 @@ public short Order /// public bool Lazy { get; init; } + /// + /// An optional list of plugin names that must exist for this service to be loaded. + /// + public string[] PluginDependencies { get; init; } + + /// + /// An optional list of plugin names that must be missing for this service to be loaded. + /// + public string[] MissingPluginDependencies { get; init; } + internal ServiceBindingOptionsAttribute(BindingOrder order) { this.order = (short)order; diff --git a/src/main/Anvil/Services/Services/ServiceManager.cs b/src/main/Anvil/Services/Services/ServiceManager.cs index 03b3f9d0d..62e3a907d 100644 --- a/src/main/Anvil/Services/Services/ServiceManager.cs +++ b/src/main/Anvil/Services/Services/ServiceManager.cs @@ -11,20 +11,20 @@ internal sealed class ServiceManager { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - private readonly ITypeLoader typeLoader; + private readonly PluginManager pluginManager; private readonly IContainerFactory containerFactory; private ServiceContainer serviceContainer; private List lateDisposables; - internal ServiceManager(ITypeLoader typeLoader, IContainerFactory containerFactory) + internal ServiceManager(PluginManager pluginManager, IContainerFactory containerFactory) { Log.Info("Using {ContainerFactory} to install service bindings", containerFactory.GetType().FullName); - this.typeLoader = typeLoader; + this.pluginManager = pluginManager; this.containerFactory = containerFactory; - serviceContainer = containerFactory.Setup(typeLoader); + serviceContainer = containerFactory.Setup(pluginManager); } public T GetService() where T : class @@ -34,7 +34,7 @@ public T GetService() where T : class internal void Init() { - RegisterCoreService(typeLoader); + RegisterCoreService(pluginManager); RegisterCoreService(this); containerFactory.BuildContainer(); From 913d688e4e9b2e05368694816ea4a774c29787b2 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Tue, 5 Oct 2021 21:25:26 +0200 Subject: [PATCH 09/16] Update Anvil to target NWN 8193.33. (#382) * Update Anvil to target NWN 8193.33. * Add migration note. --- NWN.Anvil.csproj | 4 ++-- README.md | 8 ++++++++ dockerfile | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/NWN.Anvil.csproj b/NWN.Anvil.csproj index 01e4160e9..73ed28fa7 100644 --- a/NWN.Anvil.csproj +++ b/NWN.Anvil.csproj @@ -38,8 +38,8 @@ - - + + diff --git a/README.md b/README.md index 7be94ea71..ace58c95f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ Builders can add functionality like opening a store from a dialogue with a few l # Getting Started +### I use NWNX/NWScript code extensively. How hard is it to move to Anvil? + +Anvil uses NWNX's DotNET plugin and is 100% compatible with existing NWNX/NWN servers. + +If you are using docker, you can simply swap the NWN/NWNX image with Anvil's image. + +All of your existing code will work of out the box, while offering the C# framework for you to begin developing plugins. + ### Running Anvil - Docker Anvil has its own docker images that are automatically configured to start and run Anvil and NWNXEE. Similar to the parent images, Anvil is configured by environment variables passed during `docker run`. diff --git a/dockerfile b/dockerfile index 51f93ca72..23de6e717 100644 --- a/dockerfile +++ b/dockerfile @@ -1,5 +1,5 @@ # Configure nwserver to run with nwnx -FROM nwnxee/unified:5e0afbb +FROM nwnxee/unified:c3e95f6 ARG BINARY_PATH COPY ${BINARY_PATH} /nwn/anvil/ From 1e46ac1f924b082d0d4fcba20150828ceca026ad Mon Sep 17 00:00:00 2001 From: Jorteck Date: Tue, 5 Oct 2021 22:20:22 +0200 Subject: [PATCH 10/16] Fix ChatService Target always being null. (#383) * nTellPlayerId is a player ID, not an object ID. * Update changelog. --- CHANGELOG.md | 1 + src/main/Anvil/API/Object/NwPlayer.cs | 6 ++++++ src/main/Anvil/Services/Chat/ChatService.cs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebdf1c448..a28f200ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - Fixed an issue where the `ObjectStorageService` would cause errors when performing hot reloads with `AnvilCore.Reload()` - Fixed an issue where the PluginLoader would attempt to unload plugins too early during server shutdown/hot reload. - Fixed an issue where the `EnforceLegalCharacterService` would call the `ELCValidationBefore` event outside of a script context. +- Fixed `OnChatMessageSend.Target` always being null. ## 8193.26.3 https://github.com/nwn-dotnet/Anvil/compare/v8193.26.2...v8193.26.3 diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index f3e3e8374..95f733415 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -28,6 +28,12 @@ internal NwPlayer(CNWSPlayer player) PlayerId = player.m_nPlayerID; } + internal static NwPlayer FromPlayerId(uint playerId) + { + CNWSPlayer player = LowLevel.ServerExoApp.GetClientObjectByObjectId(playerId); + return player != null ? new NwPlayer(player) : null; + } + public static implicit operator CNWSPlayer(NwPlayer player) { return player?.Player; diff --git a/src/main/Anvil/Services/Chat/ChatService.cs b/src/main/Anvil/Services/Chat/ChatService.cs index 7814a369a..88c06c8e4 100644 --- a/src/main/Anvil/Services/Chat/ChatService.cs +++ b/src/main/Anvil/Services/Chat/ChatService.cs @@ -187,7 +187,7 @@ private int OnSendServerToPlayerChatMessage(void* pMessage, ChatChannel nChatMes CExoString speakerMessage = CExoString.FromPointer(sSpeakerMessage); NwObject speaker = oidSpeaker.ToNwObject(); - bool skipMessage = ProcessEvent(nChatMessageType, speakerMessage.ToString(), speaker, nTellPlayerId.ToNwPlayer()); + bool skipMessage = ProcessEvent(nChatMessageType, speakerMessage.ToString(), speaker, NwPlayer.FromPlayerId(nTellPlayerId)); if (skipMessage) { return false.ToInt(); From 53ac3c19af46b6203cd7d5abff13d5fa1ed9b7e7 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Mon, 11 Oct 2021 00:22:54 +0200 Subject: [PATCH 11/16] Use correct method when creating a NwPlayer from a player ID. (#384) --- src/main/Anvil/API/Object/NwPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index 95f733415..5c4b85d5c 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -30,7 +30,7 @@ internal NwPlayer(CNWSPlayer player) internal static NwPlayer FromPlayerId(uint playerId) { - CNWSPlayer player = LowLevel.ServerExoApp.GetClientObjectByObjectId(playerId); + CNWSPlayer player = LowLevel.ServerExoApp.GetClientObjectByPlayerId(playerId, 0)?.AsNWSPlayer(); return player != null ? new NwPlayer(player) : null; } From 8f4227acf1183af41e5815d9a0941d80e775ef78 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Mon, 11 Oct 2021 00:26:13 +0200 Subject: [PATCH 12/16] Implement NUI Support (#374) * Add constants. * Add Newtonsoft.Json. * Add Json engine structure. * Add NUI layout elements. * Ignore NUI folders for namespaces. * Add NuiLabel * Add NuiColor. * Implement NuiProperty/NuiBind/NuiValue. * Rename FromValue -> CreateValue * Add various widgets. * Implement DrawList types. * Implement NuiCombo. * Add misc NUI types. * Add NUI creation APIs. * Update namespace ignores. * Fix warnings. * Make NuiDrawList inherit NuiElement. * Update NuiProgress value type. * Add ForegroundColor option. Remove null type from output. * Fix warnings and analysis. * Add new constants and device properties. * Implement Nui module event/misc functions. * Add additional event types. * Add NuiSetGroupLayout. * Type safety. * Create constructors for required parameters. Rename NuiCol -> NuiColumn. * Add docs. * Update changelog. --- CHANGELOG.md | 1 + NWN.Anvil.csproj | 1 + NWN.Anvil.csproj.DotSettings | 7 + .../Anvil/API/Constants/EventScriptType.cs | 1 + src/main/Anvil/API/Constants/GUIPanel.cs | 7 + src/main/Anvil/API/Constants/GuiEventType.cs | 11 ++ src/main/Anvil/API/Constants/NuiEventType.cs | 15 ++ .../API/Constants/PlayerDeviceProperty.cs | 18 ++ .../Anvil/API/Constants/PlayerLanguage.cs | 15 ++ .../Anvil/API/Constants/PlayerPlatform.cs | 24 +++ src/main/Anvil/API/EngineStructure/Json.cs | 30 ++++ .../Anvil/API/Events/Game/ModuleEvents.cs | 85 ++++++++++ src/main/Anvil/API/Nui/Bindings/NuiBind.cs | 58 +++++++ .../Anvil/API/Nui/Bindings/NuiProperty.cs | 34 ++++ src/main/Anvil/API/Nui/Bindings/NuiValue.cs | 27 +++ .../API/Nui/Bindings/NuiValueConverter.cs | 52 ++++++ src/main/Anvil/API/Nui/Constants/NuiAspect.cs | 12 ++ .../Anvil/API/Nui/Constants/NuiChartType.cs | 8 + .../Anvil/API/Nui/Constants/NuiDirection.cs | 8 + .../API/Nui/Constants/NuiDrawListItemType.cs | 12 ++ src/main/Anvil/API/Nui/Constants/NuiHAlign.cs | 9 + .../Anvil/API/Nui/Constants/NuiMouseButton.cs | 9 + .../Anvil/API/Nui/Constants/NuiScrollbars.cs | 11 ++ src/main/Anvil/API/Nui/Constants/NuiStyle.cs | 16 ++ src/main/Anvil/API/Nui/Constants/NuiVAlign.cs | 9 + .../Anvil/API/Nui/DrawList/NuiDrawList.cs | 19 +++ .../Anvil/API/Nui/DrawList/NuiDrawListArc.cs | 33 ++++ .../API/Nui/DrawList/NuiDrawListCircle.cs | 24 +++ .../API/Nui/DrawList/NuiDrawListCurve.cs | 30 ++++ .../API/Nui/DrawList/NuiDrawListImage.cs | 27 +++ .../Anvil/API/Nui/DrawList/NuiDrawListItem.cs | 13 ++ .../API/Nui/DrawList/NuiDrawListPolyLine.cs | 25 +++ .../Anvil/API/Nui/DrawList/NuiDrawListText.cs | 21 +++ src/main/Anvil/API/Nui/Layout/NuiColumn.cs | 10 ++ src/main/Anvil/API/Nui/Layout/NuiGroup.cs | 19 +++ src/main/Anvil/API/Nui/Layout/NuiLayout.cs | 11 ++ src/main/Anvil/API/Nui/Layout/NuiRow.cs | 10 ++ src/main/Anvil/API/Nui/NuiColor.cs | 56 ++++++ src/main/Anvil/API/Nui/NuiElement.cs | 75 +++++++++ src/main/Anvil/API/Nui/NuiRect.cs | 27 +++ src/main/Anvil/API/Nui/NuiVector.cs | 66 ++++++++ src/main/Anvil/API/Nui/NuiWindow.cs | 79 +++++++++ .../Anvil/API/Nui/ObjectToArrayConverter.cs | 159 ++++++++++++++++++ src/main/Anvil/API/Nui/Widgets/NuiButton.cs | 23 +++ .../Anvil/API/Nui/Widgets/NuiButtonImage.cs | 23 +++ .../Anvil/API/Nui/Widgets/NuiButtonSelect.cs | 28 +++ src/main/Anvil/API/Nui/Widgets/NuiChart.cs | 19 +++ .../Anvil/API/Nui/Widgets/NuiChartSlot.cs | 31 ++++ src/main/Anvil/API/Nui/Widgets/NuiCheck.cs | 27 +++ .../Anvil/API/Nui/Widgets/NuiColorPicker.cs | 23 +++ src/main/Anvil/API/Nui/Widgets/NuiCombo.cs | 22 +++ .../Anvil/API/Nui/Widgets/NuiComboEntry.cs | 23 +++ src/main/Anvil/API/Nui/Widgets/NuiImage.cs | 32 ++++ src/main/Anvil/API/Nui/Widgets/NuiLabel.cs | 29 ++++ src/main/Anvil/API/Nui/Widgets/NuiOptions.cs | 26 +++ src/main/Anvil/API/Nui/Widgets/NuiProgress.cs | 26 +++ src/main/Anvil/API/Nui/Widgets/NuiSlider.cs | 34 ++++ .../Anvil/API/Nui/Widgets/NuiSliderFloat.cs | 34 ++++ src/main/Anvil/API/Nui/Widgets/NuiSpacer.cs | 14 ++ src/main/Anvil/API/Nui/Widgets/NuiText.cs | 23 +++ src/main/Anvil/API/Nui/Widgets/NuiTextEdit.cs | 35 ++++ src/main/Anvil/API/Object/NwPlayer.cs | 112 ++++++++++++ src/main/Anvil/Internal/Assemblies.cs | 1 + 63 files changed, 1769 insertions(+) create mode 100644 src/main/Anvil/API/Constants/NuiEventType.cs create mode 100644 src/main/Anvil/API/Constants/PlayerDeviceProperty.cs create mode 100644 src/main/Anvil/API/Constants/PlayerLanguage.cs create mode 100644 src/main/Anvil/API/Constants/PlayerPlatform.cs create mode 100644 src/main/Anvil/API/EngineStructure/Json.cs create mode 100644 src/main/Anvil/API/Nui/Bindings/NuiBind.cs create mode 100644 src/main/Anvil/API/Nui/Bindings/NuiProperty.cs create mode 100644 src/main/Anvil/API/Nui/Bindings/NuiValue.cs create mode 100644 src/main/Anvil/API/Nui/Bindings/NuiValueConverter.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiAspect.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiChartType.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiDirection.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiDrawListItemType.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiHAlign.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiMouseButton.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiScrollbars.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiStyle.cs create mode 100644 src/main/Anvil/API/Nui/Constants/NuiVAlign.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawList.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListArc.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListCircle.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListCurve.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListImage.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListItem.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListPolyLine.cs create mode 100644 src/main/Anvil/API/Nui/DrawList/NuiDrawListText.cs create mode 100644 src/main/Anvil/API/Nui/Layout/NuiColumn.cs create mode 100644 src/main/Anvil/API/Nui/Layout/NuiGroup.cs create mode 100644 src/main/Anvil/API/Nui/Layout/NuiLayout.cs create mode 100644 src/main/Anvil/API/Nui/Layout/NuiRow.cs create mode 100644 src/main/Anvil/API/Nui/NuiColor.cs create mode 100644 src/main/Anvil/API/Nui/NuiElement.cs create mode 100644 src/main/Anvil/API/Nui/NuiRect.cs create mode 100644 src/main/Anvil/API/Nui/NuiVector.cs create mode 100644 src/main/Anvil/API/Nui/NuiWindow.cs create mode 100644 src/main/Anvil/API/Nui/ObjectToArrayConverter.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiButton.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiButtonImage.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiButtonSelect.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiChart.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiChartSlot.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiCheck.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiColorPicker.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiCombo.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiComboEntry.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiImage.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiLabel.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiOptions.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiProgress.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiSlider.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiSliderFloat.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiSpacer.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiText.cs create mode 100644 src/main/Anvil/API/Nui/Widgets/NuiTextEdit.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index a28f200ba..6794bdfde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD ### Added +- NUI: Implement API with classes, added methods to NwPlayer. - Effect: Added `Effect.Icon()` factory method for creating Icon effects. - Effect: Added `Effect.RunAction()` factory methods for creating effects that invoke C# actions. - ScriptHandleFactory: New service for dynamically creating function callbacks at runtime that are bound to script names. The returned handle is currently used for script parameters in effects. diff --git a/NWN.Anvil.csproj b/NWN.Anvil.csproj index 73ed28fa7..e3a59ed8e 100644 --- a/NWN.Anvil.csproj +++ b/NWN.Anvil.csproj @@ -37,6 +37,7 @@ + diff --git a/NWN.Anvil.csproj.DotSettings b/NWN.Anvil.csproj.DotSettings index 9a02301d4..d10ccd36b 100644 --- a/NWN.Anvil.csproj.DotSettings +++ b/NWN.Anvil.csproj.DotSettings @@ -5,6 +5,13 @@ True True True + True + True + True + True + False + True + True True True True diff --git a/src/main/Anvil/API/Constants/EventScriptType.cs b/src/main/Anvil/API/Constants/EventScriptType.cs index 64a5d8f85..b0ffebc96 100644 --- a/src/main/Anvil/API/Constants/EventScriptType.cs +++ b/src/main/Anvil/API/Constants/EventScriptType.cs @@ -26,6 +26,7 @@ public enum EventScriptType ModuleOnPlayerTarget = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_TARGET, ModuleOnPlayerGuiEvent = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_GUIEVENT, ModuleOnPlayerTileAction = NWScript.EVENT_SCRIPT_MODULE_ON_PLAYER_TILE_ACTION, + ModuleOnNuiEvent = NWScript.EVENT_SCRIPT_MODULE_ON_NUI_EVENT, AreaOnHeartbeat = NWScript.EVENT_SCRIPT_AREA_ON_HEARTBEAT, AreaOnUserDefinedEvent = NWScript.EVENT_SCRIPT_AREA_ON_USER_DEFINED_EVENT, AreaOnEnter = NWScript.EVENT_SCRIPT_AREA_ON_ENTER, diff --git a/src/main/Anvil/API/Constants/GUIPanel.cs b/src/main/Anvil/API/Constants/GUIPanel.cs index b61ba84c0..d37008c0a 100644 --- a/src/main/Anvil/API/Constants/GUIPanel.cs +++ b/src/main/Anvil/API/Constants/GUIPanel.cs @@ -12,5 +12,12 @@ public enum GUIPanel Journal = NWScript.GUI_PANEL_JOURNAL, SpellBook = NWScript.GUI_PANEL_SPELLBOOK, CharacterSheet = NWScript.GUI_PANEL_CHARACTERSHEET, + LevelUp = NWScript.GUI_PANEL_LEVELUP, + GoldInventory = NWScript.GUI_PANEL_GOLD_INVENTORY, + GoldBarter = NWScript.GUI_PANEL_GOLD_BARTER, + ExamineCreature = NWScript.GUI_PANEL_EXAMINE_CREATURE, + ExamineItem = NWScript.GUI_PANEL_EXAMINE_ITEM, + ExaminePlaceable = NWScript.GUI_PANEL_EXAMINE_PLACEABLE, + ExamineDoor = NWScript.GUI_PANEL_EXAMINE_DOOR, } } diff --git a/src/main/Anvil/API/Constants/GuiEventType.cs b/src/main/Anvil/API/Constants/GuiEventType.cs index 7617e0006..a3a31c3f2 100644 --- a/src/main/Anvil/API/Constants/GuiEventType.cs +++ b/src/main/Anvil/API/Constants/GuiEventType.cs @@ -18,5 +18,16 @@ public enum GuiEventType PlayerListPlayerClick = NWScript.GUIEVENT_PLAYERLIST_PLAYER_CLICK, PartyBarPortraitClick = NWScript.GUIEVENT_PARTYBAR_PORTRAIT_CLICK, DisabledPanelAttemptOpen = NWScript.GUIEVENT_DISABLED_PANEL_ATTEMPT_OPEN, + CompassClick = NWScript.GUIEVENT_COMPASS_CLICK, + LevelUpCancelled = NWScript.GUIEVENT_LEVELUP_CANCELLED, + AreaLoadScreenFinished = NWScript.GUIEVENT_AREA_LOADSCREEN_FINISHED, + QuickChatActivate = NWScript.GUIEVENT_QUICKCHAT_ACTIVATE, + QuickChatSelect = NWScript.GUIEVENT_QUICKCHAT_SELECT, + QuickChatClose = NWScript.GUIEVENT_QUICKCHAT_CLOSE, + SelectCreature = NWScript.GUIEVENT_SELECT_CREATURE, + UnselectCreature = NWScript.GUIEVENT_UNSELECT_CREATURE, + ExamineObject = NWScript.GUIEVENT_EXAMINE_OBJECT, + OptionsOpen = NWScript.GUIEVENT_OPTIONS_OPEN, + OptionsClose = NWScript.GUIEVENT_OPTIONS_CLOSE, } } diff --git a/src/main/Anvil/API/Constants/NuiEventType.cs b/src/main/Anvil/API/Constants/NuiEventType.cs new file mode 100644 index 000000000..2a7acfa31 --- /dev/null +++ b/src/main/Anvil/API/Constants/NuiEventType.cs @@ -0,0 +1,15 @@ +namespace Anvil.API +{ + public enum NuiEventType + { + Unknown, + Click, + Watch, + Open, + Close, + Focus, + Blur, + MouseDown, + MouseUp, + } +} diff --git a/src/main/Anvil/API/Constants/PlayerDeviceProperty.cs b/src/main/Anvil/API/Constants/PlayerDeviceProperty.cs new file mode 100644 index 000000000..9fc04e75b --- /dev/null +++ b/src/main/Anvil/API/Constants/PlayerDeviceProperty.cs @@ -0,0 +1,18 @@ +using NWN.Core; + +namespace Anvil.API +{ + public sealed class PlayerDeviceProperty + { + public static readonly PlayerDeviceProperty GuiWidth = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_GUI_WIDTH); + public static readonly PlayerDeviceProperty GuiHeight = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_GUI_HEIGHT); + public static readonly PlayerDeviceProperty GuiScale = new PlayerDeviceProperty(NWScript.PLAYER_DEVICE_PROPERTY_GUI_SCALE); + + internal string PropertyName { get; } + + internal PlayerDeviceProperty(string propertyName) + { + PropertyName = propertyName; + } + } +} diff --git a/src/main/Anvil/API/Constants/PlayerLanguage.cs b/src/main/Anvil/API/Constants/PlayerLanguage.cs new file mode 100644 index 000000000..b0134514a --- /dev/null +++ b/src/main/Anvil/API/Constants/PlayerLanguage.cs @@ -0,0 +1,15 @@ +using NWN.Core; + +namespace Anvil.API +{ + public enum PlayerLanguage + { + Invalid = NWScript.PLAYER_LANGUAGE_INVALID, + English = NWScript.PLAYER_LANGUAGE_ENGLISH, + French = NWScript.PLAYER_LANGUAGE_FRENCH, + German = NWScript.PLAYER_LANGUAGE_GERMAN, + Italian = NWScript.PLAYER_LANGUAGE_ITALIAN, + Spanish = NWScript.PLAYER_LANGUAGE_SPANISH, + Polish = NWScript.PLAYER_LANGUAGE_POLISH, + } +} diff --git a/src/main/Anvil/API/Constants/PlayerPlatform.cs b/src/main/Anvil/API/Constants/PlayerPlatform.cs new file mode 100644 index 000000000..7dfa4835b --- /dev/null +++ b/src/main/Anvil/API/Constants/PlayerPlatform.cs @@ -0,0 +1,24 @@ +using NWN.Core; + +namespace Anvil.API +{ + public enum PlayerPlatform + { + Invalid = NWScript.PLAYER_DEVICE_PLATFORM_INVALID, + WindowsX86 = NWScript.PLAYER_DEVICE_PLATFORM_WINDOWS_X86, + WindowsX64 = NWScript.PLAYER_DEVICE_PLATFORM_WINDOWS_X64, + LinuxX86 = NWScript.PLAYER_DEVICE_PLATFORM_LINUX_X86, + LinuxX64 = NWScript.PLAYER_DEVICE_PLATFORM_LINUX_X64, + LinuxArm32 = NWScript.PLAYER_DEVICE_PLATFORM_LINUX_ARM32, + LinuxArm64 = NWScript.PLAYER_DEVICE_PLATFORM_LINUX_ARM64, + MacX86 = NWScript.PLAYER_DEVICE_PLATFORM_MAC_X86, + MacX64 = NWScript.PLAYER_DEVICE_PLATFORM_MAC_X64, + Ios = NWScript.PLAYER_DEVICE_PLATFORM_IOS, + AndroidArm32 = NWScript.PLAYER_DEVICE_PLATFORM_ANDROID_ARM32, + AndroidArm64 = NWScript.PLAYER_DEVICE_PLATFORM_ANDROID_ARM64, + AndroidX64 = NWScript.PLAYER_DEVICE_PLATFORM_ANDROID_X64, + NintendoSwitch = NWScript.PLAYER_DEVICE_PLATFORM_NINTENDO_SWITCH, + MicrosoftXboxOne = NWScript.PLAYER_DEVICE_PLATFORM_MICROSOFT_XBOXONE, + SonyPs4 = NWScript.PLAYER_DEVICE_PLATFORM_SONY_PS4, + } +} diff --git a/src/main/Anvil/API/EngineStructure/Json.cs b/src/main/Anvil/API/EngineStructure/Json.cs new file mode 100644 index 000000000..3304cd8b9 --- /dev/null +++ b/src/main/Anvil/API/EngineStructure/Json.cs @@ -0,0 +1,30 @@ +using System; +using NWN.Core; + +namespace Anvil.API +{ + public sealed class Json : EngineStructure + { + internal Json(IntPtr handle) : base(handle) {} + + protected override int StructureId + { + get => NWScript.ENGINE_STRUCTURE_JSON; + } + + public static implicit operator Json(IntPtr intPtr) + { + return new Json(intPtr); + } + + public static Json Parse(string jsonString) + { + return NWScript.JsonParse(jsonString); + } + + public string Dump() + { + return NWScript.JsonDump(this); + } + } +} diff --git a/src/main/Anvil/API/Events/Game/ModuleEvents.cs b/src/main/Anvil/API/Events/Game/ModuleEvents.cs index 42763b95d..9e000c49f 100644 --- a/src/main/Anvil/API/Events/Game/ModuleEvents.cs +++ b/src/main/Anvil/API/Events/Game/ModuleEvents.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; using Anvil.API.Events; +using Newtonsoft.Json; using NWN.Core; namespace Anvil.API.Events @@ -162,6 +163,76 @@ NwObject IEvent.Context } } + /// + /// Called when a player triggers an event in the NUI system. + /// + [GameEvent(EventScriptType.ModuleOnNuiEvent)] + public sealed class OnNuiEvent : IEvent + { + /// + /// Gets the player that triggered this event. + /// + public NwPlayer Player { get; } = NWScript.NuiGetEventPlayer().ToNwPlayer(); + + /// + /// Gets the window token associated with this event. + /// + public int WindowToken { get; } = NWScript.NuiGetEventWindow(); + + /// + /// Gets the ID of the that triggered the event. + /// + public string ElementId { get; } = NWScript.NuiGetEventElement(); + + /// + /// Get the array index of the current event.
+ /// This can be used to get the index into an array, for example when rendering lists of buttons.
+ /// Returns -1 if the event is not originating from within an array. + ///
+ public int ArrayIndex { get; } = NWScript.NuiGetEventArrayIndex(); + + private readonly string eventPayload; + + /// + /// Gets the payload data associated with this event. + /// + /// + /// The payload data, or null if the event has no payload. + public T GetEventPayload() + { + return JsonConvert.DeserializeObject(eventPayload); + } + + /// + /// Gets the type of Nui event that occurred. + /// + public NuiEventType EventType { get; } + + public OnNuiEvent() + { + Json payload = NWScript.NuiGetEventPayload(); + eventPayload = payload.Dump(); + + EventType = NWScript.NuiGetEventType() switch + { + "click" => NuiEventType.Click, + "watch" => NuiEventType.Watch, + "open" => NuiEventType.Open, + "close" => NuiEventType.Close, + "focus" => NuiEventType.Focus, + "blur" => NuiEventType.Blur, + "mousedown" => NuiEventType.MouseDown, + "mouseup" => NuiEventType.MouseUp, + _ => NuiEventType.Unknown, + }; + } + + public NwObject Context + { + get => Player?.ControlledCreature; + } + } + /// /// Triggered when any sends a chat message. Private channel not hooked. /// @@ -594,6 +665,13 @@ public event Action OnPlayerChat remove => EventService.UnsubscribeAll(value); } + /// + public event Action OnNuiEvent + { + add => EventService.SubscribeAll(new GameEventFactory.RegistrationData(this), value); + remove => EventService.UnsubscribeAll(value); + } + /// public event Action OnPlayerTarget { @@ -702,6 +780,13 @@ public event Action OnCutsceneAbort remove => EventService.Unsubscribe(ControlledCreature, value); } + /// + public event Action OnNuiEvent + { + add => EventService.Subscribe(ControlledCreature, new GameEventFactory.RegistrationData(NwModule.Instance), value); + remove => EventService.Unsubscribe(ControlledCreature, value); + } + /// public event Action OnPlayerChat { diff --git a/src/main/Anvil/API/Nui/Bindings/NuiBind.cs b/src/main/Anvil/API/Nui/Bindings/NuiBind.cs new file mode 100644 index 000000000..f6b29e98a --- /dev/null +++ b/src/main/Anvil/API/Nui/Bindings/NuiBind.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using NWN.Core; + +namespace Anvil.API +{ + /// + /// A NUI property binding that can be updated after being sent to the client. + /// + /// The type of value being bound. + public sealed class NuiBind : NuiProperty + { + [JsonProperty("bind")] + public string Key { get; init; } + + public NuiBind(string key) + { + Key = key; + } + + /// + /// 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 T GetBindValue(NwPlayer player, int nUiToken) + { + Json json = NWScript.NuiGetBind(player.ControlledCreature, nUiToken, Key); + return JsonConvert.DeserializeObject(json.Dump()); + } + + /// + /// 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, T value) + { + string jsonString = JsonConvert.SerializeObject(value); + Json json = Json.Parse(jsonString); + + NWScript.NuiSetBind(player.ControlledCreature, uiToken, Key, json); + } + + /// + /// 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/src/main/Anvil/API/Nui/Bindings/NuiProperty.cs b/src/main/Anvil/API/Nui/Bindings/NuiProperty.cs new file mode 100644 index 000000000..602b55271 --- /dev/null +++ b/src/main/Anvil/API/Nui/Bindings/NuiProperty.cs @@ -0,0 +1,34 @@ +namespace Anvil.API +{ + /// + /// A NUI property that can be configured as a static readonly value, or a property that can be updated at runtime. + /// + /// The underlying type of the property. + public abstract class NuiProperty + { + public static implicit operator NuiProperty(T value) + { + return CreateValue(value); + } + + /// + /// Creates a Nui variable binding that can be changed at runtime. + /// + /// The key to use for the binding. + /// A NuiBind object. + public static NuiBind CreateBind(string key) + { + return new NuiBind(key); + } + + /// + /// Creates a readonly Nui variable that cannot be changed at runtime. + /// + /// + /// + public static NuiValue CreateValue(T value) + { + return new NuiValue(value); + } + } +} diff --git a/src/main/Anvil/API/Nui/Bindings/NuiValue.cs b/src/main/Anvil/API/Nui/Bindings/NuiValue.cs new file mode 100644 index 000000000..a362de30a --- /dev/null +++ b/src/main/Anvil/API/Nui/Bindings/NuiValue.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A readonly NUI property value that cannot be changed at runtime. + /// + /// The type of value being assigned. + [JsonConverter(typeof(NuiValueConverter))] + public sealed class NuiValue : NuiProperty + { + public static implicit operator T(NuiValue value) + { + return value != null ? value.Value : default; + } + + /// + /// Gets the value of this property. + /// + public T Value { get; init; } + + public NuiValue(T value) + { + Value = value; + } + } +} diff --git a/src/main/Anvil/API/Nui/Bindings/NuiValueConverter.cs b/src/main/Anvil/API/Nui/Bindings/NuiValueConverter.cs new file mode 100644 index 000000000..6df2a8360 --- /dev/null +++ b/src/main/Anvil/API/Nui/Bindings/NuiValueConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Reflection; +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiValueConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + Type type = value.GetType(); + PropertyInfo propertyInfo = type.GetProperty(nameof(NuiValue.Value)); + + if (propertyInfo == null) + { + writer.WriteNull(); + return; + } + + serializer.Serialize(writer, propertyInfo.GetValue(value)); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + object retVal = Activator.CreateInstance(objectType); + if (retVal == null) + { + return null; + } + + PropertyInfo propertyInfo = objectType.GetProperty(nameof(NuiValue.Value)); + if (propertyInfo == null) + { + return null; + } + + propertyInfo.SetValue(retVal, serializer.Deserialize(reader)); + return retVal; + } + + public override bool CanConvert(Type objectType) + { + return objectType.GetGenericTypeDefinition() == typeof(NuiValue<>); + } + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiAspect.cs b/src/main/Anvil/API/Nui/Constants/NuiAspect.cs new file mode 100644 index 000000000..347b5e5ed --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiAspect.cs @@ -0,0 +1,12 @@ +namespace Anvil.API +{ + public enum NuiAspect + { + Fit = 0, + Fill = 1, + Fit100 = 2, + Exact = 3, + ExactScaled = 4, + Stretch = 5, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiChartType.cs b/src/main/Anvil/API/Nui/Constants/NuiChartType.cs new file mode 100644 index 000000000..299f206f8 --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiChartType.cs @@ -0,0 +1,8 @@ +namespace Anvil.API +{ + public enum NuiChartType + { + Lines = 0, + Column = 1, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiDirection.cs b/src/main/Anvil/API/Nui/Constants/NuiDirection.cs new file mode 100644 index 000000000..9a41c5641 --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiDirection.cs @@ -0,0 +1,8 @@ +namespace Anvil.API +{ + public enum NuiDirection + { + Horizontal = 0, + Vertical = 1, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiDrawListItemType.cs b/src/main/Anvil/API/Nui/Constants/NuiDrawListItemType.cs new file mode 100644 index 000000000..4cab46afb --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiDrawListItemType.cs @@ -0,0 +1,12 @@ +namespace Anvil.API +{ + public enum NuiDrawListItemType + { + PolyLine = 0, + Curve = 1, + Circle = 2, + Arc = 3, + Text = 4, + Image = 5, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiHAlign.cs b/src/main/Anvil/API/Nui/Constants/NuiHAlign.cs new file mode 100644 index 000000000..d9426e732 --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiHAlign.cs @@ -0,0 +1,9 @@ +namespace Anvil.API +{ + public enum NuiHAlign + { + Center = 0, + Left = 1, + Right = 2, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiMouseButton.cs b/src/main/Anvil/API/Nui/Constants/NuiMouseButton.cs new file mode 100644 index 000000000..785be26eb --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiMouseButton.cs @@ -0,0 +1,9 @@ +namespace Anvil.API +{ + public enum NuiMouseButton + { + Left, + Middle, + Right, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiScrollbars.cs b/src/main/Anvil/API/Nui/Constants/NuiScrollbars.cs new file mode 100644 index 000000000..3ceab209c --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiScrollbars.cs @@ -0,0 +1,11 @@ +namespace Anvil.API +{ + public enum NuiScrollbars + { + None = 0, + X = 1, + Y = 2, + Both = 3, + Auto = 4, + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiStyle.cs b/src/main/Anvil/API/Nui/Constants/NuiStyle.cs new file mode 100644 index 000000000..4506003f6 --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiStyle.cs @@ -0,0 +1,16 @@ +namespace Anvil.API +{ + public static class NuiStyle + { + public const float PrimaryWidth = 150.0f; + public const float PrimaryHeight = 50.0f; + + public const float SecondaryWidth = 150.0f; + public const float SecondaryHeight = 35.0f; + + public const float TertiaryWidth = 100.0f; + public const float TertiaryHeight = 30.0f; + + public const float RowHeight = 25.0f; + } +} diff --git a/src/main/Anvil/API/Nui/Constants/NuiVAlign.cs b/src/main/Anvil/API/Nui/Constants/NuiVAlign.cs new file mode 100644 index 000000000..f392d72f2 --- /dev/null +++ b/src/main/Anvil/API/Nui/Constants/NuiVAlign.cs @@ -0,0 +1,9 @@ +namespace Anvil.API +{ + public enum NuiVAlign + { + Middle = 0, + Top = 1, + Bottom = 2, + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawList.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawList.cs new file mode 100644 index 000000000..582a1d65b --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawList.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawList : NuiElement + { + public override string Type + { + get => null; + } + + [JsonProperty("draw_list_scissor")] + public NuiProperty Scissor { get; set; } + + [JsonProperty("draw_list")] + public List DrawList { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListArc.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListArc.cs new file mode 100644 index 000000000..0b8c79483 --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListArc.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListArc : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.Arc; + } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("fill")] + public NuiProperty Fill { get; set; } + + [JsonProperty("line_thickness")] + public NuiProperty LineThickness { get; set; } + + [JsonProperty("c")] + public NuiProperty Center { get; set; } + + [JsonProperty("radius")] + public NuiProperty Radius { get; set; } + + [JsonProperty("amin")] + public NuiProperty AngleMin { get; set; } + + [JsonProperty("amax")] + public NuiProperty AngleMax { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListCircle.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListCircle.cs new file mode 100644 index 000000000..12cfc5813 --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListCircle.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListCircle : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.Circle; + } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("fill")] + public NuiProperty Fill { get; set; } + + [JsonProperty("line_thickness")] + public NuiProperty LineThickness { get; set; } + + [JsonProperty("rect")] + public NuiBind Rect { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListCurve.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListCurve.cs new file mode 100644 index 000000000..5b7120e1f --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListCurve.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListCurve : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.Curve; + } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("line_thickness")] + public NuiProperty LineThickness { get; set; } + + [JsonProperty("a")] + public NuiProperty PointA { get; set; } + + [JsonProperty("b")] + public NuiProperty PointB { get; set; } + + [JsonProperty("ctrl0")] + public NuiProperty Control0 { get; set; } + + [JsonProperty("ctrl1")] + public NuiProperty Control1 { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListImage.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListImage.cs new file mode 100644 index 000000000..9a58559ec --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListImage.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListImage : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.Image; + } + + [JsonProperty("image")] + public NuiProperty ResRef { get; set; } + + [JsonProperty("rect")] + public NuiProperty Rect { get; set; } + + [JsonProperty("image_aspect")] + public NuiProperty Aspect { get; set; } + + [JsonProperty("image_halign")] + public NuiProperty HorizontalAlign { get; set; } + + [JsonProperty("image_valign")] + public NuiProperty VerticalAlign { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListItem.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListItem.cs new file mode 100644 index 000000000..921d06b23 --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListItem.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public abstract class NuiDrawListItem + { + [JsonProperty("type")] + public abstract NuiDrawListItemType Type { get; } + + [JsonProperty("enabled")] + public NuiProperty Enabled { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListPolyLine.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListPolyLine.cs new file mode 100644 index 000000000..0587a7806 --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListPolyLine.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListPolyLine : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.PolyLine; + } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("fill")] + public NuiProperty Fill { get; set; } + + [JsonProperty("line_thickness")] + public NuiProperty LineThickness { get; set; } + + [JsonProperty("points")] + public List Points { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/DrawList/NuiDrawListText.cs b/src/main/Anvil/API/Nui/DrawList/NuiDrawListText.cs new file mode 100644 index 000000000..d06487495 --- /dev/null +++ b/src/main/Anvil/API/Nui/DrawList/NuiDrawListText.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiDrawListText : NuiDrawListItem + { + public override NuiDrawListItemType Type + { + get => NuiDrawListItemType.Text; + } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("rect")] + public NuiProperty Rect { get; set; } + + [JsonProperty("text")] + public NuiProperty Text { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Layout/NuiColumn.cs b/src/main/Anvil/API/Nui/Layout/NuiColumn.cs new file mode 100644 index 000000000..ec9954565 --- /dev/null +++ b/src/main/Anvil/API/Nui/Layout/NuiColumn.cs @@ -0,0 +1,10 @@ +namespace Anvil.API +{ + /// + /// A layout element that will auto-space all elements inside of it and advise the parent about its desired size. + /// + public sealed class NuiColumn : NuiLayout + { + public override string Type { get => "col"; } + } +} diff --git a/src/main/Anvil/API/Nui/Layout/NuiGroup.cs b/src/main/Anvil/API/Nui/Layout/NuiGroup.cs new file mode 100644 index 000000000..c0170ca65 --- /dev/null +++ b/src/main/Anvil/API/Nui/Layout/NuiGroup.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A group, usually with a border and some padding, holding a single element. Can scroll.
+ /// Will not advise parent of size, so you need to let it fill a span (col/row) as if it was a element. + ///
+ public sealed class NuiGroup : NuiLayout + { + public override string Type { get; } = "group"; + + [JsonProperty("border")] + public bool Border { get; set; } = true; + + [JsonProperty("scrollbars")] + public NuiScrollbars Scrollbars { get; set; } = NuiScrollbars.Auto; + } +} diff --git a/src/main/Anvil/API/Nui/Layout/NuiLayout.cs b/src/main/Anvil/API/Nui/Layout/NuiLayout.cs new file mode 100644 index 000000000..1f094bf2b --- /dev/null +++ b/src/main/Anvil/API/Nui/Layout/NuiLayout.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + public abstract class NuiLayout : NuiElement + { + [JsonProperty("children")] + public List Children { get; set; } = new List(); + } +} diff --git a/src/main/Anvil/API/Nui/Layout/NuiRow.cs b/src/main/Anvil/API/Nui/Layout/NuiRow.cs new file mode 100644 index 000000000..eb4a66930 --- /dev/null +++ b/src/main/Anvil/API/Nui/Layout/NuiRow.cs @@ -0,0 +1,10 @@ +namespace Anvil.API +{ + /// + /// A layout element that will auto-space all elements inside of it and advise the parent about its desired size. + /// + public sealed class NuiRow : NuiLayout + { + public override string Type { get; } = "row"; + } +} diff --git a/src/main/Anvil/API/Nui/NuiColor.cs b/src/main/Anvil/API/Nui/NuiColor.cs new file mode 100644 index 000000000..5f74ec290 --- /dev/null +++ b/src/main/Anvil/API/Nui/NuiColor.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public sealed class NuiColor + { + /// + /// Gets the red value of this color as a byte (0-255). + /// + [JsonProperty("r")] + public readonly byte Red; + + /// + /// Gets or sets the green value of this color as a byte (0-255). + /// + [JsonProperty("g")] + public byte Green { get; set; } + + /// + /// Gets or sets the blue value of this color as a byte (0-255). + /// + [JsonProperty("b")] + public byte Blue { get; set; } + + /// + /// Gets the alpha value of this color as a byte (0-255). + /// + [JsonProperty("a")] + public byte Alpha { get; set; } + + /// + /// Constructs a new Color from the given rgba values. + /// + /// The red value. + /// The green value. + /// The blue value. + /// The alpha value. + public NuiColor(byte red, byte green, byte blue, byte alpha = 255) + { + Red = red; + Green = green; + Blue = blue; + Alpha = alpha; + } + + public static implicit operator Color(NuiColor nuiColor) + { + return new Color(nuiColor.Red, nuiColor.Green, nuiColor.Blue, nuiColor.Alpha); + } + + public static implicit operator NuiColor(Color color) + { + return new NuiColor(color.Red, color.Green, color.Blue, color.Alpha); + } + } +} diff --git a/src/main/Anvil/API/Nui/NuiElement.cs b/src/main/Anvil/API/Nui/NuiElement.cs new file mode 100644 index 000000000..f4d48c395 --- /dev/null +++ b/src/main/Anvil/API/Nui/NuiElement.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A NUI widget/element. + /// + public abstract class NuiElement + { + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public abstract string Type { get; } + + /// + /// A unique identifier for this element. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// The width of this element, in pixels. + /// + [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] + public float? Width { get; set; } + + /// + /// The height of this element, in pixels. + /// + [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] + public float? Height { get; set; } + + /// + /// The aspect ratio (x/y) for this element. + /// + [JsonProperty("aspect", NullValueHandling = NullValueHandling.Ignore)] + public float? Aspect { get; set; } + + /// + /// The margin on the widget. The margin is the spacing outside of the widget. + /// + [JsonProperty("margin", NullValueHandling = NullValueHandling.Ignore)] + public float? Margin { get; set; } + + /// + /// The padding on the widget. The padding is the spacing inside of the widget. + /// + [JsonProperty("padding", NullValueHandling = NullValueHandling.Ignore)] + public float? Padding { get; set; } + + /// + /// Toggles if this element is active/interactable, or disabled/greyed out. + /// + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public NuiProperty Enabled { get; set; } + + /// + /// Toggles if this element should/should not be rendered. Invisible elements still take up layout space, and cannot be clicked through. + /// + [JsonProperty("visible", NullValueHandling = NullValueHandling.Ignore)] + public NuiProperty Visible { get; set; } + + /// + /// A tooltip to show when hovering over this element. + /// + [JsonProperty("tooltip", NullValueHandling = NullValueHandling.Ignore)] + public NuiProperty Tooltip { get; set; } + + /// + /// Style the foreground color of this widget.
+ /// This is dependent on the widget in question and only supports solid/full colors right now (no texture skinning).
+ /// For example, labels would style their text color; progress bars would style the bar. + ///
+ [JsonProperty("foreground_color", NullValueHandling = NullValueHandling.Ignore)] + public NuiProperty ForegroundColor { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/NuiRect.cs b/src/main/Anvil/API/Nui/NuiRect.cs new file mode 100644 index 000000000..6c45f8709 --- /dev/null +++ b/src/main/Anvil/API/Nui/NuiRect.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + public readonly struct NuiRect + { + [JsonProperty("x")] + public float X { get; } + + [JsonProperty("y")] + public float Y { get; } + + [JsonProperty("w")] + public float Width { get; } + + [JsonProperty("h")] + public float Height { get; } + + public NuiRect(float x, float y, float width, float height) + { + X = x; + Y = y; + Width = width; + Height = height; + } + } +} diff --git a/src/main/Anvil/API/Nui/NuiVector.cs b/src/main/Anvil/API/Nui/NuiVector.cs new file mode 100644 index 000000000..529cb964f --- /dev/null +++ b/src/main/Anvil/API/Nui/NuiVector.cs @@ -0,0 +1,66 @@ +using System; +using System.Numerics; +using Newtonsoft.Json; + +namespace Anvil.API +{ + public readonly struct NuiVector : IEquatable + { + [JsonProperty("x")] + public readonly float X; + + [JsonProperty("y")] + public readonly float Y; + + public NuiVector(float x, float y) + { + X = x; + Y = y; + } + + public static implicit operator Vector2(NuiVector vector) + { + return new Vector2(vector.X, vector.Y); + } + + public static implicit operator NuiVector(Vector2 vector) + { + return new NuiVector(vector.X, vector.Y); + } + + public static NuiVector operator -(NuiVector a, NuiVector b) + { + return new NuiVector(a.X - b.X, a.Y - b.Y); + } + + public static NuiVector operator +(NuiVector a, NuiVector b) + { + return new NuiVector(a.X + b.X, a.Y + b.Y); + } + + public bool Equals(NuiVector other) + { + return X.Equals(other.X) && Y.Equals(other.Y); + } + + public override bool Equals(object obj) + { + return obj is NuiVector other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(X, Y); + } + + public static bool operator ==(NuiVector left, NuiVector right) + { + return left.Equals(right); + } + + public static bool operator !=(NuiVector left, NuiVector right) + { + return !left.Equals(right); + } + } +} diff --git a/src/main/Anvil/API/Nui/NuiWindow.cs b/src/main/Anvil/API/Nui/NuiWindow.cs new file mode 100644 index 000000000..c16bc5d30 --- /dev/null +++ b/src/main/Anvil/API/Nui/NuiWindow.cs @@ -0,0 +1,79 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// Represents a NUI scriptable window container. + /// + public sealed class NuiWindow + { + public NuiWindow(NuiLayout root, NuiProperty title) + { + Title = title; + Root = root; + } + + /// + /// Gets the current serialized version of this window. + /// + [JsonProperty("version")] + public int Version { get; private set; } = 1; + + /// + /// Gets or sets the title of this window. + /// + [JsonProperty("title")] + public NuiProperty Title { get; set; } + + /// + /// Gets or sets the root parent layout containing the window content. + /// + [JsonProperty("root")] + public NuiLayout Root { get; set; } + + /// + /// Gets or sets the geometry and bounds of this window.
+ /// Set x and y to -1.0 to center the window. + ///
+ [JsonProperty("geometry")] + public NuiProperty Geometry { get; set; } = new NuiRect(-1, -1, 0, 0); + + /// + /// Gets or sets whether this window can be resized. + /// + [JsonProperty("resizable")] + public NuiProperty Resizable { get; set; } = true; + + /// + /// Gets or sets whether this window is collapsed.
+ /// Use a static value to force the popup into a collapsed/unfolded state. + ///
+ [JsonProperty("collapsed")] + public NuiProperty Collapsed { get; set; } = true; + + /// + /// Gets or sets whether this window can be closed.
+ /// You must provide a way to close the window if you set this to false. + ///
+ [JsonProperty("closable")] + public NuiProperty Closable { get; set; } = true; + + /// + /// Gets or sets whether the background should be rendered. + /// + [JsonProperty("transparent")] + public NuiProperty Transparent { get; set; } = true; + + /// + /// Gets or sets whether the window border should be rendered. + /// + [JsonProperty("border")] + public NuiProperty Border { get; set; } = true; + + /// + /// Gets or sets the element ID for this window. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/ObjectToArrayConverter.cs b/src/main/Anvil/API/Nui/ObjectToArrayConverter.cs new file mode 100644 index 000000000..038412c4e --- /dev/null +++ b/src/main/Anvil/API/Nui/ObjectToArrayConverter.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Anvil.API +{ + // https://stackoverflow.com/questions/39461518/how-to-deserialize-an-array-of-values-with-a-fixed-schema-to-a-strongly-typed-da/39462464#39462464 + internal sealed class ObjectToArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(T) == objectType; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + Type objectType = value.GetType(); + JsonObjectContract contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract; + if (contract == null) + { + throw new JsonSerializationException($"invalid type {objectType.FullName}."); + } + + writer.WriteStartArray(); + foreach (JsonProperty property in SerializableProperties(contract)) + { + object propertyValue = property?.ValueProvider?.GetValue(value); + if (property?.Converter != null && property.Converter.CanWrite) + { + property.Converter.WriteJson(writer, propertyValue, serializer); + } + else + { + serializer.Serialize(writer, propertyValue); + } + } + + writer.WriteEndArray(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JsonObjectContract contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract; + if (contract == null) + { + throw new JsonSerializationException($"invalid type {objectType.FullName}."); + } + + if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null) + { + return null; + } + + if (reader.TokenType != JsonToken.StartArray) + { + throw new JsonSerializationException($"token {reader.TokenType} was not JsonToken.StartArray"); + } + + if (existingValue == null && contract.DefaultCreator == null) + { + return null; + } + + // Not implemented: JsonObjectContract.CreatorParameters, serialization callbacks, + existingValue ??= contract.DefaultCreator(); + + using IEnumerator enumerator = SerializableProperties(contract).GetEnumerator(); + + while (true) + { + switch (reader.ReadToContentAndAssert().TokenType) + { + case JsonToken.EndArray: + return existingValue; + + default: + if (!enumerator.MoveNext()) + { + reader.Skip(); + break; + } + + JsonProperty property = enumerator.Current; + object propertyValue; + // TODO: + // https://www.newtonsoft.com/json/help/html/Properties_T_Newtonsoft_Json_Serialization_JsonProperty.htm + // JsonProperty.ItemConverter, ItemIsReference, ItemReferenceLoopHandling, ItemTypeNameHandling, DefaultValue, DefaultValueHandling, ReferenceLoopHandling, Required, TypeNameHandling, ... + + if (property?.PropertyType == null || property.ValueProvider == null) + { + continue; + } + + if (property.Converter != null && property.Converter.CanRead) + { + propertyValue = property.Converter.ReadJson(reader, property.PropertyType, property.ValueProvider.GetValue(existingValue), serializer); + } + else + { + propertyValue = serializer.Deserialize(reader, property.PropertyType); + } + + property.ValueProvider.SetValue(existingValue, propertyValue); + break; + } + } + } + + private static IEnumerable SerializableProperties(JsonObjectContract contract) + { + return contract.Properties.Where(p => !p.Ignored && p.Readable && p.Writable); + } + } + + internal static class JsonExtensions + { + public static JsonReader ReadToContentAndAssert(this JsonReader reader) + { + return reader.ReadAndAssert().MoveToContentAndAssert(); + } + + public static JsonReader MoveToContentAndAssert(this JsonReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(); + } + + if (reader.TokenType == JsonToken.None) // Skip past beginning of stream. + { + reader.ReadAndAssert(); + } + + while (reader.TokenType == JsonToken.Comment) // Skip past comments. + { + reader.ReadAndAssert(); + } + + return reader; + } + + public static JsonReader ReadAndAssert(this JsonReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(); + } + + if (!reader.Read()) + { + throw new JsonReaderException("Unexpected end of JSON stream."); + } + + return reader; + } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiButton.cs b/src/main/Anvil/API/Nui/Widgets/NuiButton.cs new file mode 100644 index 000000000..ad36ab1ac --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiButton.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A clickable button with text as the label. + /// + public sealed class NuiButton : NuiElement + { + public override string Type + { + get => "button"; + } + + public NuiButton(NuiProperty label) + { + Label = label; + } + + [JsonProperty("label")] + public NuiProperty Label { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiButtonImage.cs b/src/main/Anvil/API/Nui/Widgets/NuiButtonImage.cs new file mode 100644 index 000000000..eaa3bcb45 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiButtonImage.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A clickable button with an image as the label. + /// + public sealed class NuiButtonImage : NuiElement + { + public override string Type + { + get => "button_image"; + } + + public NuiButtonImage(NuiProperty resRef) + { + ResRef = resRef; + } + + [JsonProperty("label")] + public NuiProperty ResRef { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiButtonSelect.cs b/src/main/Anvil/API/Nui/Widgets/NuiButtonSelect.cs new file mode 100644 index 000000000..b0e1d51d1 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiButtonSelect.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A clickable button with text as the label.
+ /// Same as , but this one is a toggle. + ///
+ public sealed class NuiButtonSelect : NuiElement + { + public override string Type + { + get => "button_select"; + } + + public NuiButtonSelect(NuiProperty label, NuiProperty selected) + { + Label = label; + Selected = selected; + } + + [JsonProperty("label")] + public NuiProperty Label { get; set; } + + [JsonProperty("value")] + public NuiProperty Selected { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiChart.cs b/src/main/Anvil/API/Nui/Widgets/NuiChart.cs new file mode 100644 index 000000000..eadd6c9a4 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiChart.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A line/column chart element. + /// + public sealed class NuiChart : NuiElement + { + public override string Type + { + get => "chart"; + } + + [JsonProperty("value")] + public List ChartSlots { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiChartSlot.cs b/src/main/Anvil/API/Nui/Widgets/NuiChartSlot.cs new file mode 100644 index 000000000..7f025e376 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiChartSlot.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A chart element/data set for use in . + /// + public sealed class NuiChartSlot + { + public NuiChartSlot(NuiChartType chartType, NuiProperty legend, NuiProperty color, NuiProperty> data) + { + ChartType = chartType; + Legend = legend; + Color = color; + Data = data; + } + + [JsonProperty("type")] + public NuiChartType ChartType { get; set; } + + [JsonProperty("legend")] + public NuiProperty Legend { get; set; } + + [JsonProperty("color")] + public NuiProperty Color { get; set; } + + [JsonProperty("data")] + public NuiProperty> Data { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiCheck.cs b/src/main/Anvil/API/Nui/Widgets/NuiCheck.cs new file mode 100644 index 000000000..979070caf --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiCheck.cs @@ -0,0 +1,27 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A checkbox with a label to the right of it. + /// + public sealed class NuiCheck : NuiElement + { + public override string Type + { + get => "check"; + } + + public NuiCheck(NuiProperty label, NuiProperty selected) + { + Label = label; + Selected = selected; + } + + [JsonProperty("label")] + public NuiProperty Label { get; set; } + + [JsonProperty("value")] + public NuiProperty Selected { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiColorPicker.cs b/src/main/Anvil/API/Nui/Widgets/NuiColorPicker.cs new file mode 100644 index 000000000..3f0e79761 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiColorPicker.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A simple color picker, with no borders or spacing. + /// + public sealed class NuiColorPicker : NuiElement + { + public override string Type + { + get => "color_picker"; + } + + public NuiColorPicker(NuiProperty color) + { + Color = color; + } + + [JsonProperty("value")] + public NuiProperty Color { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiCombo.cs b/src/main/Anvil/API/Nui/Widgets/NuiCombo.cs new file mode 100644 index 000000000..ff5158f69 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiCombo.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A dropdown menu/combobox. + /// + public sealed class NuiCombo : NuiElement + { + public override string Type + { + get => "combo"; + } + + [JsonProperty("elements")] + public NuiProperty> Entries { get; set; } = new List(); + + [JsonProperty("value")] + public NuiProperty Selected { get; set; } = 0; + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiComboEntry.cs b/src/main/Anvil/API/Nui/Widgets/NuiComboEntry.cs new file mode 100644 index 000000000..1457237fa --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiComboEntry.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A combo/list element for use in . + /// + [JsonConverter(typeof(ObjectToArrayConverter))] + public sealed class NuiComboEntry + { + [JsonProperty(Order = 1)] + public string Label { get; set; } + + [JsonProperty(Order = 2)] + public int Value { get; set; } + + public NuiComboEntry(string label, int value) + { + Label = label; + Value = value; + } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiImage.cs b/src/main/Anvil/API/Nui/Widgets/NuiImage.cs new file mode 100644 index 000000000..1bfedb18b --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiImage.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// An image, with no border or padding. + /// + public sealed class NuiImage : NuiElement + { + public override string Type + { + get => "image"; + } + + public NuiImage(NuiProperty resRef) + { + ResRef = resRef; + } + + [JsonProperty("value")] + public NuiProperty ResRef { get; set; } + + [JsonProperty("image_halign")] + public NuiProperty HorizontalAlign { get; set; } = NuiHAlign.Left; + + [JsonProperty("image_valign")] + public NuiProperty VerticalAlign { get; set; } = NuiVAlign.Top; + + [JsonProperty("image_aspect")] + public NuiProperty ImageAspect { get; set; } = NuiAspect.Exact; + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiLabel.cs b/src/main/Anvil/API/Nui/Widgets/NuiLabel.cs new file mode 100644 index 000000000..6708cf1a2 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiLabel.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A single-line, styleable, non-editable text field. + /// + public sealed class NuiLabel : NuiElement + { + public override string Type + { + get => "label"; + } + + public NuiLabel(NuiProperty label) + { + Label = label; + } + + [JsonProperty("value")] + public NuiProperty Label { get; set; } + + [JsonProperty("text_halign")] + public NuiProperty HorizontalAlign { get; set; } = NuiHAlign.Left; + + [JsonProperty("text_valign")] + public NuiProperty VerticalAlign { get; set; } = NuiVAlign.Top; + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiOptions.cs b/src/main/Anvil/API/Nui/Widgets/NuiOptions.cs new file mode 100644 index 000000000..0850bc9c3 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiOptions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A list of options (radio buttons).
+ /// Only one can be selected at a time. + ///
+ public sealed class NuiOptions : NuiElement + { + public override string Type + { + get => "options"; + } + + [JsonProperty("value")] + public NuiProperty Selection { get; set; } = -1; + + [JsonProperty("direction")] + public NuiDirection Direction { get; set; } = NuiDirection.Horizontal; + + [JsonProperty("elements")] + public List Options { get; set; } = new List(); + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiProgress.cs b/src/main/Anvil/API/Nui/Widgets/NuiProgress.cs new file mode 100644 index 000000000..ccb6eef99 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiProgress.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A generic progress bar. + /// + public sealed class NuiProgress : NuiElement + { + public override string Type + { + get => "progress"; + } + + public NuiProgress(NuiProperty value) + { + Value = value; + } + + /// + /// The current value of this progress bar (0-1). + /// + [JsonProperty("value")] + public NuiProperty Value { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiSlider.cs b/src/main/Anvil/API/Nui/Widgets/NuiSlider.cs new file mode 100644 index 000000000..24fb77738 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiSlider.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A slider bar with integer values. + /// + public sealed class NuiSlider : NuiElement + { + public override string Type + { + get => "slider"; + } + + public NuiSlider(NuiProperty value, NuiProperty min, NuiProperty max) + { + Value = value; + Min = min; + Max = max; + } + + [JsonProperty("value")] + public NuiProperty Value { get; set; } + + [JsonProperty("min")] + public NuiProperty Min { get; set; } + + [JsonProperty("max")] + public NuiProperty Max { get; set; } + + [JsonProperty("step")] + public NuiProperty Step { get; set; } = 1; + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiSliderFloat.cs b/src/main/Anvil/API/Nui/Widgets/NuiSliderFloat.cs new file mode 100644 index 000000000..b58160cd7 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiSliderFloat.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A slider bar with floating-point values. + /// + public sealed class NuiSliderFloat : NuiElement + { + public override string Type + { + get => "sliderf"; + } + + public NuiSliderFloat(NuiProperty value, NuiProperty min, NuiProperty max) + { + Value = value; + Min = min; + Max = max; + } + + [JsonProperty("value")] + public NuiProperty Value { get; set; } + + [JsonProperty("min")] + public NuiProperty Min { get; set; } + + [JsonProperty("max")] + public NuiProperty Max { get; set; } + + [JsonProperty("step")] + public NuiProperty StepSize { get; set; } = 0.01f; + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiSpacer.cs b/src/main/Anvil/API/Nui/Widgets/NuiSpacer.cs new file mode 100644 index 000000000..89afe432e --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiSpacer.cs @@ -0,0 +1,14 @@ +namespace Anvil.API +{ + /// + /// A special widget that just takes up layout space.
+ /// Configure the space used with the Width and Height properties. + ///
+ public sealed class NuiSpacer : NuiElement + { + public override string Type + { + get => "spacer"; + } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiText.cs b/src/main/Anvil/API/Nui/Widgets/NuiText.cs new file mode 100644 index 000000000..3439092f0 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiText.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// A non-editable text field. Supports multiple lines and has a skinned border and a scrollbar if needed. + /// + public sealed class NuiText : NuiElement + { + public override string Type + { + get => "text"; + } + + public NuiText(NuiProperty text) + { + Text = text; + } + + [JsonProperty("value")] + public NuiProperty Text { get; set; } + } +} diff --git a/src/main/Anvil/API/Nui/Widgets/NuiTextEdit.cs b/src/main/Anvil/API/Nui/Widgets/NuiTextEdit.cs new file mode 100644 index 000000000..ca354b537 --- /dev/null +++ b/src/main/Anvil/API/Nui/Widgets/NuiTextEdit.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; + +namespace Anvil.API +{ + /// + /// An editable text field. Can be optionally configured as multi-line. + /// + public sealed class NuiTextEdit : NuiElement + { + public override string Type + { + get => "textedit"; + } + + public NuiTextEdit(NuiProperty label, NuiProperty value, ushort maxLength, bool multiLine) + { + Label = label; + Value = value; + MaxLength = maxLength; + MultiLine = multiLine; + } + + [JsonProperty("label")] + public NuiProperty Label { get; set; } + + [JsonProperty("value")] + public NuiProperty Value { get; set; } + + [JsonProperty("max")] + public ushort MaxLength { get; set; } + + [JsonProperty("multiline")] + public bool MultiLine { get; set; } + } +} diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index 5c4b85d5c..fa7d04291 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Anvil.Internal; using Anvil.Services; +using Newtonsoft.Json; using NLog; using NWN.Core; using NWN.Native.API; @@ -117,6 +118,22 @@ public bool IsDM get => NWScript.GetIsDM(ControlledCreature).ToBool(); } + /// + /// Gets the language configured by this player. + /// + public PlayerLanguage Language + { + get => (PlayerLanguage)NWScript.GetPlayerLanguage(ControlledCreature); + } + + /// + /// Gets the platform this player is currently playing from. + /// + public PlayerPlatform Platform + { + get => (PlayerPlatform)NWScript.GetPlayerDevicePlatform(ControlledCreature); + } + /// /// Gets or sets whether the player has DM privileges gained through a player login (as opposed to the DM client). /// @@ -1165,5 +1182,100 @@ public void DestroySQLDatabase() { NWScript.SqlDestroyDatabase(ControlledCreature); } + + /// + /// Gets the specified device property/capability as advertised by the client. + /// + /// The property to query. + /// The queried property value, or -1 if:
+ /// - the property was never set by the client,
+ /// - the actual value is -1,
+ /// - the player is running a older build that does not advertise device properties,
+ /// - the player has disabled sending device properties (Options/Game/Privacy).
+ public int GetDeviceProperty(PlayerDeviceProperty property) + { + return NWScript.GetPlayerDeviceProperty(ControlledCreature, property.PropertyName); + } + + /// + /// Create a NUI window inline for this player. + /// + /// The window to create. + /// A unique alphanumeric ID identifying this window. Re-creating a window with the same id of one already open will immediately close the old one. + /// The window token on success (!= 0), or 0 on error. + public int CreateNuiWindow(NuiWindow window, string windowId = "") + { + string jsonString = JsonConvert.SerializeObject(window); + Json json = Json.Parse(jsonString); + return NWScript.NuiCreate(ControlledCreature, json, windowId); + } + + /// + /// Get the userdata of the given window token. + /// + /// The token for the window to query. + /// A serializable class structure matching the data to fetch. + /// The fetched data, or null if the window does not exist on the given player, or has no userdata set. + public T NuiGetUserData(int uiToken) + { + Json json = NWScript.NuiGetUserData(ControlledCreature, uiToken); + return JsonConvert.DeserializeObject(json.Dump()); + } + + /// + /// Sets an arbitrary json value as userdata on the given window token.
+ /// This userdata is not read or handled by the game engine and not sent to clients.
+ /// This mechanism only exists as a convenience for the programmer to store data bound to a windows' lifecycle.
+ /// Will do nothing if the window does not exist. + ///
+ /// The token to associate the data with. + /// The data to store. + /// The type of data to store. Must be serializable to JSON. + public void NuiSetUserData(int uiToken, T userData) + { + Json json = Json.Parse(JsonConvert.SerializeObject(userData)); + NWScript.NuiSetUserData(ControlledCreature, uiToken, json); + } + + /// + /// Gets the root window ID associated with the specified token. + /// + /// The token to query. + /// The ID of the window if assigned, otherwise an empty string. + public string NuiGetWindowId(int uiToken) + { + return NWScript.NuiGetWindowId(ControlledCreature, uiToken); + } + + /// + /// Destroys the given window, by token, immediately closing it on the client.
+ /// Does nothing if nUiToken does not exist on the client.
+ /// Does not send a close event - this immediately destroys all serverside state.
+ /// The client will close the window asynchronously. + ///
+ /// The token of the window to destroy. + public void NuiDestroy(int uiToken) + { + NWScript.NuiDestroy(ControlledCreature, uiToken); + } + + /// + /// Swaps out the element specified by ID with the given nui layout (partial). + /// + /// The ui token to update. + /// The ID of the element to update. + /// The updated element to publish. + public void NuiSetGroupLayout(int uiToken, string elementId, NuiGroup updatedLayout) + { + Json json = Json.Parse(JsonConvert.SerializeObject(updatedLayout)); + NWScript.NuiSetGroupLayout(ControlledCreature, uiToken, elementId, json); + } + + /// + public void NuiSetGroupLayout(int uiToken, string elementId, NuiWindow updatedLayout) + { + Json json = Json.Parse(JsonConvert.SerializeObject(updatedLayout)); + NWScript.NuiSetGroupLayout(ControlledCreature, uiToken, elementId, json); + } } } diff --git a/src/main/Anvil/Internal/Assemblies.cs b/src/main/Anvil/Internal/Assemblies.cs index 3932dbcda..a9d70697c 100644 --- a/src/main/Anvil/Internal/Assemblies.cs +++ b/src/main/Anvil/Internal/Assemblies.cs @@ -20,6 +20,7 @@ internal static class Assemblies Native, typeof(NLog.Logger).Assembly, typeof(LightInject.ServiceContainer).Assembly, + typeof(Newtonsoft.Json.JsonConvert).Assembly, }; public static readonly List ReservedNames = AllAssemblies From 4691e2478c2481b52ad9109a1465f33db0fbb08e Mon Sep 17 00:00:00 2001 From: Jorteck Date: Wed, 13 Oct 2021 23:54:18 +0200 Subject: [PATCH 13/16] Fix Plugin Hot Reload. API cleanup/docs. (#385) * Fix references to plugins breaking unloadability. Make unload blocking. * Add env var to disable inital prelink check. * Fix NuiWindow defaults. * Update readme/changelog. * Fix analysis warnings. --- CHANGELOG.md | 2 + README.md | 1 + src/main/Anvil/API/Nui/NuiWindow.cs | 4 +- src/main/Anvil/AnvilCore.cs | 60 +++++++-------- src/main/Anvil/Internal/CoreInteropHandler.cs | 8 +- src/main/Anvil/Internal/EnvironmentConfig.cs | 1 + .../Internal/UnhandledExceptionLogger.cs | 2 +- src/main/Anvil/Plugins/Plugin.cs | 12 ++- src/main/Anvil/Plugins/PluginLoadContext.cs | 24 +++++- .../Services/AnvilContainerFactory.cs | 74 ++++++++++--------- .../Services/Services/IContainerFactory.cs | 7 +- .../Services/Services/InjectionService.cs | 13 +++- .../Anvil/Services/Services/ServiceManager.cs | 36 ++++----- 13 files changed, 148 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6794bdfde..d82ddcbee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - LocalVariableCassowary: Added to support cassowary local variables. - ILateDisposable: Added a new service event interface that is invoked after the server is destroyed. - ServiceBindingOptions: Added `PluginDependencies` and `MissingPluginDependencies` properties for setting up services with optional plugin dependencies. +- Added `PRELINK_ENABLED` setting for disabling native prelink checks. ### Changed - Refactored various internal usages of NWN.Native to use collection/list accessors for native types. @@ -37,6 +38,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - Fixed an issue where the PluginLoader would attempt to unload plugins too early during server shutdown/hot reload. - Fixed an issue where the `EnforceLegalCharacterService` would call the `ELCValidationBefore` event outside of a script context. - Fixed `OnChatMessageSend.Target` always being null. +- Fixed Plugin Unloadability & Hot Reload. ## 8193.26.3 https://github.com/nwn-dotnet/Anvil/compare/v8193.26.2...v8193.26.3 diff --git a/README.md b/README.md index ace58c95f..d2af3521c 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ The following options can be configured via environment variables: |`ANVIL_NLOG_CONFIG`||A valid path to a NLog XML config file.|See the [NLog Wiki](https://github.com/nlog/NLog/wiki/Configuration-file) for configuration options.| |`ANVIL_RELOAD_ENABLED`|`false`|`true/false`|Enables support for plugin hot-reloading via `AnvilCore.Reload()`. Recommended for advanced users.| |`ANVIL_PREVENT_START_NO_PLUGIN`|`false`|`true/false`|Prevents the server from starting if no plugins are detected/loaded.| +|`ANVIL_PRELINK_ENABLED`|`true`|`true/false`|Disables some initial startup checks when linking to the native NWN server binary. This is an advanced setting that should almost always be unset/set to true. |`ANVIL_LOG_MODE`|`Off`|`Off/Duplicate/Redirect`|Configures redirection of the NWN server log. `Off` disables redirection, `Duplicate` creates a copy of the log entries in Anvil/NLog, `Redirect` redirects the log entries to Anvil/NLog, and skips the original log entry.| # Builder/Developer's Guide diff --git a/src/main/Anvil/API/Nui/NuiWindow.cs b/src/main/Anvil/API/Nui/NuiWindow.cs index c16bc5d30..2453e643a 100644 --- a/src/main/Anvil/API/Nui/NuiWindow.cs +++ b/src/main/Anvil/API/Nui/NuiWindow.cs @@ -49,7 +49,7 @@ public NuiWindow(NuiLayout root, NuiProperty title) /// Use a static value to force the popup into a collapsed/unfolded state. /// [JsonProperty("collapsed")] - public NuiProperty Collapsed { get; set; } = true; + public NuiProperty Collapsed { get; set; } /// /// Gets or sets whether this window can be closed.
@@ -62,7 +62,7 @@ public NuiWindow(NuiLayout root, NuiProperty title) /// Gets or sets whether the background should be rendered. ///
[JsonProperty("transparent")] - public NuiProperty Transparent { get; set; } = true; + public NuiProperty Transparent { get; set; } = false; /// /// Gets or sets whether the window border should be rendered. diff --git a/src/main/Anvil/AnvilCore.cs b/src/main/Anvil/AnvilCore.cs index fe177edcb..aaeeefd07 100644 --- a/src/main/Anvil/AnvilCore.cs +++ b/src/main/Anvil/AnvilCore.cs @@ -21,20 +21,21 @@ public sealed class AnvilCore : IServerLifeCycleEventHandler private static AnvilCore instance; - // Core Services - private CoreInteropHandler interopHandler; - private IContainerFactory containerFactory; + // Public Core Services private PluginManager pluginManager; + private ServiceManager serviceManager; + + // Internal Core Services + private CoreInteropHandler interopHandler; private LoggerManager loggerManager; private UnhandledExceptionLogger unhandledExceptionLogger; - private ServiceManager serviceManager; /// /// Entrypoint to start Anvil. /// /// The NativeHandles pointer, provided by the NWNX bootstrap entry point. /// The size of the NativeHandles bootstrap structure, provided by the NWNX entry point. - /// An optional custom binding installer to use instead of the default . + /// An optional container factory to use instead of the default . /// The init result code to return back to NWNX. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Init(IntPtr arg, int argLength, IContainerFactory containerFactory = default) @@ -43,10 +44,10 @@ public static int Init(IntPtr arg, int argLength, IContainerFactory containerFac instance = new AnvilCore(); instance.interopHandler = new CoreInteropHandler(instance); - instance.containerFactory = containerFactory; - instance.pluginManager = new PluginManager(); instance.loggerManager = new LoggerManager(); instance.unhandledExceptionLogger = new UnhandledExceptionLogger(); + instance.pluginManager = new PluginManager(); + instance.serviceManager = new ServiceManager(instance.pluginManager, instance.interopHandler, containerFactory); return NWNCore.Init(arg, argLength, instance.interopHandler, instance.interopHandler); } @@ -67,13 +68,10 @@ public static async void Reload() Log.Info("Reloading Anvil"); - instance.serviceManager.ShutdownServices(); + instance.ShutdownServices(); instance.serviceManager.ShutdownLateServices(); instance.pluginManager.Unload(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - instance.pluginManager.Load(); instance.InitServices(); } @@ -81,11 +79,6 @@ public static async void Reload() private AnvilCore() {} void IServerLifeCycleEventHandler.HandleLifeCycleEvent(LifeCycleEvent eventType) - { - HandleLifeCycleEvent(eventType); - } - - private void HandleLifeCycleEvent(LifeCycleEvent eventType) { switch (eventType) { @@ -95,10 +88,9 @@ private void HandleLifeCycleEvent(LifeCycleEvent eventType) break; case LifeCycleEvent.DestroyServer: Log.Info("Server is shutting down..."); - serviceManager.ShutdownServices(); + ShutdownServices(); break; case LifeCycleEvent.DestroyServerAfter: - serviceManager.ShutdownLateServices(); ShutdownCore(); break; case LifeCycleEvent.Unhandled: @@ -115,34 +107,36 @@ private void InitCore() loggerManager.InitVariables(); unhandledExceptionLogger.Init(); - AssemblyName assemblyName = Assemblies.Anvil.GetName(); + AssemblyName anvilAssemblyName = Assemblies.Anvil.GetName(); Log.Info("Loading {Name} {Version} (NWN.Core: {CoreVersion}, NWN.Native: {NativeVersion})", - assemblyName.Name, - Assemblies.Anvil.GetName().Version, + anvilAssemblyName.Name, + anvilAssemblyName.Version, Assemblies.Core.GetName().Version, Assemblies.Native.GetName().Version); CheckServerVersion(); - } - - private void InitServices() - { pluginManager.Load(); - serviceManager = new ServiceManager(pluginManager, containerFactory); - serviceManager.Init(); - interopHandler.Init(serviceManager.GetService(), serviceManager.GetService()); } private void ShutdownCore() { - serviceManager = null; - + serviceManager.ShutdownLateServices(); pluginManager.Unload(); unhandledExceptionLogger.Dispose(); loggerManager.Dispose(); } + private void InitServices() + { + serviceManager.Init(instance.pluginManager, instance.serviceManager); + } + + private void ShutdownServices() + { + serviceManager.ShutdownServices(); + } + private void CheckServerVersion() { AssemblyName assemblyName = Assemblies.Anvil.GetName(); @@ -159,6 +153,12 @@ private void CheckServerVersion() private void PrelinkNative() { + if (!EnvironmentConfig.NativePrelinkEnabled) + { + Log.Warn("Marshaller prelinking is disabled (ANVIL_PRELINK_ENABLED=false). You may encounter random crashes or issues"); + return; + } + Log.Info("Prelinking native methods"); try diff --git a/src/main/Anvil/Internal/CoreInteropHandler.cs b/src/main/Anvil/Internal/CoreInteropHandler.cs index 613e0f147..3cfaf88da 100644 --- a/src/main/Anvil/Internal/CoreInteropHandler.cs +++ b/src/main/Anvil/Internal/CoreInteropHandler.cs @@ -6,7 +6,7 @@ namespace Anvil.Internal { - internal sealed class CoreInteropHandler : ICoreFunctionHandler, ICoreEventHandler + internal sealed class CoreInteropHandler : ICoreFunctionHandler, ICoreEventHandler, IDisposable { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -36,6 +36,12 @@ public void Init(ICoreRunScriptHandler scriptHandler, ICoreLoopHandler loopHandl this.loopHandler = loopHandler; } + public void Dispose() + { + scriptHandler = null; + loopHandler = null; + } + void ICoreEventHandler.OnSignal(string signal) { LifeCycleEvent eventType = signal switch diff --git a/src/main/Anvil/Internal/EnvironmentConfig.cs b/src/main/Anvil/Internal/EnvironmentConfig.cs index 38d91a22c..329a8b1e8 100644 --- a/src/main/Anvil/Internal/EnvironmentConfig.cs +++ b/src/main/Anvil/Internal/EnvironmentConfig.cs @@ -17,6 +17,7 @@ public static class EnvironmentConfig public static readonly string NLogConfigPath = GetAnvilVariableString("NLOG_CONFIG"); public static readonly bool ReloadEnabled = GetAnvilVariableBool("RELOAD_ENABLED"); public static readonly bool PreventStartNoPlugin = GetAnvilVariableBool("PREVENT_START_NO_PLUGIN"); + public static readonly bool NativePrelinkEnabled = GetAnvilVariableBool("PRELINK_ENABLED", true); public static readonly LogMode LogMode = GetAnvilVariableEnum("LOG_MODE", LogMode.Default); // NWNX diff --git a/src/main/Anvil/Internal/UnhandledExceptionLogger.cs b/src/main/Anvil/Internal/UnhandledExceptionLogger.cs index ea3581a47..c7d61274b 100644 --- a/src/main/Anvil/Internal/UnhandledExceptionLogger.cs +++ b/src/main/Anvil/Internal/UnhandledExceptionLogger.cs @@ -3,7 +3,7 @@ namespace Anvil.Internal { - public sealed class UnhandledExceptionLogger : IDisposable + internal sealed class UnhandledExceptionLogger : IDisposable { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); diff --git a/src/main/Anvil/Plugins/Plugin.cs b/src/main/Anvil/Plugins/Plugin.cs index 72b055d1d..653221474 100644 --- a/src/main/Anvil/Plugins/Plugin.cs +++ b/src/main/Anvil/Plugins/Plugin.cs @@ -50,12 +50,18 @@ public void Dispose() { Assembly = null; + pluginLoadContext.Dispose(); + WeakReference unloadHandle = new WeakReference(pluginLoadContext); + pluginLoadContext = null; + if (EnvironmentConfig.ReloadEnabled) { - pluginLoadContext.Unload(); + while (unloadHandle.IsAlive) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } } - - pluginLoadContext = null; } } } diff --git a/src/main/Anvil/Plugins/PluginLoadContext.cs b/src/main/Anvil/Plugins/PluginLoadContext.cs index caac5ce75..1bb197810 100644 --- a/src/main/Anvil/Plugins/PluginLoadContext.cs +++ b/src/main/Anvil/Plugins/PluginLoadContext.cs @@ -1,16 +1,18 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.Loader; using Anvil.Internal; namespace Anvil.Plugins { - internal sealed class PluginLoadContext : AssemblyLoadContext + internal sealed class PluginLoadContext : AssemblyLoadContext, IDisposable { private readonly PluginManager pluginManager; private readonly string pluginName; private readonly AssemblyDependencyResolver resolver; + private readonly Dictionary assemblyCache = new Dictionary(); public PluginLoadContext(PluginManager pluginManager, string pluginPath, string pluginName) : base(EnvironmentConfig.ReloadEnabled) { @@ -20,6 +22,17 @@ public PluginLoadContext(PluginManager pluginManager, string pluginPath, string } protected override Assembly Load(AssemblyName assemblyName) + { + if (!assemblyCache.TryGetValue(assemblyName.FullName, out Assembly assembly)) + { + assembly = GetAssembly(assemblyName); + assemblyCache[assemblyName.FullName] = assembly; + } + + return assembly; + } + + private Assembly GetAssembly(AssemblyName assemblyName) { // Resolve this plugin's assembly locally. if (assemblyName.Name == pluginName) @@ -60,5 +73,14 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) return IntPtr.Zero; } + + public void Dispose() + { + assemblyCache.Clear(); + if (EnvironmentConfig.ReloadEnabled) + { + Unload(); + } + } } } diff --git a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs index 85e766e30..5b49593a4 100644 --- a/src/main/Anvil/Services/Services/AnvilContainerFactory.cs +++ b/src/main/Anvil/Services/Services/AnvilContainerFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Anvil.API; @@ -15,46 +16,51 @@ public class AnvilContainerFactory : IContainerFactory { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); - protected PluginManager PluginManager; - protected ServiceContainer ServiceContainer; - - public ServiceContainer Setup(PluginManager pluginManager) + public ServiceContainer CreateContainer(PluginManager pluginManager, IEnumerable coreServices) { - PluginManager = pluginManager; - ServiceContainer = new ServiceContainer(new ContainerOptions { EnablePropertyInjection = true, EnableVariance = false }); - SetupInjectPropertySelector(); + ServiceContainer serviceContainer = new ServiceContainer(new ContainerOptions { EnablePropertyInjection = true, EnableVariance = false }); + + SetupInjectPropertySelector(serviceContainer); + serviceContainer.RegisterInstance((IServiceContainer)serviceContainer); + + foreach (object coreService in coreServices) + { + RegisterCoreService(serviceContainer, coreService); + } - ServiceContainer.RegisterInstance((IServiceContainer)ServiceContainer); - return ServiceContainer; + BuildContainer(pluginManager, serviceContainer); + return serviceContainer; } - public void RegisterCoreService(T instance) + private static void RegisterCoreService(ServiceContainer serviceContainer, object instance) { Type instanceType = instance.GetType(); ServiceBindingOptionsAttribute options = instanceType.GetCustomAttribute(); string serviceName = GetServiceName(instanceType, options); - ServiceContainer.RegisterInstance(instance, serviceName); + serviceContainer.RegisterInstance(instanceType, instance, serviceName); } - public void BuildContainer() + private void BuildContainer(PluginManager pluginManager, ServiceContainer serviceContainer) { Log.Info("Loading services..."); - foreach (Type type in PluginManager.LoadedTypes) + foreach (Type type in pluginManager.LoadedTypes) { - TryRegisterType(type); + TryRegisterType(pluginManager, serviceContainer, type); } - RegisterOverrides(); + RegisterOverrides(pluginManager, serviceContainer); } /// /// Override in a child class to specify additional bindings/overrides.
/// See https://www.lightinject.net/ for documentation. ///
- protected virtual void RegisterOverrides() {} + // ReSharper disable UnusedParameter.Global + protected virtual void RegisterOverrides(PluginManager pluginManager, ServiceContainer serviceContainer) {} + // ReSharper restore UnusedParameter.Global - private void TryRegisterType(Type type) + private static void TryRegisterType(PluginManager pluginManager, ServiceContainer serviceContainer, Type type) { if (!type.IsClass || type.IsAbstract || type.ContainsGenericParameters) { @@ -68,40 +74,40 @@ private void TryRegisterType(Type type) } ServiceBindingOptionsAttribute options = type.GetCustomAttribute(); - if (IsServiceRequirementsMet(options)) + if (IsServiceRequirementsMet(pluginManager, options)) { - RegisterBindings(type, bindings, options); + RegisterBindings(serviceContainer, type, bindings, options); } } - private void RegisterBindings(Type bindTo, ServiceBindingAttribute[] bindings, ServiceBindingOptionsAttribute options) + private static void RegisterBindings(ServiceContainer serviceContainer, Type bindTo, ServiceBindingAttribute[] bindings, ServiceBindingOptionsAttribute options) { string serviceName = GetServiceName(bindTo, options); PerContainerLifetime lifeTime = new PerContainerLifetime(); - RegisterExplicitBindings(bindTo, bindings, serviceName, lifeTime); + RegisterExplicitBindings(serviceContainer, bindTo, bindings, serviceName, lifeTime); if (options is not { Lazy: true }) { - RegisterImplicitBindings(bindTo, serviceName, lifeTime); + RegisterImplicitBindings(serviceContainer, bindTo, serviceName, lifeTime); } Log.Info("Registered service {Service}", bindTo.FullName); } - private bool IsServiceRequirementsMet(ServiceBindingOptionsAttribute options) + private static bool IsServiceRequirementsMet(PluginManager pluginManager, ServiceBindingOptionsAttribute options) { if (options == null || options.PluginDependencies == null && options.MissingPluginDependencies == null) { return true; } - if (options.PluginDependencies != null && options.PluginDependencies.Any(dependency => !PluginManager.IsPluginLoaded(dependency))) + if (options.PluginDependencies != null && options.PluginDependencies.Any(dependency => !pluginManager.IsPluginLoaded(dependency))) { return false; } - if (options.MissingPluginDependencies != null && options.MissingPluginDependencies.Any(dependency => PluginManager.IsPluginLoaded(dependency))) + if (options.MissingPluginDependencies != null && options.MissingPluginDependencies.Any(pluginManager.IsPluginLoaded)) { return false; } @@ -109,37 +115,37 @@ private bool IsServiceRequirementsMet(ServiceBindingOptionsAttribute options) return true; } - private void RegisterImplicitBindings(Type bindTo, string serviceName, ILifetime lifeTime) + private static void RegisterImplicitBindings(ServiceContainer serviceContainer, Type bindTo, string serviceName, ILifetime lifeTime) { - ServiceContainer.Register(typeof(object), bindTo, serviceName, lifeTime); + serviceContainer.Register(typeof(object), bindTo, serviceName, lifeTime); if (bindTo.IsAssignableTo(typeof(IInitializable))) { - ServiceContainer.Register(typeof(IInitializable), bindTo, serviceName, lifeTime); + serviceContainer.Register(typeof(IInitializable), bindTo, serviceName, lifeTime); } if (bindTo.IsAssignableTo(typeof(ILateDisposable))) { - ServiceContainer.Register(typeof(ILateDisposable), bindTo, serviceName, lifeTime); + serviceContainer.Register(typeof(ILateDisposable), bindTo, serviceName, lifeTime); } } - private void RegisterExplicitBindings(Type bindTo, ServiceBindingAttribute[] newBindings, string serviceName, ILifetime lifeTime) + private static void RegisterExplicitBindings(ServiceContainer serviceContainer, Type bindTo, ServiceBindingAttribute[] newBindings, string serviceName, ILifetime lifeTime) { foreach (ServiceBindingAttribute bindingInfo in newBindings) { - ServiceContainer.Register(bindingInfo.BindFrom, bindTo, serviceName, lifeTime); + serviceContainer.Register(bindingInfo.BindFrom, bindTo, serviceName, lifeTime); Log.Debug("Bind {BindFrom} -> {BindTo}", bindingInfo.BindFrom.FullName, bindTo.FullName); } } - private void SetupInjectPropertySelector() + private static void SetupInjectPropertySelector(ServiceContainer serviceContainer) { InjectPropertySelector propertySelector = new InjectPropertySelector(InjectPropertyTypes.InstanceOnly); - ServiceContainer.PropertyDependencySelector = new InjectPropertyDependencySelector(propertySelector); + serviceContainer.PropertyDependencySelector = new InjectPropertyDependencySelector(propertySelector); } - private string GetServiceName(Type implementation, ServiceBindingOptionsAttribute options) + private static string GetServiceName(Type implementation, ServiceBindingOptionsAttribute options) { short bindingOrder = options?.Order ?? (short)BindingOrder.Default; return bindingOrder.ToString("D5") + implementation.FullName; diff --git a/src/main/Anvil/Services/Services/IContainerFactory.cs b/src/main/Anvil/Services/Services/IContainerFactory.cs index 356b8e0a8..46eb69623 100644 --- a/src/main/Anvil/Services/Services/IContainerFactory.cs +++ b/src/main/Anvil/Services/Services/IContainerFactory.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Anvil.Plugins; using LightInject; @@ -5,10 +6,6 @@ namespace Anvil.Services { public interface IContainerFactory { - ServiceContainer Setup(PluginManager pluginManager); - - void RegisterCoreService(T instance); - - void BuildContainer(); + ServiceContainer CreateContainer(PluginManager pluginManager, IEnumerable coreServices); } } diff --git a/src/main/Anvil/Services/Services/InjectionService.cs b/src/main/Anvil/Services/Services/InjectionService.cs index 5e846735b..75e952202 100644 --- a/src/main/Anvil/Services/Services/InjectionService.cs +++ b/src/main/Anvil/Services/Services/InjectionService.cs @@ -8,9 +8,10 @@ namespace Anvil.Services { [ServiceBinding(typeof(InjectionService))] [ServiceBindingOptions(BindingOrder.API)] - public sealed class InjectionService + public sealed class InjectionService : IDisposable { private readonly IServiceContainer container; + private readonly List injectedStaticProperties = new List(); public InjectionService(IServiceContainer container, PluginManager pluginManager) { @@ -47,8 +48,18 @@ private void InjectStaticProperties(IEnumerable types) { object value = container.TryGetInstance(propertyInfo.PropertyType); propertyInfo.SetValue(null, value); + injectedStaticProperties.Add(propertyInfo); } } } + + // We clear injected properties as they can hold invalid references when reloading Anvil. + void IDisposable.Dispose() + { + foreach (PropertyInfo propertyInfo in injectedStaticProperties) + { + propertyInfo.SetValue(null, default); + } + } } } diff --git a/src/main/Anvil/Services/Services/ServiceManager.cs b/src/main/Anvil/Services/Services/ServiceManager.cs index 62e3a907d..7fe03ae4e 100644 --- a/src/main/Anvil/Services/Services/ServiceManager.cs +++ b/src/main/Anvil/Services/Services/ServiceManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Anvil.Internal; using Anvil.Plugins; using LightInject; using NLog; @@ -7,24 +8,24 @@ namespace Anvil.Services { [ServiceBindingOptions(BindingOrder.Core)] - internal sealed class ServiceManager + public sealed class ServiceManager { private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private readonly PluginManager pluginManager; + private readonly CoreInteropHandler interopHandler; private readonly IContainerFactory containerFactory; private ServiceContainer serviceContainer; private List lateDisposables; - internal ServiceManager(PluginManager pluginManager, IContainerFactory containerFactory) + internal ServiceManager(PluginManager pluginManager, CoreInteropHandler interopHandler, IContainerFactory containerFactory) { - Log.Info("Using {ContainerFactory} to install service bindings", containerFactory.GetType().FullName); - this.pluginManager = pluginManager; + this.interopHandler = interopHandler; this.containerFactory = containerFactory; - serviceContainer = containerFactory.Setup(pluginManager); + Log.Info("Using {ContainerFactory} to install service bindings", containerFactory.GetType().FullName); } public T GetService() where T : class @@ -32,13 +33,11 @@ public T GetService() where T : class return serviceContainer.GetInstance(); } - internal void Init() + internal void Init(params object[] coreServices) { - RegisterCoreService(pluginManager); - RegisterCoreService(this); - - containerFactory.BuildContainer(); - NotifyInitComplete(); + serviceContainer = containerFactory.CreateContainer(pluginManager, coreServices); + interopHandler.Init(GetService(), GetService()); + InitServices(); } internal void ShutdownServices() @@ -50,13 +49,19 @@ internal void ShutdownServices() Log.Info("Unloading services..."); lateDisposables = serviceContainer.GetAllInstances().ToList(); - serviceContainer.Dispose(); + interopHandler.Dispose(); + serviceContainer.Dispose(); serviceContainer = null; } internal void ShutdownLateServices() { + if (lateDisposables == null) + { + return; + } + foreach (ILateDisposable lateDisposable in lateDisposables) { lateDisposable.LateDispose(); @@ -65,12 +70,7 @@ internal void ShutdownLateServices() lateDisposables = null; } - private void RegisterCoreService(T instance) - { - containerFactory.RegisterCoreService(instance); - } - - private void NotifyInitComplete() + private void InitServices() { foreach (IInitializable initializable in serviceContainer.GetAllInstances()) { From 487d28365181ed532d2d2883fab2a91087ea81d6 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Sun, 17 Oct 2021 01:27:08 +0200 Subject: [PATCH 14/16] Use weak references for plugin assembly cache. (#386) --- src/main/Anvil/Plugins/PluginLoadContext.cs | 8 ++++---- src/main/Anvil/Plugins/PluginManager.cs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/Anvil/Plugins/PluginLoadContext.cs b/src/main/Anvil/Plugins/PluginLoadContext.cs index 1bb197810..bda6e9708 100644 --- a/src/main/Anvil/Plugins/PluginLoadContext.cs +++ b/src/main/Anvil/Plugins/PluginLoadContext.cs @@ -12,7 +12,7 @@ internal sealed class PluginLoadContext : AssemblyLoadContext, IDisposable private readonly string pluginName; private readonly AssemblyDependencyResolver resolver; - private readonly Dictionary assemblyCache = new Dictionary(); + private readonly Dictionary> assemblyCache = new Dictionary>(); public PluginLoadContext(PluginManager pluginManager, string pluginPath, string pluginName) : base(EnvironmentConfig.ReloadEnabled) { @@ -23,10 +23,11 @@ public PluginLoadContext(PluginManager pluginManager, string pluginPath, string protected override Assembly Load(AssemblyName assemblyName) { - if (!assemblyCache.TryGetValue(assemblyName.FullName, out Assembly assembly)) + if (!assemblyCache.TryGetValue(assemblyName.FullName, out WeakReference assemblyRef) || !assemblyRef.TryGetTarget(out Assembly assembly)) { assembly = GetAssembly(assemblyName); - assemblyCache[assemblyName.FullName] = assembly; + assemblyRef = new WeakReference(assembly); + assemblyCache[assemblyName.FullName] = assemblyRef; } return assembly; @@ -76,7 +77,6 @@ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) public void Dispose() { - assemblyCache.Clear(); if (EnvironmentConfig.ReloadEnabled) { Unload(); diff --git a/src/main/Anvil/Plugins/PluginManager.cs b/src/main/Anvil/Plugins/PluginManager.cs index 11953a211..ce7afd23d 100644 --- a/src/main/Anvil/Plugins/PluginManager.cs +++ b/src/main/Anvil/Plugins/PluginManager.cs @@ -50,6 +50,7 @@ internal void Unload() Log.Info("Unloading plugins..."); foreach (Plugin plugin in plugins) { + Log.Info("Unloading DotNET plugin {PluginName} - {PluginPath}", plugin.AssemblyName.Name, plugin.PluginPath); plugin.Dispose(); Log.Info("Unloaded DotNET plugin {PluginName} - {PluginPath}", plugin.AssemblyName.Name, plugin.PluginPath); } From 0473f89140bab5f994ee13c1b1553caeb1a53112 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Sun, 17 Oct 2021 02:13:29 +0200 Subject: [PATCH 15/16] Object Storage Fixes (#387) * Use separate GFF field for anvil object storage. Implement import flow from NWNX. * Update changelog. --- CHANGELOG.md | 9 +- .../Anvil/API/Extensions/ResGffExtensions.cs | 98 +++++++++++++++++++ .../Services/ObjectStorage/ObjectStorage.cs | 25 ++--- .../ObjectStorage/ObjectStorageService.cs | 45 ++++++--- 4 files changed, 149 insertions(+), 28 deletions(-) create mode 100644 src/main/Anvil/API/Extensions/ResGffExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d82ddcbee..82644ca42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - Effect: Added `Effect.Icon()` factory method for creating Icon effects. - Effect: Added `Effect.RunAction()` factory methods for creating effects that invoke C# actions. - ScriptHandleFactory: New service for dynamically creating function callbacks at runtime that are bound to script names. The returned handle is currently used for script parameters in effects. -- ModuleEvents: Added `OnPlayerGuiEvent` and `OnPlayerTarget` events. +- ModuleEvents: Added `OnPlayerGuiEvent`, `OnNuiEvent` and `OnPlayerTarget` events. - GUIPanel: Added new constants published with NWN 8193.31 - NwPlayer: Added `SetGuiPanelDisabled` for disabling built-in GUI elements. - VirtualMachine: Added `RecursionLevel` property. @@ -25,13 +25,15 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - VirtualMachine: `IsInScriptContext` now checks the current executing thread, and now only returns true while on the main thread and inside of a VM script context. - HookService: Hooks are now returned/disposed after the server has been destroyed. - IScriptDispatcher: Custom Script Dispatchers must now define an execution order. This order is used when a script call is triggered from the VM, and determines which service/s implementing this interface get executed first. +- ObjectStorageService (**Breaking**): Persistent data written in Anvil can no-longer be accessed by NWNX. Anvil can still import NWNX persistent data. +- ObjectStorageSerive: Anvil no-longer writes a duplicate `NWNX_POS` serialized field and instead writes its own `ANVIL_POS` field for persistent object data. ### Deprecated - Effect: Deprecated `Effect.AreaOfEffect` that uses strings for the script handlers. Use the overload that uses `ScriptCallbackHandle` parameters instead. ### Removed -- HookService: Removed the optional `shutdownDispose` parameter. -- AnvilCore: Removed custom `ITypeLoader` support, and hardcoded references to the updated PluginManager. +- HookService: Removed the optional `shutdownDispose` parameter (superseded by `ILateDisposable`). +- AnvilCore: Removed custom `ITypeLoader` support, and hardcoded references to the updated `PluginManager`. ### Fixed - Fixed an issue where the `ObjectStorageService` would cause errors when performing hot reloads with `AnvilCore.Reload()` @@ -39,6 +41,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - Fixed an issue where the `EnforceLegalCharacterService` would call the `ELCValidationBefore` event outside of a script context. - Fixed `OnChatMessageSend.Target` always being null. - Fixed Plugin Unloadability & Hot Reload. +- Fixed a native crash caused by a conflict with Anvil's `ObjectStorageService` and NWNX's Object Storage (POS). ## 8193.26.3 https://github.com/nwn-dotnet/Anvil/compare/v8193.26.2...v8193.26.3 diff --git a/src/main/Anvil/API/Extensions/ResGffExtensions.cs b/src/main/Anvil/API/Extensions/ResGffExtensions.cs new file mode 100644 index 000000000..0894459ce --- /dev/null +++ b/src/main/Anvil/API/Extensions/ResGffExtensions.cs @@ -0,0 +1,98 @@ +using NWN.Native.API; + +namespace Anvil.API +{ + internal static unsafe class ResGffExtensions + { + public static bool TryReadCExoString(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out CExoString value) + { + int bSuccess; + value = resGff.ReadFieldCExoString(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadCResRef(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out CResRef value) + { + int bSuccess; + value = resGff.ReadFieldCResRef(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadInt(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out int value) + { + int bSuccess; + value = resGff.ReadFieldINT(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadInt64(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out long value) + { + int bSuccess; + value = resGff.ReadFieldINT64(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadByte(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out byte value) + { + int bSuccess; + value = resGff.ReadFieldBYTE(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadCExoLocString(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out CExoLocString value) + { + int bSuccess; + value = resGff.ReadFieldCExoLocString(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadChar(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out byte value) + { + int bSuccess; + value = resGff.ReadFieldCHAR(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadWord(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out ushort value) + { + int bSuccess; + value = resGff.ReadFieldWORD(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadDWord(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out uint value) + { + int bSuccess; + value = resGff.ReadFieldDWORD(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadDWord64(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out ulong value) + { + int bSuccess; + value = resGff.ReadFieldDWORD64(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadFloat(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out float value) + { + int bSuccess; + value = resGff.ReadFieldFLOAT(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadShort(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out short value) + { + int bSuccess; + value = resGff.ReadFieldSHORT(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + + public static bool TryReadDouble(this CResGFF resGff, CResStruct resStruct, byte* fieldName, out double value) + { + int bSuccess; + value = resGff.ReadFieldDOUBLE(resStruct, fieldName, &bSuccess); + return bSuccess.ToBool(); + } + } +} diff --git a/src/main/Anvil/Services/ObjectStorage/ObjectStorage.cs b/src/main/Anvil/Services/ObjectStorage/ObjectStorage.cs index 9d04da466..6aa2f32cd 100644 --- a/src/main/Anvil/Services/ObjectStorage/ObjectStorage.cs +++ b/src/main/Anvil/Services/ObjectStorage/ObjectStorage.cs @@ -134,11 +134,11 @@ public bool Remove(string prefix, string key) stringMap.Remove(fullKey); } - internal string Serialize(bool persistOnly = true) + internal string Serialize(bool persistentDataOnly = true) { - List>> intData = GetValuesToSerialize(intMap, persistOnly); - List>> floatData = GetValuesToSerialize(floatMap, persistOnly); - List>> stringData = GetValuesToSerialize(stringMap, persistOnly); + List>> intData = GetValuesToSerialize(intMap, persistentDataOnly); + List>> floatData = GetValuesToSerialize(floatMap, persistentDataOnly); + List>> stringData = GetValuesToSerialize(stringMap, persistentDataOnly); if (intData.Count == 0 && floatData.Count == 0 && stringData.Count == 0) { @@ -158,7 +158,7 @@ void WriteStorage(IReadOnlyCollection value) in store) { - if (!persistOnly || value.Persist) + if (!persistentDataOnly || value.Persist) { if (!valueHasCount) { @@ -177,12 +177,8 @@ void WriteStorage(IReadOnlyCollection(Dictionary> store, int entryCo store[key] = new ObjectStorageValue { - Persist = persist, + Persist = isPersistentData, Value = parseValue(value), }; } @@ -275,6 +271,13 @@ internal ObjectStorage Clone() return clone; } + internal void Clear() + { + intMap.Clear(); + floatMap.Clear(); + stringMap.Clear(); + } + private int ReadLength(StringReader stringReader) { char c = ReadAndAssertEndOfData(stringReader); diff --git a/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs b/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs index bc0322dc9..922ec3b96 100644 --- a/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs +++ b/src/main/Anvil/Services/ObjectStorage/ObjectStorageService.cs @@ -9,7 +9,8 @@ namespace Anvil.Services [ServiceBinding(typeof(ObjectStorageService))] public sealed unsafe class ObjectStorageService { - private static readonly byte* GffFieldNamePtr = "NWNX_POS".GetNullTerminatedString(); + private static readonly byte* NWNXGffFieldNamePtr = "NWNX_POS".GetNullTerminatedString(); + private static readonly byte* AnvilGffFieldNamePtr = "ANVIL_POS".GetNullTerminatedString(); private static readonly Logger Log = LogManager.GetCurrentClassLogger(); @@ -41,13 +42,8 @@ public ObjectStorageService(HookService hookService) areaDestructorHook = hookService.RequestHook(OnAreaDestructor, FunctionsLinux._ZN8CNWSAreaD1Ev, HookOrder.VeryEarly); eatTURDHook = hookService.RequestHook(OnEatTURD, FunctionsLinux._ZN10CNWSPlayer7EatTURDEP14CNWSPlayerTURD, HookOrder.VeryEarly); dropTURDHook = hookService.RequestHook(OnDropTURD, FunctionsLinux._ZN10CNWSPlayer8DropTURDEv, HookOrder.VeryEarly); - - // We want to prioritize our call first for serialization, so it gets called last in the CallOriginal call in NWNX. - const int orderBeforeNWNX = HookOrder.VeryEarly - 1; - const int orderAfterNWNX = HookOrder.VeryEarly + 1; - - saveToGffHook = hookService.RequestHook(OnSaveToGff, FunctionsLinux._ZN8CNWSUUID9SaveToGffEP7CResGFFP10CResStruct, orderBeforeNWNX); - loadFromGffHook = hookService.RequestHook(OnLoadFromGff, FunctionsLinux._ZN8CNWSUUID11LoadFromGffEP7CResGFFP10CResStruct, orderAfterNWNX); + saveToGffHook = hookService.RequestHook(OnSaveToGff, FunctionsLinux._ZN8CNWSUUID9SaveToGffEP7CResGFFP10CResStruct, HookOrder.VeryEarly); + loadFromGffHook = hookService.RequestHook(OnLoadFromGff, FunctionsLinux._ZN8CNWSUUID11LoadFromGffEP7CResGFFP10CResStruct, HookOrder.VeryEarly); } public ObjectStorage GetObjectStorage(NwObject gameObject) @@ -138,7 +134,7 @@ private void OnSaveToGff(void* pUUID, void* pRes, void* pStruct) CResStruct resStruct = CResStruct.FromPointer(pStruct); string serialized = GetObjectStorage(uuid.m_parent).Serialize(); - resGff.WriteFieldCExoString(resStruct, serialized.ToExoString(), GffFieldNamePtr); + resGff.WriteFieldCExoString(resStruct, serialized.ToExoString(), AnvilGffFieldNamePtr); saveToGffHook.CallOriginal(pUUID, pRes, pStruct); } @@ -149,17 +145,38 @@ private int OnLoadFromGff(void* pUUID, void* pRes, void* pStruct) CResGFF resGff = CResGFF.FromPointer(pRes); CResStruct resStruct = CResStruct.FromPointer(pStruct); - int bSuccess; - CExoString str = resGff.ReadFieldCExoString(resStruct, GffFieldNamePtr, &bSuccess); - if (bSuccess.ToBool()) + bool hasAnvilPos = resGff.TryReadCExoString(resStruct, AnvilGffFieldNamePtr, out CExoString anvilSerialized); + bool hasNwnxPos = resGff.TryReadCExoString(resStruct, NWNXGffFieldNamePtr, out CExoString nwnxSerialized); + + if (!hasAnvilPos && !hasNwnxPos) + { + return loadFromGffHook.CallOriginal(pUUID, pRes, pStruct); + } + + ObjectStorage storage = GetObjectStorage(uuid.m_parent); + storage.Clear(); + + if (hasNwnxPos) + { + try + { + storage.Deserialize(nwnxSerialized.ToString()); + } + catch (Exception e) + { + Log.Error(e, "Failed to import NWNX object storage"); + } + } + + if (hasAnvilPos) { try { - GetObjectStorage(uuid.m_parent).Deserialize(str.ToString()); + storage.Deserialize(anvilSerialized.ToString()); } catch (Exception e) { - Log.Error(e, "Failed to load object storage"); + Log.Error(e, "Failed to load Anvil object storage"); } } From 371b4c7541236efc4f049690323b8259fab248e0 Mon Sep 17 00:00:00 2001 From: Jorteck Date: Sun, 17 Oct 2021 03:17:55 +0200 Subject: [PATCH 16/16] Implement player rest duration overrides. (#388) * Implement player rest duration overrides. * Formatting. * Updated changelog. --- CHANGELOG.md | 1 + NWN.Anvil.csproj.DotSettings | 1 + src/main/Anvil/API/Object/NwPlayer.cs | 23 +++++++ .../Object/RestDurationOverrideService.cs | 62 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 src/main/Anvil/Services/Object/RestDurationOverrideService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 82644ca42..80c5f4bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ https://github.com/nwn-dotnet/Anvil/compare/v8193.26.3...HEAD - ModuleEvents: Added `OnPlayerGuiEvent`, `OnNuiEvent` and `OnPlayerTarget` events. - GUIPanel: Added new constants published with NWN 8193.31 - NwPlayer: Added `SetGuiPanelDisabled` for disabling built-in GUI elements. +- NwPlayer: Added `RestDurationOverride` property. - VirtualMachine: Added `RecursionLevel` property. - LocalVariableCassowary: Added to support cassowary local variables. - ILateDisposable: Added a new service event interface that is invoked after the server is destroyed. diff --git a/NWN.Anvil.csproj.DotSettings b/NWN.Anvil.csproj.DotSettings index d10ccd36b..bffbfd860 100644 --- a/NWN.Anvil.csproj.DotSettings +++ b/NWN.Anvil.csproj.DotSettings @@ -26,6 +26,7 @@ True True True + True True True True diff --git a/src/main/Anvil/API/Object/NwPlayer.cs b/src/main/Anvil/API/Object/NwPlayer.cs index fa7d04291..0a1425713 100644 --- a/src/main/Anvil/API/Object/NwPlayer.cs +++ b/src/main/Anvil/API/Object/NwPlayer.cs @@ -21,6 +21,9 @@ public sealed partial class NwPlayer : IEquatable [Inject] private static EventService EventService { get; set; } + [Inject] + private static RestDurationOverrideService RestDurationOverrideService { get; set; } + internal readonly CNWSPlayer Player; internal NwPlayer(CNWSPlayer player) @@ -266,6 +269,26 @@ public float CutsceneCameraMoveRate set => NWScript.SetCutsceneCameraMoveRate(ControlledCreature, value); } + /// + /// Gets or sets a custom rest duration for this player.
+ /// Null indicates that no override is set. Assign null to use the default rest duration. + ///
+ public TimeSpan? RestDurationOverride + { + get => RestDurationOverrideService.GetDurationOverride(LoginCreature); + set + { + if (value.HasValue) + { + RestDurationOverrideService.SetDurationOverride(LoginCreature, value.Value); + } + else + { + RestDurationOverrideService.ClearDurationOverride(LoginCreature); + } + } + } + /// /// Sets the camera height for this player. /// diff --git a/src/main/Anvil/Services/Object/RestDurationOverrideService.cs b/src/main/Anvil/Services/Object/RestDurationOverrideService.cs new file mode 100644 index 000000000..41fe5491b --- /dev/null +++ b/src/main/Anvil/Services/Object/RestDurationOverrideService.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Anvil.API; +using NWN.Native.API; + +namespace Anvil.Services +{ + [ServiceBinding(typeof(RestDurationOverrideService))] + [ServiceBindingOptions(BindingOrder.API)] + internal sealed unsafe class RestDurationOverrideService + { + private static readonly CExoString DurationTableKey = "Duration".ToExoString(); + + private delegate uint AIActionRestHook(void* pCreature, void* pNode); + + private readonly FunctionHook aiActionRestHook; + private readonly Dictionary restDurationOverrides = new Dictionary(); + + public RestDurationOverrideService(HookService hookService) + { + aiActionRestHook = hookService.RequestHook(OnAIActionRest, FunctionsLinux._ZN12CNWSCreature12AIActionRestEP20CNWSObjectActionNode, HookOrder.Late); + } + + public TimeSpan? GetDurationOverride(NwCreature creature) + { + return restDurationOverrides.TryGetValue(creature, out int retVal) ? TimeSpan.FromMilliseconds(retVal) : null; + } + + public void SetDurationOverride(NwCreature creature, TimeSpan duration) + { + restDurationOverrides[creature] = (int)Math.Round(duration.TotalMilliseconds); + } + + public void ClearDurationOverride(NwCreature creature) + { + restDurationOverrides.Remove(creature); + } + + private uint OnAIActionRest(void* pCreature, void* pNode) + { + CNWSCreature creature = CNWSCreature.FromPointer(pCreature); + NwCreature nwCreature = creature.ToNwObject(); + + if (nwCreature != null && restDurationOverrides.TryGetValue(nwCreature, out int durationOverride)) + { + byte creatureLevel = creature.m_pStats.GetLevel(0); + int originalValue; + + C2DA durationTable = NWNXLib.Rules().m_p2DArrays.m_pRestDurationTable; + + durationTable.GetINTEntry(creatureLevel, DurationTableKey, &originalValue); + durationTable.SetINTEntry(creatureLevel, DurationTableKey, durationOverride); + uint retVal = aiActionRestHook.CallOriginal(pCreature, pNode); + durationTable.SetINTEntry(creatureLevel, DurationTableKey, originalValue); + + return retVal; + } + + return aiActionRestHook.CallOriginal(pCreature, pNode); + } + } +}