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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ https://github.com/nwnxee/unified/compare/build8193.37.13...HEAD
- Tweaks: added `NWNX_TWEAKS_UNHARDCODE_SPECIAL_ABILITY_TARGET_TYPE` to allow special abilities to be used on target types other than creatures.
- Events: Added events `NWNX_ON_ABILITY_CHANGE_{BEFORE|AFTER}` which fire when an ability of a player changes.
- 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

##### New Plugins
- N/A
Expand Down
3 changes: 2 additions & 1 deletion Plugins/Tweaks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ add_plugin(Tweaks
"CutsceneModeNoTURD.cpp"
"CanUseItemsWhilePolymorphed.cpp"
"ResistEnergyStacksWithEpicEnergyResistance.cpp"
"UnhardcodeSpecialAbilityTargetType.cpp")
"UnhardcodeSpecialAbilityTargetType.cpp"
"SortCharListByLastPlayedDate.cpp")
7 changes: 6 additions & 1 deletion Plugins/Tweaks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ Tweaks stuff. See below.
| `NWNX_TWEAKS_CUTSCENE_MODE_NO_TURD` | true or false | SetCutsceneMode() will not cause a TURD to be dropped. |
| `NWNX_TWEAKS_CAN_USE_ITEMS_WHILE_POLYMORPHED` | true or false | Allow all items to be used while polymorphed. |
| `NWNX_TWEAKS_RESIST_ENERGY_STACKS_WITH_EPIC_ENERGY_RESISTANCE` | true or false | Resist Energy feats stack with Epic Energy Resistance. |
| `NWNX_TWEAKS_UNHARDCODE_SPECIAL_ABILITY_TARGET_TYPE` | true or false | Allows special abilities to be used on target types other than creatures. |
| `NWNX_TWEAKS_UNHARDCODE_SPECIAL_ABILITY_TARGET_TYPE` | true or false | Allows special abilities to be used on target types other than creatures. |
| `NWNX_TWEAKS_CHARLIST_SORT_BY_LAST_PLAYED_DATE` | true or false | Servervault characters will be sorted by last time played date, instead of character name in the character list |

## Environment variable values

Expand Down Expand Up @@ -122,3 +123,7 @@ Some values for convenience:
| 7168 | All Good/Evil vs AlignmentGroup |
| 57344 | All Good/Evil vs SpecificAlignment |
| 65535 | Hide All VFX |

## NWNX_TWEAKS_CHARLIST_SORT_BY_LAST_PLAYED_DATE

Warning: This tweak will disable savegame characters getting sent to the client. If you are using multiplayer savegames do not use this tweak!
276 changes: 276 additions & 0 deletions Plugins/Tweaks/SortCharListByLastPlayedDate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
#include "nwnx.hpp"

#include "API/CAppManager.hpp"
#include "API/CServerExoApp.hpp"
#include "API/CNetLayer.hpp"
#include "API/CNWSMessage.hpp"
#include "API/CNWSPlayer.hpp"
#include "API/CServerInfo.hpp"
#include "API/CNetLayerPlayerInfo.hpp"
#include "API/CExoBase.hpp"
#include "API/CExoResMan.hpp"
#include "API/CResGFF.hpp"
#include "API/CResStruct.hpp"
#include "API/NWPlayerCharacterList_st.hpp"
#include "API/CResList.hpp"
#include "API/CNWSModule.hpp"
#include "API/CExoAliasList.hpp"
#include "API/CExoLocStringInternal.hpp"

#include <algorithm>
#include <vector>
#include <sys/stat.h>

using namespace NWNXLib;
using namespace NWNXLib::API;
using namespace NWNXLib::API::Constants;

static BOOL SendServerToPlayerCharListHook(CNWSMessage* pMessage, CNWSPlayer* pPlayer);
static Hooks::Hook s_SendServerToPlayerCharListHook;

struct CharacterInfoWithFiletime : NWPlayerCharacterList_st
{
std::time_t fLastFileModified;
};

void SortCharListByLastPlayedDate() __attribute__((constructor));
void SortCharListByLastPlayedDate()
{
if (!Config::Get<bool>("CHARLIST_SORT_BY_LAST_PLAYED_DATE", false))
return;

LOG_INFO("Character list will be sorted by last played date");
s_SendServerToPlayerCharListHook = Hooks::HookFunction(&CNWSMessage::SendServerToPlayerCharList, &SendServerToPlayerCharListHook, Hooks::Order::Final);
}

static void SetCharacterListIndex(CharacterInfoWithFiletime* pCharInfo, int nIndex)
{
// Set lowest bit of each rgb component to 1, reducing the range of available indices from 2^24 to 2^21
// while avoiding null bytes and preserving uniqueness + order
nIndex = nIndex << 1;
int b = (nIndex & 0xFF) | 1;
int g = ((nIndex >> 7) & 0xFF) | 1;
int r = ((nIndex >> 14) & 0xFF) | 1;

auto& pStrList = pCharInfo->sLocFirstName.m_pExoLocStringInternal->m_lstString;
for (auto *pNode = pStrList.GetHeadPos(); pNode; pNode = pNode->pNext)
{
if (EXOLOCSTRING* sCharName = pStrList.GetAtPos(pNode))
sCharName->sString = CExoString::F("<c%c%c%c></c>", r, g, b) + sCharName->sString;
}
}

static std::time_t GetBICFileTime(const CExoString& sVaultPath, const CResRef& sBicResRef)
{
CExoString sFilename = sVaultPath + CExoString(sBicResRef) + ".bic";

struct stat fileStats;
if (stat(sFilename.c_str(), &fileStats) == 0)
return fileStats.st_mtime;
else
return 0;
}

static void GetServerVaultBasePaths(CNWSPlayer* pPlayer, CExoString* sOutRootServerVault, CExoString* sOutPlayerServerVault)
{
CServerExoApp* pExoApp = Globals::AppManager()->m_pServerExoApp;

*sOutRootServerVault = Globals::ExoBase()->m_pcExoAliasList->GetAliasPath("SERVERVAULT");
CExoString sPlayerdir;
if (pExoApp->GetServerInfo()->m_PersistantWorldOptions.bServerVaultByPlayerName)
sPlayerdir = pPlayer->GetPlayerName();
else
sPlayerdir = pExoApp->GetNetLayer()->GetPlayerInfo(pPlayer->m_nPlayerID)->m_cCDKey.sPublic;

*sOutPlayerServerVault = *sOutRootServerVault + sPlayerdir + "/";
}

static BOOL SendServerToPlayerCharListHook(CNWSMessage* pMessage, CNWSPlayer* pPlayer)
{
auto* pAppManager = Globals::AppManager();
auto* pExoApp = pAppManager->m_pServerExoApp;
auto* pServerInfo = pExoApp->GetServerInfo();
CNetLayerPlayerInfo* pPlayerInfo = pExoApp->GetNetLayer()->GetPlayerInfo(pPlayer->m_nPlayerID);

pMessage->CreateWriteMessage(sizeof(uint16_t), pPlayer->m_nPlayerID);
if ((pAppManager->m_bMultiplayerEnabled && pServerInfo->m_JoiningRestrictions.bAllowLocalVaultChars) ||
(pPlayerInfo->m_bPlayerInUse && pPlayerInfo->m_bGameMasterPrivileges))
{
pMessage->WriteWORD(0);
}
else
{
std::vector<CharacterInfoWithFiletime> lstChars;

CExoString sRootServerVault;
CExoString sPlayerServerVault;
GetServerVaultBasePaths(pPlayer, &sRootServerVault, &sPlayerServerVault);

CExoString sSubdirectory = pPlayerInfo->m_cCDKey.sPublic;
if (pServerInfo->m_PersistantWorldOptions.bServerVaultByPlayerName)
sSubdirectory = pPlayer->GetPlayerName();

CExoArrayList<CExoString> lstBicFiles;
lstBicFiles.SetSize(0);
CExoString sDirectory = CExoString::F("SERVERVAULT:%s", sSubdirectory.CStr());
Globals::ExoBase()->GetDirectoryList(&lstBicFiles, sDirectory, ResRefType::BIC);
if (lstBicFiles.num > 0)
{
Globals::ExoResMan()->AddResourceDirectory(sDirectory, 81000000);
for (int i = 0; i < lstBicFiles.num; i++)
{
CExoString sBicResRef = lstBicFiles[i];
sBicResRef = sBicResRef.SubString(0, sBicResRef.m_nStringLength - 4);

CResGFF* pRes = new CResGFF(ResRefType::BIC, "BIC ", sBicResRef);
if (pRes->m_bLoaded)
{
if (!pRes->m_bValidationFailed)
{
BOOL bSuccess;
CResStruct topLevelStruct;

pRes->GetTopLevelStruct(&topLevelStruct);

CharacterInfoWithFiletime charInfo;
charInfo.sLocFirstName = pRes->ReadFieldCExoLocString(&topLevelStruct, "FirstName", bSuccess);
charInfo.sLocLastName = pRes->ReadFieldCExoLocString(&topLevelStruct, "LastName", bSuccess);
charInfo.resFileName = sBicResRef;
charInfo.nType = MessageLoginMinor::ServerSubDirectoryCharacter;
charInfo.nPortraitId = pRes->ReadFieldWORD(&topLevelStruct, "PortraitId", bSuccess, 0xFFFF);
charInfo.resPortrait = pRes->ReadFieldCResRef(&topLevelStruct, "Portrait", bSuccess);
charInfo.fLastFileModified = GetBICFileTime(sPlayerServerVault, charInfo.resFileName);

CResList classList;
if (pRes->GetList(&classList, &topLevelStruct, "ClassList"))
{
uint32_t nClassCount = pRes->GetListCount(&classList);
for (uint32_t i = 0; i < nClassCount; i++)
{
CResStruct classStruct;
if (pRes->GetListElement(&classStruct, &classList, i))
{
NWPlayerCharacterListClass_st classInfo;
classInfo.nClass = pRes->ReadFieldINT(&classStruct, "Class", bSuccess, -1);
classInfo.nClassLevel = (uint8_t)pRes->ReadFieldSHORT(&classStruct, "ClassLevel", bSuccess);
charInfo.lstClasses.Add(classInfo);
}
}
}

lstChars.push_back(charInfo);
}
else
{
LOG_ERROR("Corrupt BIC file in servervault of player: '%s' (%s) -> '%s.bic', character skipped.",
pPlayer->GetPlayerName().CStr(),
pPlayerInfo->m_cCDKey.sPublic.CStr(),
sBicResRef.CStr());
}
}

delete pRes;
}

Globals::ExoResMan()->RemoveResourceDirectory(sDirectory);
}

lstBicFiles.SetSize(0);
if (!pServerInfo->m_PersistantWorldOptions.bSuppressBaseServerVault)
{
Globals::ExoBase()->GetDirectoryList(&lstBicFiles, "SERVERVAULT:", ResRefType::BIC);
if (lstBicFiles.num > 0)
{
for (int i = 0; i < lstBicFiles.num; i++)
{
CExoString sBicResRef = lstBicFiles[i];
sBicResRef = sBicResRef.SubString(0, sBicResRef.m_nStringLength - 4);

CResGFF* pRes = new CResGFF(ResRefType::BIC, "BIC ", sBicResRef);
if (pRes->m_bLoaded)
{
if (!pRes->m_bValidationFailed)
{
BOOL bSuccess;
CResStruct topLevelStruct;

pRes->GetTopLevelStruct(&topLevelStruct);

CharacterInfoWithFiletime charInfo;
charInfo.sLocFirstName = pRes->ReadFieldCExoLocString(&topLevelStruct, "FirstName", bSuccess);
charInfo.sLocLastName = pRes->ReadFieldCExoLocString(&topLevelStruct, "LastName", bSuccess);
charInfo.resFileName = sBicResRef;
charInfo.nType = MessageLoginMinor::ServerCharacter;
charInfo.nPortraitId = pRes->ReadFieldWORD(&topLevelStruct, "PortraitId", bSuccess, 0xFFFF);
charInfo.resPortrait = pRes->ReadFieldCResRef(&topLevelStruct, "Portrait", bSuccess);
charInfo.fLastFileModified = GetBICFileTime(sRootServerVault, charInfo.resFileName);

CResList classList;
if (pRes->GetList(&classList, &topLevelStruct, "ClassList"))
{
uint32_t nClassCount = pRes->GetListCount(&classList);
for (uint32_t i = 0; i < nClassCount; i++)
{
CResStruct classStruct;
if (pRes->GetListElement(&classStruct, &classList, i))
{
NWPlayerCharacterListClass_st classInfo;
classInfo.nClass = pRes->ReadFieldINT(&classStruct, "Class", bSuccess, -1);
classInfo.nClassLevel = (uint8_t)pRes->ReadFieldSHORT(&classStruct, "ClassLevel", bSuccess);
charInfo.lstClasses.Add(classInfo);
}
}
}

lstChars.push_back(charInfo);
}
else
{
LOG_ERROR("Corrupt BIC file in base servervault: '%s.bic', character skipped.", sBicResRef.CStr());
}
}

delete pRes;
}
}
}

std::sort(lstChars.begin(), lstChars.end(),
[](const CharacterInfoWithFiletime& a, const CharacterInfoWithFiletime& b)
{
return a.fLastFileModified > b.fLastFileModified;
});

for (size_t i=0; i < lstChars.size(); i++)
{
SetCharacterListIndex(&lstChars[i], i);
}

pMessage->WriteWORD(lstChars.size());
for (size_t i = 0; i < lstChars.size(); i++)
{
auto& charInfo = lstChars[i];
pMessage->WriteCExoLocStringServer(charInfo.sLocFirstName);
pMessage->WriteCExoLocStringServer(charInfo.sLocLastName);
pMessage->WriteCResRef(charInfo.resFileName);
pMessage->WriteBYTE(charInfo.nType);
pMessage->WriteWORD(charInfo.nPortraitId);
pMessage->WriteCResRef(charInfo.resPortrait);

uint32_t nClassCount = charInfo.lstClasses.num;
pMessage->WriteBYTE((uint8_t)nClassCount);
for (uint32_t j = 0; j < nClassCount; j++)
{
pMessage->WriteINT(charInfo.lstClasses[j].nClass);
pMessage->WriteBYTE(charInfo.lstClasses[j].nClassLevel);
}
}
}

uint8_t* pMessageData;
uint32_t nMessageSize;
if (pMessage->GetWriteMessage(&pMessageData, &nMessageSize))
return pMessage->SendServerToPlayerMessage(pPlayer->m_nPlayerID, MessageMajor::CharList, MessageCharListMinor::ListResponse, pMessageData, nMessageSize);

return false;
}