diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f299ae9d5..54304280a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ https://github.com/nwnxee/unified/compare/build8193.37.13...HEAD - Events: Added `NWNX_EVENT_INIT_ON_FIRST_SUBSCRIBE` messagebus message as a wrapper for the `InitOnFirstSubscribe` function. Broadcasts `NWNX_EVENT_INIT_ON_FIRST_SUBSCRIBE_CALLBACK` message when a registered event gets subscribed to. - Tweaks: Added `NWNX_TWEAKS_CHARLIST_SORT_BY_LAST_PLAYED_DATE` to enable character list sorting by last played date - Events: Added events `NWNX_ON_DECREMENT_REMAINING_FEAT_USES_{BEFORE|AFTER}` which fire when the remaining uses of a feat are decremented +- Experimental: added `NWNX_EXPERIMENTAL_UFM_HOTFIX` to attempt to fix a server hang in CNetLayerWindow::UnpacketizeFullMessages. ##### New Plugins - N/A @@ -23,11 +24,13 @@ https://github.com/nwnxee/unified/compare/build8193.37.13...HEAD - Creature: GetNumberOfBonusSpells(), ModifyNumberBonusSpells() - Store: GetBlackMarket(), SetBlackMarket() - Util: SetStartingLocation() +- Object: GetLocalizedDescription(), SetLocalizedDescription() ### Changed - Damage: Added bRangedAttack to the NWNX_Damage_AttackEventData struct. - Events: Added ID to the NWNX_ON_ITEMPROPERTY_EFFECT_* events data. - Utils: Change LOG_INFO to LOG_DEBUG for console commands. +- Admin: Player/DM password functions no longer print the passwords to the log. ### Deprecated - N/A diff --git a/NWNXLib/API/API/CNWSDoor.hpp b/NWNXLib/API/API/CNWSDoor.hpp index 976c3a60d98..187d0cbc842 100644 --- a/NWNXLib/API/API/CNWSDoor.hpp +++ b/NWNXLib/API/API/CNWSDoor.hpp @@ -95,6 +95,7 @@ struct CNWSDoor : CNWSObject Vector GetNearestActionPoint(const Vector & vCloseTo, BOOL bPretendDoorIsClosed = false); inline CExoLocString &GetFirstName() override { return m_sLocName; } + inline CExoLocString &GetDescription() { return m_sDescription; } #ifdef NWN_CLASS_EXTENSION_CNWSDoor NWN_CLASS_EXTENSION_CNWSDoor diff --git a/NWNXLib/API/API/CNWSItem.hpp b/NWNXLib/API/API/CNWSItem.hpp index 4dfee81d24a..e0843704393 100644 --- a/NWNXLib/API/API/CNWSItem.hpp +++ b/NWNXLib/API/API/CNWSItem.hpp @@ -117,6 +117,7 @@ struct CNWSItem : CNWItem, CNWSObject float CalculatePassiveCost(CNWItemProperty * pItemProperty); inline CExoLocString &GetFirstName() override { return m_sName; } + inline CExoLocString &GetDescription(bool nIdentified) { return nIdentified ? m_sIdentifiedDescription : m_sUnidentifiedDescription; } #ifdef NWN_CLASS_EXTENSION_CNWSItem NWN_CLASS_EXTENSION_CNWSItem diff --git a/NWNXLib/API/API/CNWSPlaceable.hpp b/NWNXLib/API/API/CNWSPlaceable.hpp index d6c5cf2a32b..82c3ad2bf32 100644 --- a/NWNXLib/API/API/CNWSPlaceable.hpp +++ b/NWNXLib/API/API/CNWSPlaceable.hpp @@ -127,6 +127,7 @@ struct CNWSPlaceable : CNWSObject void CalculateActionPoints(); inline CExoLocString &GetFirstName() override { return m_sLocName; } + inline CExoLocString &GetDescription() { return m_sDescription; } #ifdef NWN_CLASS_EXTENSION_CNWSPlaceable NWN_CLASS_EXTENSION_CNWSPlaceable diff --git a/NWNXLib/API/API/CNetLayer.hpp b/NWNXLib/API/API/CNetLayer.hpp index 234d544bafe..97264ceb201 100644 --- a/NWNXLib/API/API/CNetLayer.hpp +++ b/NWNXLib/API/API/CNetLayer.hpp @@ -16,14 +16,9 @@ struct CNetLayerSessionInfo; typedef int BOOL; -enum class CNetInstance -{ - INSTANCE_CLIENT, - INSTANCE_SERVER -}; - struct CNetLayer { + const CNetInstance m_nInstance; CNetLayerInternal * m_pcNetLayerInternal; CNetLayer(CNetInstance); diff --git a/NWNXLib/API/API/CNetLayerWindow.hpp b/NWNXLib/API/API/CNetLayerWindow.hpp index 76604ff45d7..60d77d09eca 100644 --- a/NWNXLib/API/API/CNetLayerWindow.hpp +++ b/NWNXLib/API/API/CNetLayerWindow.hpp @@ -17,6 +17,7 @@ typedef int BOOL; struct CNetLayerWindow { + CNetInstance m_nInstance; BOOL m_bInitialized; CNetLayerInternal * m_pNetLayer; BOOL m_bWindowInUse; diff --git a/NWNXLib/API/nwn_api.hpp b/NWNXLib/API/nwn_api.hpp index 52e5ab4efbb..89e28d4f81a 100644 --- a/NWNXLib/API/nwn_api.hpp +++ b/NWNXLib/API/nwn_api.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "External/json/json.hpp" @@ -223,6 +224,17 @@ struct CNetConnectionId : public DistinctSimpleType static const CNetConnectionId SINGLEPLAYER; }; +struct CNetPeer +{ + struct sockaddr_in m_address; +}; + +enum class CNetInstance +{ + INSTANCE_CLIENT, + INSTANCE_SERVER +}; + #define NWN_CLASS_EXTENSION_CGameObject \ using CleanupFunc = std::function; \ void nwnxSet(const std::string& key, int value, bool persist = false, const char *pn = PLUGIN_NAME); \ diff --git a/Plugins/Administration/Administration.cpp b/Plugins/Administration/Administration.cpp index c015c694c68..621ff73772c 100644 --- a/Plugins/Administration/Administration.cpp +++ b/Plugins/Administration/Administration.cpp @@ -57,15 +57,14 @@ static CExoLinkedListNode* FindTURD(std::string playerName, std::string characte NWNX_EXPORT ArgumentStack GetPlayerPassword(ArgumentStack&&) { const CExoString password = Globals::AppManager()->m_pServerExoApp->GetNetLayer()->GetPlayerPassword(); - LOG_DEBUG("Returned player password '%s'.", password.m_sString); return std::string(password.m_sString ? password.m_sString : ""); } NWNX_EXPORT ArgumentStack SetPlayerPassword(ArgumentStack&& args) { const auto newPass = args.extract(); - LOG_NOTICE("Set player password to '%s'.", newPass); Globals::AppManager()->m_pServerExoApp->GetNetLayer()->SetPlayerPassword(newPass.c_str()); + LOG_DEBUG("Set player password."); return {}; } @@ -79,15 +78,14 @@ NWNX_EXPORT ArgumentStack ClearPlayerPassword(ArgumentStack&&) NWNX_EXPORT ArgumentStack GetDMPassword(ArgumentStack&&) { const CExoString password = Globals::AppManager()->m_pServerExoApp->GetNetLayer()->GetGameMasterPassword(); - LOG_DEBUG("Returned DM password '%s'.", password.m_sString); return std::string(password.m_sString ? password.m_sString : ""); } NWNX_EXPORT ArgumentStack SetDMPassword(ArgumentStack&& args) { const auto newPass = args.extract(); - LOG_NOTICE("Set DM password to '%s'.", newPass); Globals::AppManager()->m_pServerExoApp->GetNetLayer()->SetGameMasterPassword(newPass.c_str()); + LOG_DEBUG("Set DM password."); return {}; } diff --git a/Plugins/Experimental/CMakeLists.txt b/Plugins/Experimental/CMakeLists.txt index 75b51abf554..85e8b0c06d1 100644 --- a/Plugins/Experimental/CMakeLists.txt +++ b/Plugins/Experimental/CMakeLists.txt @@ -5,4 +5,5 @@ add_plugin(Experimental "UnhardcodeRangerDualWield.cpp" "PlayerHitpointsAsPercentage.cpp" "IgnoreModuleVersion.cpp" - "EndCombatRoundAfterSpellcast.cpp") + "EndCombatRoundAfterSpellcast.cpp" + "UFMHotfix.cpp") diff --git a/Plugins/Experimental/README.md b/Plugins/Experimental/README.md index b3f2f1b72f9..f19376241cc 100644 --- a/Plugins/Experimental/README.md +++ b/Plugins/Experimental/README.md @@ -20,3 +20,4 @@ The following environmental variable is required to load the plugin: | `NWNX_EXPERIMENTAL_UNHARDCODE_RANGER_DUALWIELD` | true/false | Removes the hardcoded effects of the Ranger's Dual-wield feat. | | `NWNX_EXPERIMENTAL_IGNORE_MODULE_VERSION` | true/false | Ignore the module's version when loading. | | `NWNX_EXPERIMENTAL_END_COMBATROUND_AFTER_SPELLCAST` | true/false | Combat rounds end immediately a spell cast making it possible to cast another spell right after | +| `NWNX_EXPERIMENTAL_UFM_HOTFIX` | true/false | Attemps to resolve a server hang in CNetLayerWindow::UnpacketizeFullMessages. | diff --git a/Plugins/Experimental/UFMHotfix.cpp b/Plugins/Experimental/UFMHotfix.cpp new file mode 100644 index 00000000000..68b150bb43f --- /dev/null +++ b/Plugins/Experimental/UFMHotfix.cpp @@ -0,0 +1,215 @@ +// +// This implements a hotfix for the mythical UnpacketizeFullMessages->fmtStr server hang +// that community servers have seen. +// +// This is *entirely theoretical so far* and has not been verified to actually WAR it. +// Even if it DOES address the hang, then this may not be a fully mergeable upstream fix. +// (The game might need a punchthrough to detect sliding window dtoring). +// +// This plugin is a minimal attack surface, hopefully, and ONLY addresses the packets=1 +// case where the original function is re-entrant if the actual frame has multiple +// messages packed into it. If one of the earlier messages is a "disconnect player and kill +// sliding window" message, then the later messages will read invalid data and go into +// a ~infinite loop on the frame counter. +// +// Also see nwn/issues#4398 +// +// Version: 1.0, 2026-05-02, niv +// + +#include "nwn_api.hpp" +#include "nwnx.hpp" + +#include +#include + +#include "API/CNetLayer.hpp" +#include "API/CExoString.hpp" +#include "API/CNetLayerPlayerInfo.hpp" +#include "API/CNetLayerWindow.hpp" + +using namespace NWNXLib; + +static Hooks::Hook ufmHook; + +#define INVALID_FRAME 65535 +#define TRUE 1 +#define FALSE 0 +#define NWN_MAX_PLAYERS 0xff +#define FRAME_HEADER_LENGTH 12 + +struct CExoNetExtendableBuffer +{ + BOOL GetFrameData(uint16_t nFrameIndex, uint8_t** pData, uint32_t* nFrameSize, + CNetConnectionId* nConnectionId, uint16_t* nNextFrame); + uint32_t ChangeFrameReference(uint16_t nFrameIndex, int32_t nIncrement); +}; + +struct CNetLayerInternal +{ + BOOL UncompressMessage(uint32_t nPlayerId, uint8_t* pData, uint32_t nSize); + + struct IntendedConnectionData + { + uint8_t m_nConnectionType; + int32_t m_nPlayerLanguage; + CExoString m_sPlayerName; + CExoString m_sCDKey; + CExoString m_sLegacyCDKey; + }; + + CNetInstance m_nInstance; + IntendedConnectionData m_pConnectData; + CExoString m_sConnectExpectCryptoPublicKeyBase64; + CBaseExoApp* m_pcExoApp; + struct CExoNet* m_pcExoNet; + BOOL m_bInitialized; + CNetLayerWindow m_aWindows[NWN_MAX_PLAYERS]; + CNetLayerPlayerInfo m_pcPlayerInfo[NWN_MAX_PLAYERS]; + CExoNetExtendableBuffer* m_pebFrameStorage; // offset verified OK +}; + +static inline uint16_t GetFrameNumPackets(uint8_t* pData) +{ + return (uint16_t)((((uint16_t)pData[8]) << 8) + pData[9]); +} + +static inline uint32_t GetFrameMessageSize(uint8_t* pData) +{ + if (pData[7] & 0x80) + { + return ((uint32_t)(pData[10] << 8) + (uint32_t)(pData[11]) + (uint32_t)(pData[12] << 24) + + (uint32_t)(pData[13] << 16)); + } + else + { + return ((uint32_t)(pData[10] << 8) + (uint32_t)(pData[11])); + } +} + +static BOOL ufmImpl(CNetLayerWindow* thisPtr, BOOL bHighPriority) +{ + uint16_t nFrame; + uint8_t* pData; + uint32_t nDataSize; + uint16_t nNextFrame; + CNetConnectionId nConnectionId; + + static_assert(sizeof(CNetInstance) == 4, "Need API fix @Daz"); + static_assert(offsetof(CNetLayer, m_nInstance) == 0x0, "Need API fix @Daz"); + static_assert(offsetof(CNetLayerWindow, m_nInstance) == 0x0, "Need API fix @Daz"); + static_assert(offsetof(CNetLayerInternal, m_nInstance) == 0x0, "Need API fix @Daz"); + static_assert(offsetof(CNetLayerInternal, m_aWindows) == 0x68, "Need API fix @Daz"); + + nFrame = INVALID_FRAME; + + if (bHighPriority == TRUE && thisPtr->m_lQueueIncomingHiFrames.num > 0) + { + nFrame = thisPtr->m_lQueueIncomingHiFrames[0]; + } + else if (bHighPriority == FALSE && thisPtr->m_lQueueIncomingLoFrames.num > 0) + { + nFrame = thisPtr->m_lQueueIncomingLoFrames[0]; + } + else + { + return FALSE; + } + + ASSERT_OR_RETURN(nFrame != INVALID_FRAME, FALSE); + + pData = NULL; + + thisPtr->m_pNetLayer->m_pebFrameStorage->GetFrameData(nFrame, &pData, &nDataSize, + &nConnectionId, &nNextFrame); + + ASSERT_OR_RETURN(pData != NULL, FALSE); + + uint16_t nPacketsRequired = GetFrameNumPackets(pData); + + if (nPacketsRequired != 1) + { + return ufmHook->CallOriginal(thisPtr, bHighPriority); + } + + // + // Potential sporadic server hang fix that has been plagueing PWs: + // + // Decompose the messages that share this packet. + // We must not walk the live frame-storage buffer directly because + // HandleMessage() can disconnect the player re-entrantly and tear the + // current window down, which frees queued incoming frames underneath us. + // + // This hang is hard to chase and harder to repro in production and likely + // has been around since 1.69; but EE has started sending larger C2S packets + // and is seeing more player traffic, so the case where clients frame-pack messages + // has become more common. + // + uint8_t* pMessageData = new uint8_t[nDataSize]; + SCOPEGUARD(delete[] pMessageData); + memcpy(pMessageData, pData, nDataSize); + + uint32_t nRemainingSize = nDataSize; + BOOL bPacketWellFormed = TRUE; + + while (nRemainingSize >= FRAME_HEADER_LENGTH) + { + uint8_t* pCurrentMessage = pMessageData + nDataSize - nRemainingSize; + uint32_t nPayloadSize = GetFrameMessageSize(pCurrentMessage); + + if (nPayloadSize > nRemainingSize - FRAME_HEADER_LENGTH) + { + LOG_WARNING( + "Packet not wellformed; sliding window gone? This would have hung the server! " + "player=%u packet=%u remaining=%u payload=%u flags=0x%02x", + thisPtr->m_nPlayerId, nDataSize, nRemainingSize, nPayloadSize, pCurrentMessage[7]); + bPacketWellFormed = FALSE; + break; + } + + uint32_t nMessageDataSize = nPayloadSize + FRAME_HEADER_LENGTH; + + if (!thisPtr->m_pNetLayer->UncompressMessage(thisPtr->m_nPlayerId, pCurrentMessage, + nMessageDataSize)) + { + bPacketWellFormed = FALSE; + break; + } + + nRemainingSize -= nMessageDataSize; + } + + if (bPacketWellFormed == TRUE && nRemainingSize != 0) + { + LOG_WARNING( + "Packet has trailing data after unpacketization! " + "player=%u packet=%u trailing=%u", + thisPtr->m_nPlayerId, nDataSize, nRemainingSize); + } + + if (bHighPriority == TRUE) + { + nFrame = thisPtr->m_lQueueIncomingHiFrames[0]; + thisPtr->m_lQueueIncomingHiFrames.DelIndex(0); + } + else + { + nFrame = thisPtr->m_lQueueIncomingLoFrames[0]; + thisPtr->m_lQueueIncomingLoFrames.DelIndex(0); + } + + thisPtr->m_pNetLayer->m_pebFrameStorage->ChangeFrameReference(nFrame, -1); + + return TRUE; +} + +static void UFM_HotfixCtor() __attribute__((constructor)); +static void UFM_HotfixCtor() +{ + if (!Config::Get("UFM_HOTFIX", false)) + return; + + LOG_INFO("EXPERIMENTAL: Attempting to resolve a server hang in CNetLayerWindow::UnpacketizeFullMessages."); + + ufmHook = Hooks::HookFunction(&CNetLayerWindow::UnpacketizeFullMessages, ufmImpl, Hooks::Order::Earliest); +} diff --git a/Plugins/Object/NWScript/nwnx_object.nss b/Plugins/Object/NWScript/nwnx_object.nss index cbc27f77fb4..a5fbd7a1d8b 100644 --- a/Plugins/Object/NWScript/nwnx_object.nss +++ b/Plugins/Object/NWScript/nwnx_object.nss @@ -432,6 +432,21 @@ string NWNX_Object_GetLocalizedName(object oObject, int nLanguage, int nGender = /// @param nGender Gender to use, 0 or 1. void NWNX_Object_SetLocalizedName(object oObject, string sName, int nLanguage, int nGender = 0); +/// @brief Return the description of the object for nLanguage. +/// @param oObject an object +/// @param nLanguage A PLAYER_LANGUAGE constant. +/// @param nGender Gender to use, 0 or 1. +/// @return The localized string. +/// @param bIdentified Only for items, identified description or not +string NWNX_Object_GetLocalizedDescription(object oObject, int nLanguage, int nGender = 0, int bIdentified = 1); + +/// @brief Set the description of the object as set in the toolset for nLanguage. +/// @param oObject an object +/// @param sDescription New value to set +/// @param nLanguage A PLAYER_LANGUAGE constant. +/// @param nGender Gender to use, 0 or 1. +/// @param bIdentified Only for items, identified description or not +void NWNX_Object_SetLocalizedDescription(object oObject, string sDescription, int nLanguage, int nGender = 0, int bIdentified = 1); /// @} int NWNX_Object_GetLocalVariableCount(object obj) @@ -908,3 +923,29 @@ void NWNX_Object_SetLocalizedName(object oObject, string sName, int nLanguage, i NWNXCall(NWNX_Object, sFunc); } + +string NWNX_Object_GetLocalizedDescription(object oObject, int nLanguage, int nGender = 0, int bIdentified = 1) +{ + string sFunc = "GetLocalizedDescription"; + + NWNXPushInt(bIdentified); + NWNXPushInt(nGender); + NWNXPushInt(nLanguage); + NWNXPushObject(oObject); + + NWNXCall(NWNX_Object, sFunc); + return NWNXPopString(); +} + +void NWNX_Object_SetLocalizedDescription(object oObject, string sDescription, int nLanguage, int nGender = 0, int bIdentified = 1) +{ + string sFunc = "SetLocalizedDescription"; + + NWNXPushInt(bIdentified); + NWNXPushInt(nGender); + NWNXPushInt(nLanguage); + NWNXPushString(sDescription); + NWNXPushObject(oObject); + + NWNXCall(NWNX_Object, sFunc); +} diff --git a/Plugins/Object/Object.cpp b/Plugins/Object/Object.cpp index 43856e2341e..df678e30bc8 100644 --- a/Plugins/Object/Object.cpp +++ b/Plugins/Object/Object.cpp @@ -1263,7 +1263,7 @@ NWNX_EXPORT ArgumentStack GetLocalizedName(ArgumentStack&& args) return myString; } - return {}; + return ""; } NWNX_EXPORT ArgumentStack SetLocalizedName(ArgumentStack&& args) @@ -1294,3 +1294,56 @@ NWNX_EXPORT ArgumentStack SetLocalizedName(ArgumentStack&& args) return {}; } + +NWNX_EXPORT ArgumentStack GetLocalizedDescription(ArgumentStack&& args) +{ + if (auto *pGameObject = Utils::PopGameObject(args)) + { + const auto nLanguage = args.extract(); + const auto nGender = args.extract(); + const auto nIdentified = args.extract(); + + CExoString myString; + + if (auto *pDoor = Utils::AsNWSDoor(pGameObject)) + pDoor->GetDescription().GetString(nLanguage, &myString, nGender); + else if (auto *pPlaceable = Utils::AsNWSPlaceable(pGameObject)) + pPlaceable->GetDescription().GetString(nLanguage, &myString, nGender); + else if (auto *pItem = Utils::AsNWSItem(pGameObject)) + pItem->GetDescription(nIdentified).GetString(nLanguage, &myString, nGender); + + return myString; + } + return ""; +} + +NWNX_EXPORT ArgumentStack SetLocalizedDescription(ArgumentStack&& args) +{ + if (auto *pGameObject = Utils::PopGameObject(args)) + { + const auto sValue = args.extract(); + const auto nLanguage = args.extract(); + const auto nGender = args.extract(); + const auto nIdentified = args.extract(); + + CExoString myString(sValue); + + if (auto *pDoor = Utils::AsNWSDoor(pGameObject)) + { + pDoor->GetDescription().RemoveString(nLanguage, nGender); + pDoor->GetDescription().AddString(nLanguage, myString, nGender); + } + else if (auto *pPlaceable = Utils::AsNWSPlaceable(pGameObject)) + { + pPlaceable->GetDescription().RemoveString(nLanguage, nGender); + pPlaceable->GetDescription().AddString(nLanguage, myString, nGender); + } + else if (auto *pItem = Utils::AsNWSItem(pGameObject)) + { + pItem->GetDescription(nIdentified).RemoveString(nLanguage, nGender); + pItem->GetDescription(nIdentified).AddString(nLanguage, myString, nGender); + } + } + + return {}; +}