Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions NWNXLib/API/API/CNWSDoor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions NWNXLib/API/API/CNWSItem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions NWNXLib/API/API/CNWSPlaceable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions NWNXLib/API/API/CNetLayer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions NWNXLib/API/API/CNetLayerWindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ typedef int BOOL;

struct CNetLayerWindow
{
CNetInstance m_nInstance;
BOOL m_bInitialized;
CNetLayerInternal * m_pNetLayer;
BOOL m_bWindowInUse;
Expand Down
12 changes: 12 additions & 0 deletions NWNXLib/API/nwn_api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <memory>
#include <vector>
#include <type_traits>
#include <netinet/in.h>

#include "External/json/json.hpp"

Expand Down Expand Up @@ -223,6 +224,17 @@ struct CNetConnectionId : public DistinctSimpleType<uint32_t>
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(void*)>; \
void nwnxSet(const std::string& key, int value, bool persist = false, const char *pn = PLUGIN_NAME); \
Expand Down
6 changes: 2 additions & 4 deletions Plugins/Administration/Administration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>();
LOG_NOTICE("Set player password to '%s'.", newPass);
Globals::AppManager()->m_pServerExoApp->GetNetLayer()->SetPlayerPassword(newPass.c_str());
LOG_DEBUG("Set player password.");
return {};
}

Expand All @@ -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<std::string>();
LOG_NOTICE("Set DM password to '%s'.", newPass);
Globals::AppManager()->m_pServerExoApp->GetNetLayer()->SetGameMasterPassword(newPass.c_str());
LOG_DEBUG("Set DM password.");
return {};
}

Expand Down
3 changes: 2 additions & 1 deletion Plugins/Experimental/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ add_plugin(Experimental
"UnhardcodeRangerDualWield.cpp"
"PlayerHitpointsAsPercentage.cpp"
"IgnoreModuleVersion.cpp"
"EndCombatRoundAfterSpellcast.cpp")
"EndCombatRoundAfterSpellcast.cpp"
"UFMHotfix.cpp")
1 change: 1 addition & 0 deletions Plugins/Experimental/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
215 changes: 215 additions & 0 deletions Plugins/Experimental/UFMHotfix.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>

#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<BOOL>(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<bool>("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);
}
Loading
Loading