From 8c9f34283f8670dda98959d0785d682e6f652a93 Mon Sep 17 00:00:00 2001 From: Tom Barham Date: Tue, 22 Feb 2022 08:33:53 +1000 Subject: Advanced chat: custom messages and client hooks (#74) Co-authored-by: Emma Miler <27428383+emma-miler@users.noreply.github.com> --- .../NorthstarDedicatedTest.vcxproj | 6 + .../NorthstarDedicatedTest.vcxproj.filters | 18 + NorthstarDedicatedTest/chatcommand.cpp | 12 +- NorthstarDedicatedTest/chatcommand.h | 2 +- NorthstarDedicatedTest/clientchathooks.cpp | 86 ++++ NorthstarDedicatedTest/clientchathooks.h | 5 + NorthstarDedicatedTest/dllmain.cpp | 9 +- NorthstarDedicatedTest/localchatwriter.cpp | 460 +++++++++++++++++++++ NorthstarDedicatedTest/localchatwriter.h | 50 +++ NorthstarDedicatedTest/serverauthentication.cpp | 127 +----- NorthstarDedicatedTest/serverauthentication.h | 2 +- NorthstarDedicatedTest/serverchathooks.cpp | 195 +++++++++ NorthstarDedicatedTest/serverchathooks.h | 29 ++ NorthstarDedicatedTest/squirrel.cpp | 7 + NorthstarDedicatedTest/squirrel.h | 64 +++ 15 files changed, 956 insertions(+), 116 deletions(-) create mode 100644 NorthstarDedicatedTest/clientchathooks.cpp create mode 100644 NorthstarDedicatedTest/clientchathooks.h create mode 100644 NorthstarDedicatedTest/localchatwriter.cpp create mode 100644 NorthstarDedicatedTest/localchatwriter.h create mode 100644 NorthstarDedicatedTest/serverchathooks.cpp create mode 100644 NorthstarDedicatedTest/serverchathooks.h diff --git a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj index 9fa7e9df..5235dfc9 100644 --- a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj +++ b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj @@ -112,6 +112,9 @@ + + + @@ -556,6 +559,7 @@ + @@ -569,6 +573,7 @@ + @@ -594,6 +599,7 @@ + diff --git a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters index 67679462..11205541 100644 --- a/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters +++ b/NorthstarDedicatedTest/NorthstarDedicatedTest.vcxproj.filters @@ -1449,6 +1449,15 @@ Header Files\Client + + Header Files\Server + + + Header Files\Client + + + Header Files\Client + @@ -1583,6 +1592,15 @@ Source Files\Client + + Source Files\Server + + + Source Files\Client + + + Source Files\Client + diff --git a/NorthstarDedicatedTest/chatcommand.cpp b/NorthstarDedicatedTest/chatcommand.cpp index 0540a9a0..cd6a998c 100644 --- a/NorthstarDedicatedTest/chatcommand.cpp +++ b/NorthstarDedicatedTest/chatcommand.cpp @@ -2,6 +2,7 @@ #include "chatcommand.h" #include "concommand.h" #include "dedicated.h" +#include "localchatwriter.h" // note: isIngameChat is an int64 because the whole register the arg is stored in needs to be 0'd out to work // if isIngameChat is false, we use network chat instead @@ -20,6 +21,14 @@ void ConCommand_say_team(const CCommand& args) ClientSayText(nullptr, args.ArgS(), true, true); } +void ConCommand_log(const CCommand& args) +{ + if (args.ArgC() >= 2) + { + LocalChatWriter(LocalChatWriter::GameContext).WriteLine(args.ArgS()); + } +} + void InitialiseChatCommands(HMODULE baseAddress) { if (IsDedicated()) @@ -28,4 +37,5 @@ void InitialiseChatCommands(HMODULE baseAddress) ClientSayText = (ClientSayTextType)((char*)baseAddress + 0x54780); RegisterConCommand("say", ConCommand_say, "Enters a message in public chat", FCVAR_CLIENTDLL); RegisterConCommand("say_team", ConCommand_say_team, "Enters a message in team chat", FCVAR_CLIENTDLL); -} \ No newline at end of file + RegisterConCommand("log", ConCommand_log, "Log a message to the local chat window", FCVAR_CLIENTDLL); +} diff --git a/NorthstarDedicatedTest/chatcommand.h b/NorthstarDedicatedTest/chatcommand.h index 1c095fb7..546d0126 100644 --- a/NorthstarDedicatedTest/chatcommand.h +++ b/NorthstarDedicatedTest/chatcommand.h @@ -1,3 +1,3 @@ #pragma once -void InitialiseChatCommands(HMODULE baseAddress); \ No newline at end of file +void InitialiseChatCommands(HMODULE baseAddress); diff --git a/NorthstarDedicatedTest/clientchathooks.cpp b/NorthstarDedicatedTest/clientchathooks.cpp new file mode 100644 index 00000000..81413002 --- /dev/null +++ b/NorthstarDedicatedTest/clientchathooks.cpp @@ -0,0 +1,86 @@ +#include "pch.h" +#include "clientchathooks.h" +#include +#include "squirrel.h" +#include "serverchathooks.h" +#include "localchatwriter.h" + +typedef void(__fastcall* CHudChat__AddGameLineType)(void* self, const char* message, int fromPlayerId, bool isteam, bool isdead); +CHudChat__AddGameLineType CHudChat__AddGameLine; + +struct ChatTags +{ + bool whisper; + bool team; + bool dead; +}; + +static void CHudChat__AddGameLineHook(void* self, const char* message, int inboxId, bool isTeam, bool isDead) +{ + // This hook is called for each HUD, but we only want our logic to run once. + if (!IsFirstHud(self)) + { + return; + } + + int senderId = inboxId & CUSTOM_MESSAGE_INDEX_MASK; + bool isAnonymous = senderId == 0; + bool isCustom = isAnonymous || (inboxId & CUSTOM_MESSAGE_INDEX_BIT); + + // Type is set to 0 for non-custom messages, custom messages have a type encoded as the first byte + int type = 0; + const char* payload = message; + if (isCustom) + { + type = message[0]; + payload = message + 1; + } + + g_ClientSquirrelManager->setupfunc("CHudChat_ProcessMessageStartThread"); + g_ClientSquirrelManager->pusharg((int)senderId - 1); + g_ClientSquirrelManager->pusharg(payload); + g_ClientSquirrelManager->pusharg(isTeam); + g_ClientSquirrelManager->pusharg(isDead); + g_ClientSquirrelManager->pusharg(type); + g_ClientSquirrelManager->call(5); +} + +// void NSChatWrite( int context, string str ) +static SQRESULT SQ_ChatWrite(void* sqvm) +{ + int context = ClientSq_getinteger(sqvm, 1); + const char* str = ClientSq_getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)context).Write(str); + return SQRESULT_NOTNULL; +} + +// void NSChatWriteRaw( int context, string str ) +static SQRESULT SQ_ChatWriteRaw(void* sqvm) +{ + int context = ClientSq_getinteger(sqvm, 1); + const char* str = ClientSq_getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)context).InsertText(str); + return SQRESULT_NOTNULL; +} + +// void NSChatWriteLine( int context, string str ) +static SQRESULT SQ_ChatWriteLine(void* sqvm) +{ + int context = ClientSq_getinteger(sqvm, 1); + const char* str = ClientSq_getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)context).WriteLine(str); + return SQRESULT_NOTNULL; +} + +void InitialiseClientChatHooks(HMODULE baseAddress) +{ + HookEnabler hook; + ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x22E580, &CHudChat__AddGameLineHook, reinterpret_cast(&CHudChat__AddGameLine)); + + g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWrite", "int context, string text", "", SQ_ChatWrite); + g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWriteRaw", "int context, string text", "", SQ_ChatWriteRaw); + g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWriteLine", "int context, string text", "", SQ_ChatWriteLine); +} diff --git a/NorthstarDedicatedTest/clientchathooks.h b/NorthstarDedicatedTest/clientchathooks.h new file mode 100644 index 00000000..79a1b3e2 --- /dev/null +++ b/NorthstarDedicatedTest/clientchathooks.h @@ -0,0 +1,5 @@ +#pragma once +#include "pch.h" +#include "serverchathooks.h" + +void InitialiseClientChatHooks(HMODULE baseAddress); diff --git a/NorthstarDedicatedTest/dllmain.cpp b/NorthstarDedicatedTest/dllmain.cpp index 4cc0a84d..dcff9f4a 100644 --- a/NorthstarDedicatedTest/dllmain.cpp +++ b/NorthstarDedicatedTest/dllmain.cpp @@ -34,6 +34,9 @@ #include "audio.h" #include "buildainfile.h" #include "configurables.h" +#include "serverchathooks.h" +#include "clientchathooks.h" +#include "localchatwriter.h" #include #include "pch.h" @@ -120,13 +123,14 @@ bool InitialiseNorthstar() AddDllLoadCallback("client.dll", InitialiseScriptMainMenuPromos); AddDllLoadCallback("client.dll", InitialiseMiscClientFixes); AddDllLoadCallback("client.dll", InitialiseClientPrintHooks); + AddDllLoadCallback("client.dll", InitialiseClientChatHooks); + AddDllLoadCallback("client.dll", InitialiseLocalChatWriter); } AddDllLoadCallback("engine.dll", InitialiseEngineSpewFuncHooks); AddDllLoadCallback("server.dll", InitialiseServerSquirrel); AddDllLoadCallback("engine.dll", InitialiseBanSystem); AddDllLoadCallback("engine.dll", InitialiseServerAuthentication); - AddDllLoadCallback("server.dll", InitialiseServerAuthenticationServerDLL); AddDllLoadCallback("engine.dll", InitialiseSharedMasterServer); AddDllLoadCallback("server.dll", InitialiseMiscServerScriptCommand); AddDllLoadCallback("server.dll", InitialiseMiscServerFixes); @@ -138,6 +142,9 @@ bool InitialiseNorthstar() AddDllLoadCallback("engine.dll", InitialiseEngineRpakFilesystem); AddDllLoadCallback("engine.dll", InitialiseKeyValues); + AddDllLoadCallback("engine.dll", InitialiseServerChatHooks_Engine); + AddDllLoadCallback("server.dll", InitialiseServerChatHooks_Server); + // maxplayers increase AddDllLoadCallback("engine.dll", InitialiseMaxPlayersOverride_Engine); AddDllLoadCallback("client.dll", InitialiseMaxPlayersOverride_Client); diff --git a/NorthstarDedicatedTest/localchatwriter.cpp b/NorthstarDedicatedTest/localchatwriter.cpp new file mode 100644 index 00000000..fada3c33 --- /dev/null +++ b/NorthstarDedicatedTest/localchatwriter.cpp @@ -0,0 +1,460 @@ +#include "pch.h" +#include "localchatwriter.h" + +class vgui_BaseRichText_vtable; + +class vgui_BaseRichText +{ + public: + vgui_BaseRichText_vtable* vtable; +}; + +class vgui_BaseRichText_vtable +{ + public: + char unknown1[1880]; + + void(__fastcall* InsertChar)(vgui_BaseRichText* self, wchar_t ch); + + // yes these are swapped from the Source 2013 code, who knows why + void(__fastcall* InsertStringWide)(vgui_BaseRichText* self, const wchar_t* wszText); + void(__fastcall* InsertStringAnsi)(vgui_BaseRichText* self, const char* text); + + void(__fastcall* SelectNone)(vgui_BaseRichText* self); + void(__fastcall* SelectAllText)(vgui_BaseRichText* self); + void(__fastcall* SelectNoText)(vgui_BaseRichText* self); + void(__fastcall* CutSelected)(vgui_BaseRichText* self); + void(__fastcall* CopySelected)(vgui_BaseRichText* self); + void(__fastcall* SetPanelInteractive)(vgui_BaseRichText* self, bool bInteractive); + void(__fastcall* SetUnusedScrollbarInvisible)(vgui_BaseRichText* self, bool bInvis); + + void* unknown2; + + void(__fastcall* GotoTextStart)(vgui_BaseRichText* self); + void(__fastcall* GotoTextEnd)(vgui_BaseRichText* self); + + void* unknown3[3]; + + void(__fastcall* SetVerticalScrollbar)(vgui_BaseRichText* self, bool state); + void(__fastcall* SetMaximumCharCount)(vgui_BaseRichText* self, int maxChars); + void(__fastcall* InsertColorChange)(vgui_BaseRichText* self, vgui_Color col); + void(__fastcall* InsertIndentChange)(vgui_BaseRichText* self, int pixelsIndent); + void(__fastcall* InsertClickableTextStart)(vgui_BaseRichText* self, const char* pchClickAction); + void(__fastcall* InsertClickableTextEnd)(vgui_BaseRichText* self); + void(__fastcall* InsertPossibleURLString)( + vgui_BaseRichText* self, const char* text, vgui_Color URLTextColor, vgui_Color normalTextColor); + void(__fastcall* InsertFade)(vgui_BaseRichText* self, float flSustain, float flLength); + void(__fastcall* ResetAllFades)(vgui_BaseRichText* self, bool bHold, bool bOnlyExpired, float flNewSustain); + void(__fastcall* SetToFullHeight)(vgui_BaseRichText* self); + int(__fastcall* GetNumLines)(vgui_BaseRichText* self); +}; + +class CGameSettings +{ + public: + char unknown1[92]; + int isChatEnabled; +}; + +// Not sure what this actually refers to but chatFadeLength and chatFadeSustain +// have their value at the same offset +class CGameFloatVar +{ + public: + char unknown1[88]; + float value; +}; + +class CHudChat +{ + public: + char unknown1[720]; + + vgui_Color m_sameTeamColor; + vgui_Color m_enemyTeamColor; + vgui_Color m_mainTextColor; + vgui_Color m_networkNameColor; + + char unknown2[12]; + + int m_unknownContext; + + char unknown3[8]; + + vgui_BaseRichText* m_richText; + + CHudChat* next; + CHudChat* previous; +}; + +CGameSettings** gGameSettings; +CGameFloatVar** gChatFadeLength; +CGameFloatVar** gChatFadeSustain; + +// Linked list of CHudChats +CHudChat** gHudChatList; + +typedef void(__fastcall* ConvertANSIToUnicodeType)(LPCSTR ansi, int ansiCharLength, LPWSTR unicode, int unicodeCharLength); +ConvertANSIToUnicodeType ConvertANSIToUnicode; + +LocalChatWriter::SwatchColor swatchColors[4] = { + LocalChatWriter::MainTextColor, + LocalChatWriter::SameTeamNameColor, + LocalChatWriter::EnemyTeamNameColor, + LocalChatWriter::NetworkNameColor, +}; + +vgui_Color darkColors[8] = {vgui_Color{0, 0, 0, 255}, vgui_Color{205, 49, 49, 255}, vgui_Color{13, 188, 121, 255}, + vgui_Color{229, 229, 16, 255}, vgui_Color{36, 114, 200, 255}, vgui_Color{188, 63, 188, 255}, + vgui_Color{17, 168, 205, 255}, vgui_Color{229, 229, 229, 255}}; + +vgui_Color lightColors[8] = {vgui_Color{102, 102, 102, 255}, vgui_Color{241, 76, 76, 255}, vgui_Color{35, 209, 139, 255}, + vgui_Color{245, 245, 67, 255}, vgui_Color{59, 142, 234, 255}, vgui_Color{214, 112, 214, 255}, + vgui_Color{41, 184, 219, 255}, vgui_Color{255, 255, 255, 255}}; + +class AnsiEscapeParser +{ + public: + explicit AnsiEscapeParser(LocalChatWriter* writer) : m_writer(writer) {} + + void HandleVal(unsigned long val) + { + switch (m_next) + { + case Next::ControlType: + m_next = HandleControlType(val); + break; + case Next::ForegroundType: + m_next = HandleForegroundType(val); + break; + case Next::Foreground8Bit: + m_next = HandleForeground8Bit(val); + break; + case Next::ForegroundR: + m_next = HandleForegroundR(val); + break; + case Next::ForegroundG: + m_next = HandleForegroundG(val); + break; + case Next::ForegroundB: + m_next = HandleForegroundB(val); + break; + } + } + + private: + enum class Next + { + ControlType, + ForegroundType, + Foreground8Bit, + ForegroundR, + ForegroundG, + ForegroundB + }; + + LocalChatWriter* m_writer; + Next m_next = Next::ControlType; + vgui_Color m_expandedColor{0, 0, 0, 0}; + + Next HandleControlType(unsigned long val) + { + // Reset + if (val == 0 || val == 39) + { + m_writer->InsertSwatchColorChange(LocalChatWriter::MainTextColor); + return Next::ControlType; + } + + // Dark foreground color + if (val >= 30 && val < 38) + { + m_writer->InsertColorChange(darkColors[val - 30]); + return Next::ControlType; + } + + // Light foreground color + if (val >= 90 && val < 98) + { + m_writer->InsertColorChange(lightColors[val - 90]); + return Next::ControlType; + } + + // Game swatch color + if (val >= 110 && val < 114) + { + m_writer->InsertSwatchColorChange(swatchColors[val - 110]); + return Next::ControlType; + } + + // Expanded foreground color + if (val == 38) + { + return Next::ForegroundType; + } + + return Next::ControlType; + } + + Next HandleForegroundType(unsigned long val) + { + // Next values are r,g,b + if (val == 2) + { + m_expandedColor = {0, 0, 0, 255}; + return Next::ForegroundR; + } + // Next value is 8-bit swatch color + if (val == 5) + { + return Next::Foreground8Bit; + } + + // Invalid + return Next::ControlType; + } + + Next HandleForeground8Bit(unsigned long val) + { + if (val < 8) + { + m_writer->InsertColorChange(darkColors[val]); + } + else if (val < 16) + { + m_writer->InsertColorChange(lightColors[val - 8]); + } + else if (val < 232) + { + unsigned char code = val - 16; + unsigned char blue = code % 6; + unsigned char green = ((code - blue) / 6) % 6; + unsigned char red = (code - blue - (green * 6)) / 36; + m_writer->InsertColorChange( + vgui_Color{(unsigned char)(red * 51), (unsigned char)(green * 51), (unsigned char)(blue * 51), 255}); + } + else if (val < UCHAR_MAX) + { + unsigned char brightness = (val - 232) * 10 + 8; + m_writer->InsertColorChange(vgui_Color{brightness, brightness, brightness, 255}); + } + + return Next::ControlType; + } + + Next HandleForegroundR(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor.r = (unsigned char)val; + return Next::ForegroundG; + } + + Next HandleForegroundG(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor.g = (unsigned char)val; + return Next::ForegroundB; + } + + Next HandleForegroundB(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor.b = (unsigned char)val; + m_writer->InsertColorChange(m_expandedColor); + return Next::ControlType; + } +}; + +LocalChatWriter::LocalChatWriter(Context context) : m_context(context) {} + +void LocalChatWriter::Write(const char* str) +{ + char writeBuffer[256]; + + while (true) + { + const char* startOfEscape = strstr(str, "\033["); + + if (startOfEscape == NULL) + { + // No more escape sequences, write the remaining text and exit + InsertText(str); + break; + } + + if (startOfEscape != str) + { + // There is some text before the escape sequence, just print that + + size_t copyChars = startOfEscape - str; + if (copyChars > 255) + copyChars = 255; + strncpy(writeBuffer, str, copyChars); + writeBuffer[copyChars] = 0; + + InsertText(writeBuffer); + } + + const char* escape = startOfEscape + 2; + str = ApplyAnsiEscape(escape); + } +} + +void LocalChatWriter::WriteLine(const char* str) +{ + InsertChar(L'\n'); + InsertSwatchColorChange(MainTextColor); + Write(str); +} + +void LocalChatWriter::InsertChar(wchar_t ch) +{ + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertChar(hud->m_richText, ch); + } + + if (ch != L'\n') + { + InsertDefaultFade(); + } +} + +void LocalChatWriter::InsertText(const char* str) +{ + WCHAR messageUnicode[288]; + ConvertANSIToUnicode(str, -1, messageUnicode, 274); + + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, messageUnicode); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertText(const wchar_t* str) +{ + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, str); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertColorChange(vgui_Color color) +{ + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertColorChange(hud->m_richText, color); + } +} + +static vgui_Color GetHudSwatchColor(CHudChat* hud, LocalChatWriter::SwatchColor swatchColor) +{ + switch (swatchColor) + { + case LocalChatWriter::MainTextColor: + return hud->m_mainTextColor; + case LocalChatWriter::SameTeamNameColor: + return hud->m_sameTeamColor; + case LocalChatWriter::EnemyTeamNameColor: + return hud->m_enemyTeamColor; + case LocalChatWriter::NetworkNameColor: + return hud->m_networkNameColor; + } + return vgui_Color{0, 0, 0, 0}; +} + +void LocalChatWriter::InsertSwatchColorChange(SwatchColor swatchColor) +{ + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertColorChange(hud->m_richText, GetHudSwatchColor(hud, swatchColor)); + } +} + +const char* LocalChatWriter::ApplyAnsiEscape(const char* escape) +{ + AnsiEscapeParser decoder(this); + while (true) + { + char* afterControlType = NULL; + unsigned long controlType = strtoul(escape, &afterControlType, 10); + + // Malformed cases: + // afterControlType = NULL: strtoul errored + // controlType = 0 and escape doesn't actually start with 0: wasn't a number + if (afterControlType == NULL || (controlType == 0 && escape[0] != '0')) + { + return escape; + } + + decoder.HandleVal(controlType); + + // m indicates the end of the sequence + if (afterControlType[0] == 'm') + { + return afterControlType + 1; + } + + // : or ; indicates more values remain, anything else is malformed + if (afterControlType[0] != ':' && afterControlType[0] != ';') + { + return afterControlType; + } + + escape = afterControlType + 1; + } +} + +void LocalChatWriter::InsertDefaultFade() +{ + float fadeLength = 0.f; + float fadeSustain = 0.f; + if ((*gGameSettings)->isChatEnabled) + { + fadeLength = (*gChatFadeLength)->value; + fadeSustain = (*gChatFadeSustain)->value; + } + + for (CHudChat* hud = *gHudChatList; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertFade(hud->m_richText, fadeSustain, fadeLength); + } +} + +bool IsFirstHud(void* hud) { return hud == *gHudChatList; } + +void InitialiseLocalChatWriter(HMODULE baseAddress) +{ + gGameSettings = (CGameSettings**)((char*)baseAddress + 0x11BAA48); + gChatFadeLength = (CGameFloatVar**)((char*)baseAddress + 0x11BAB78); + gChatFadeSustain = (CGameFloatVar**)((char*)baseAddress + 0x11BAC08); + gHudChatList = (CHudChat**)((char*)baseAddress + 0x11BA9E8); + + ConvertANSIToUnicode = (ConvertANSIToUnicodeType)((char*)baseAddress + 0x7339A0); +} diff --git a/NorthstarDedicatedTest/localchatwriter.h b/NorthstarDedicatedTest/localchatwriter.h new file mode 100644 index 00000000..b9ca3220 --- /dev/null +++ b/NorthstarDedicatedTest/localchatwriter.h @@ -0,0 +1,50 @@ +#pragma once +#include "pch.h" + +struct vgui_Color +{ + unsigned char r; + unsigned char g; + unsigned char b; + unsigned char a; +}; + +class LocalChatWriter +{ + public: + enum Context + { + NetworkContext = 0, + GameContext = 1 + }; + enum SwatchColor + { + MainTextColor, + SameTeamNameColor, + EnemyTeamNameColor, + NetworkNameColor + }; + + explicit LocalChatWriter(Context context); + + // Custom chat writing with ANSI escape codes + void Write(const char* str); + void WriteLine(const char* str); + + // Low-level RichText access + void InsertChar(wchar_t ch); + void InsertText(const char* str); + void InsertText(const wchar_t* str); + void InsertColorChange(vgui_Color color); + void InsertSwatchColorChange(SwatchColor color); + + private: + Context m_context; + + const char* ApplyAnsiEscape(const char* escape); + void InsertDefaultFade(); +}; + +bool IsFirstHud(void* hud); + +void InitialiseLocalChatWriter(HMODULE baseAddress); diff --git a/NorthstarDedicatedTest/serverauthentication.cpp b/NorthstarDedicatedTest/serverauthentication.cpp index 4618955d..57646218 100644 --- a/NorthstarDedicatedTest/serverauthentication.cpp +++ b/NorthstarDedicatedTest/serverauthentication.cpp @@ -13,7 +13,6 @@ #include #include #include "configurables.h" -#include "squirrel.h" const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!"; @@ -263,6 +262,21 @@ void ServerAuthenticationManager::WritePersistentData(void* player) } } +bool ServerAuthenticationManager::CheckPlayerChatRatelimit(void* player) +{ + if (Plat_FloatTime() - m_additionalPlayerData[player].lastSayTextLimitStart >= 1.0) + { + m_additionalPlayerData[player].lastSayTextLimitStart = Plat_FloatTime(); + m_additionalPlayerData[player].sayTextLimitCount = 0; + } + + if (m_additionalPlayerData[player].sayTextLimitCount >= Cvar_sv_max_chat_messages_per_sec->m_nValue) + return false; + + m_additionalPlayerData[player].sayTextLimitCount++; + return true; +} + // auth hooks // store these in vars so we can use them in CBaseClient::Connect @@ -517,69 +531,6 @@ bool ProcessConnectionlessPacketHook(void* a1, netpacket_t* packet) return ProcessConnectionlessPacket(a1, packet); } -void ReplaceStringInPlace(std::string& subject, const std::string& search, const std::string& replace) -{ - size_t pos = 0; - while ((pos = subject.find(search, pos)) != std::string::npos) - { - subject.replace(pos, search.length(), replace); - pos += replace.length(); - } -} - -std::string currentMessage; -int currentPlayerId; -int currentChannelId; -bool shouldBlock; -bool isProcessed = true; - -SQRESULT setMessage(void* sqvm) -{ - currentMessage = ServerSq_getstring(sqvm, 1); - currentPlayerId = ServerSq_getinteger(sqvm, 2); - currentChannelId = ServerSq_getinteger(sqvm, 3); - shouldBlock = ServerSq_getbool(sqvm, 4); - return SQRESULT_NOTNULL; -} - -void CServerGameDLL__OnReceivedSayTextMessageHook(void* self, unsigned int senderClientIndex, const char* message, int channelId) -{ - void* sender = GetPlayerByIndex(senderClientIndex - 1); // senderClientIndex starts at 1 - - // check chat ratelimits - if (Plat_FloatTime() - g_ServerAuthenticationManager->m_additionalPlayerData[sender].lastSayTextLimitStart >= 1.0) - { - g_ServerAuthenticationManager->m_additionalPlayerData[sender].lastSayTextLimitStart = Plat_FloatTime(); - g_ServerAuthenticationManager->m_additionalPlayerData[sender].sayTextLimitCount = 0; - } - - if (g_ServerAuthenticationManager->m_additionalPlayerData[sender].sayTextLimitCount >= Cvar_sv_max_chat_messages_per_sec->m_nValue) - return; - - g_ServerAuthenticationManager->m_additionalPlayerData[sender].sayTextLimitCount++; - - bool shouldDoChathooks = strstr(GetCommandLineA(), "-enablechathooks"); - if (shouldDoChathooks) - { - - currentMessage = message; - currentPlayerId = senderClientIndex - 1; // Stupid fix cause of index offsets - currentChannelId = channelId; - shouldBlock = false; - isProcessed = false; - - g_ServerSquirrelManager->ExecuteCode("CServerGameDLL_ProcessMessageStartThread()"); - if (!shouldBlock && currentPlayerId + 1 == senderClientIndex) // stop player id spoofing from server - { - CServerGameDLL__OnReceivedSayTextMessage(self, currentPlayerId + 1, currentMessage.c_str(), currentChannelId); - } - } - else - { - CServerGameDLL__OnReceivedSayTextMessage(self, senderClientIndex, message, channelId); - } -} - void ResetPdataCommand(const CCommand& args) { if (*sv_m_State == server_state_t::ss_active) @@ -683,51 +634,3 @@ void InitialiseServerAuthentication(HMODULE baseAddress) *((char*)ptr + 14) = (char)0x90; } } - -SQRESULT getMessageServer(void* sqvm) -{ - ServerSq_pushstring(sqvm, currentMessage.c_str(), -1); - return SQRESULT_NOTNULL; -} -SQRESULT getPlayerServer(void* sqvm) -{ - ServerSq_pushinteger(sqvm, currentPlayerId); - return SQRESULT_NOTNULL; -} -SQRESULT getChannelServer(void* sqvm) -{ - ServerSq_pushinteger(sqvm, currentChannelId); - return SQRESULT_NOTNULL; -} -SQRESULT getShouldProcessMessage(void* sqvm) -{ - ServerSq_pushbool(sqvm, !isProcessed); - return SQRESULT_NOTNULL; -} -SQRESULT pushMessage(void* sqvm) -{ - currentMessage = ServerSq_getstring(sqvm, 1); - currentPlayerId = ServerSq_getinteger(sqvm, 2); - currentChannelId = ServerSq_getinteger(sqvm, 3); - shouldBlock = ServerSq_getbool(sqvm, 4); - isProcessed = true; - return SQRESULT_NOTNULL; -} - -void InitialiseServerAuthenticationServerDLL(HMODULE baseAddress) -{ - HookEnabler hook; - ENABLER_CREATEHOOK( - hook, (char*)baseAddress + 0x1595C0, &CServerGameDLL__OnReceivedSayTextMessageHook, - reinterpret_cast(&CServerGameDLL__OnReceivedSayTextMessage)); - - g_ServerSquirrelManager->AddFuncRegistration( - "void", "NSSetMessage", "string message, int playerId, int channelId, bool shouldBlock", "", setMessage); - - g_ServerSquirrelManager->AddFuncRegistration("string", "NSChatGetCurrentMessage", "", "", getMessageServer); - g_ServerSquirrelManager->AddFuncRegistration("int", "NSChatGetCurrentPlayer", "", "", getPlayerServer); - g_ServerSquirrelManager->AddFuncRegistration("int", "NSChatGetCurrentChannel", "", "", getChannelServer); - g_ServerSquirrelManager->AddFuncRegistration("bool", "NSShouldProcessMessage", "", "", getShouldProcessMessage); - g_ServerSquirrelManager->AddFuncRegistration( - "void", "NSPushMessage", "string message, int playerId, int channelId, bool shouldBlock", "", pushMessage); -} diff --git a/NorthstarDedicatedTest/serverauthentication.h b/NorthstarDedicatedTest/serverauthentication.h index faa7ae96..8ff5099e 100644 --- a/NorthstarDedicatedTest/serverauthentication.h +++ b/NorthstarDedicatedTest/serverauthentication.h @@ -96,13 +96,13 @@ class ServerAuthenticationManager bool AuthenticatePlayer(void* player, int64_t uid, char* authToken); bool RemovePlayerAuthData(void* player); void WritePersistentData(void* player); + bool CheckPlayerChatRatelimit(void* player); }; typedef void (*CBaseClient__DisconnectType)(void* self, uint32_t unknownButAlways1, const char* reason, ...); extern CBaseClient__DisconnectType CBaseClient__Disconnect; void InitialiseServerAuthentication(HMODULE baseAddress); -void InitialiseServerAuthenticationServerDLL(HMODULE baseAddress); extern ServerAuthenticationManager* g_ServerAuthenticationManager; extern ConVar* Cvar_ns_player_auth_port; \ No newline at end of file diff --git a/NorthstarDedicatedTest/serverchathooks.cpp b/NorthstarDedicatedTest/serverchathooks.cpp new file mode 100644 index 00000000..93f7f383 --- /dev/null +++ b/NorthstarDedicatedTest/serverchathooks.cpp @@ -0,0 +1,195 @@ +#include "pch.h" +#include "serverchathooks.h" +#include +#include +#include +#include "serverauthentication.h" +#include "squirrel.h" +#include "miscserverscript.h" + +class CServerGameDLL; +class CBasePlayer; + +class CRecipientFilter +{ + char unknown[58]; +}; + +CServerGameDLL* gServer; + +typedef void(__fastcall* CServerGameDLL__OnReceivedSayTextMessageType)( + CServerGameDLL* self, unsigned int senderPlayerId, const char* text, int channelId); +CServerGameDLL__OnReceivedSayTextMessageType CServerGameDLL__OnReceivedSayTextMessage; +CServerGameDLL__OnReceivedSayTextMessageType CServerGameDLL__OnReceivedSayTextMessageHookBase; + +typedef CBasePlayer*(__fastcall* UTIL_PlayerByIndexType)(int playerIndex); +UTIL_PlayerByIndexType UTIL_PlayerByIndex; + +typedef void(__fastcall* CRecipientFilter__ConstructType)(CRecipientFilter* self); +CRecipientFilter__ConstructType CRecipientFilter__Construct; + +typedef void(__fastcall* CRecipientFilter__DestructType)(CRecipientFilter* self); +CRecipientFilter__DestructType CRecipientFilter__Destruct; + +typedef void(__fastcall* CRecipientFilter__AddAllPlayersType)(CRecipientFilter* self); +CRecipientFilter__AddAllPlayersType CRecipientFilter__AddAllPlayers; + +typedef void(__fastcall* CRecipientFilter__AddRecipientType)(CRecipientFilter* self, const CBasePlayer* player); +CRecipientFilter__AddRecipientType CRecipientFilter__AddRecipient; + +typedef void(__fastcall* CRecipientFilter__MakeReliableType)(CRecipientFilter* self); +CRecipientFilter__MakeReliableType CRecipientFilter__MakeReliable; + +typedef void(__fastcall* UserMessageBeginType)(CRecipientFilter* filter, const char* messagename); +UserMessageBeginType UserMessageBegin; + +typedef void(__fastcall* MessageEndType)(); +MessageEndType MessageEnd; + +typedef void(__fastcall* MessageWriteByteType)(int iValue); +MessageWriteByteType MessageWriteByte; + +typedef void(__fastcall* MessageWriteStringType)(const char* sz); +MessageWriteStringType MessageWriteString; + +typedef void(__fastcall* MessageWriteBoolType)(bool bValue); +MessageWriteBoolType MessageWriteBool; + +bool isSkippingHook = false; + +static void CServerGameDLL__OnReceivedSayTextMessageHook(CServerGameDLL* self, unsigned int senderPlayerId, const char* text, bool isTeam) +{ + // MiniHook doesn't allow calling the base function outside of anywhere but the hook function. + // To allow bypassing the hook, isSkippingHook can be set. + if (isSkippingHook) + { + isSkippingHook = false; + CServerGameDLL__OnReceivedSayTextMessageHookBase(self, senderPlayerId, text, isTeam); + return; + } + + void* sender = GetPlayerByIndex(senderPlayerId - 1); + + // check chat ratelimits + if (!g_ServerAuthenticationManager->CheckPlayerChatRatelimit(sender)) + { + return; + } + + g_ServerSquirrelManager->setupfunc("CServerGameDLL_ProcessMessageStartThread"); + g_ServerSquirrelManager->pusharg((int)senderPlayerId - 1); + g_ServerSquirrelManager->pusharg(text); + g_ServerSquirrelManager->pusharg(isTeam); + g_ServerSquirrelManager->call(3); +} + +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isteam) +{ + isSkippingHook = true; + CServerGameDLL__OnReceivedSayTextMessage( + gServer, + // Ensure the first bit isn't set, since this indicates a custom message + (playerIndex + 1) & CUSTOM_MESSAGE_INDEX_MASK, text, isteam); +} + +void ChatBroadcastMessage(int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType) +{ + CBasePlayer* toPlayer = NULL; + if (toPlayerIndex >= 0) + { + toPlayer = UTIL_PlayerByIndex(toPlayerIndex + 1); + if (toPlayer == NULL) + return; + } + + // Build a new string where the first byte is the message type + char sendText[256]; + sendText[0] = (char)messageType; + strncpy(sendText + 1, text, 255); + sendText[255] = 0; + + // Anonymous custom messages use playerId=0, non-anonymous ones use a player ID with the first bit set + unsigned int fromPlayerId = fromPlayerIndex < 0 ? 0 : ((fromPlayerIndex + 1) | CUSTOM_MESSAGE_INDEX_BIT); + + CRecipientFilter filter; + CRecipientFilter__Construct(&filter); + if (toPlayer == NULL) + { + CRecipientFilter__AddAllPlayers(&filter); + } + else + { + CRecipientFilter__AddRecipient(&filter, toPlayer); + } + CRecipientFilter__MakeReliable(&filter); + + UserMessageBegin(&filter, "SayText"); + MessageWriteByte(fromPlayerId); + MessageWriteString(sendText); + MessageWriteBool(isTeam); + MessageWriteBool(isDead); + MessageEnd(); + + CRecipientFilter__Destruct(&filter); +} + +SQRESULT SQ_SendMessage(void* sqvm) +{ + int playerIndex = ServerSq_getinteger(sqvm, 1); + const char* text = ServerSq_getstring(sqvm, 2); + bool isTeam = ServerSq_getbool(sqvm, 3); + + ChatSendMessage(playerIndex, text, isTeam); + + return SQRESULT_NULL; +} + +SQRESULT SQ_BroadcastMessage(void* sqvm) +{ + int fromPlayerIndex = ServerSq_getinteger(sqvm, 1); + int toPlayerIndex = ServerSq_getinteger(sqvm, 2); + const char* text = ServerSq_getstring(sqvm, 3); + bool isTeam = ServerSq_getbool(sqvm, 4); + bool isDead = ServerSq_getbool(sqvm, 5); + int messageType = ServerSq_getinteger(sqvm, 6); + + if (messageType < 1) + { + ServerSq_pusherror(sqvm, fmt::format("Invalid message type {}", messageType).c_str()); + return SQRESULT_ERROR; + } + + ChatBroadcastMessage(fromPlayerIndex, toPlayerIndex, text, isTeam, isDead, (CustomMessageType)messageType); + + return SQRESULT_NULL; +} + +void InitialiseServerChatHooks_Engine(HMODULE baseAddress) { gServer = (CServerGameDLL*)((char*)baseAddress + 0x13F0AA98); } + +void InitialiseServerChatHooks_Server(HMODULE baseAddress) +{ + CServerGameDLL__OnReceivedSayTextMessage = (CServerGameDLL__OnReceivedSayTextMessageType)((char*)baseAddress + 0x1595C0); + UTIL_PlayerByIndex = (UTIL_PlayerByIndexType)((char*)baseAddress + 0x26AA10); + CRecipientFilter__Construct = (CRecipientFilter__ConstructType)((char*)baseAddress + 0x1E9440); + CRecipientFilter__Destruct = (CRecipientFilter__DestructType)((char*)baseAddress + 0x1E9700); + CRecipientFilter__AddAllPlayers = (CRecipientFilter__AddAllPlayersType)((char*)baseAddress + 0x1E9940); + CRecipientFilter__AddRecipient = (CRecipientFilter__AddRecipientType)((char*)baseAddress + 0x1E9b30); + CRecipientFilter__MakeReliable = (CRecipientFilter__MakeReliableType)((char*)baseAddress + 0x1EA4E0); + + UserMessageBegin = (UserMessageBeginType)((char*)baseAddress + 0x15C520); + MessageEnd = (MessageEndType)((char*)baseAddress + 0x158880); + MessageWriteByte = (MessageWriteByteType)((char*)baseAddress + 0x158A90); + MessageWriteString = (MessageWriteStringType)((char*)baseAddress + 0x158D00); + MessageWriteBool = (MessageWriteBoolType)((char*)baseAddress + 0x158A00); + + HookEnabler hook; + ENABLER_CREATEHOOK( + hook, CServerGameDLL__OnReceivedSayTextMessage, &CServerGameDLL__OnReceivedSayTextMessageHook, + reinterpret_cast(&CServerGameDLL__OnReceivedSayTextMessageHookBase)); + + // Chat sending functions + g_ServerSquirrelManager->AddFuncRegistration("void", "NSSendMessage", "int playerIndex, string text, bool isTeam", "", SQ_SendMessage); + g_ServerSquirrelManager->AddFuncRegistration( + "void", "NSBroadcastMessage", "int fromPlayerIndex, int toPlayerIndex, string text, bool isTeam, bool isDead, int messageType", "", + SQ_BroadcastMessage); +} diff --git a/NorthstarDedicatedTest/serverchathooks.h b/NorthstarDedicatedTest/serverchathooks.h new file mode 100644 index 00000000..f3425ae6 --- /dev/null +++ b/NorthstarDedicatedTest/serverchathooks.h @@ -0,0 +1,29 @@ +#pragma once +#include "pch.h" +#include +#include + +enum class CustomMessageType : char +{ + Chat = 1, + Whisper = 2 +}; + +constexpr unsigned char CUSTOM_MESSAGE_INDEX_BIT = 0b10000000; +constexpr unsigned char CUSTOM_MESSAGE_INDEX_MASK = ~CUSTOM_MESSAGE_INDEX_BIT; + +// Send a vanilla chat message as if it was from the player. +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isteam); + +// Send a custom message. +// fromPlayerIndex: set to -1 for a [SERVER] message, or another value to send from a specific player +// toPlayerIndex: set to -1 to send to all players, or another value to send to a single player +// isTeam: display a [TEAM] badge +// isDead: display a [DEAD] badge +// messageType: send a specific message type +void ChatBroadcastMessage( + int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType); + +void InitialiseServerChatHooks_Engine(HMODULE baseAddress); + +void InitialiseServerChatHooks_Server(HMODULE baseAddress); diff --git a/NorthstarDedicatedTest/squirrel.cpp b/NorthstarDedicatedTest/squirrel.cpp index 20f99967..19c561c0 100644 --- a/NorthstarDedicatedTest/squirrel.cpp +++ b/NorthstarDedicatedTest/squirrel.cpp @@ -84,6 +84,9 @@ sq_getfloatType ServerSq_getfloat; sq_getboolType ClientSq_getbool; sq_getboolType ServerSq_getbool; +sq_getType ClientSq_sq_get; +sq_getType ServerSq_sq_get; + template void ExecuteCodeCommand(const CCommand& args); // inits @@ -136,6 +139,8 @@ void InitialiseClientSquirrel(HMODULE baseAddress) ClientSq_getfloat = (sq_getfloatType)((char*)baseAddress + 0x6100); ClientSq_getbool = (sq_getboolType)((char*)baseAddress + 0x6130); + ClientSq_sq_get = (sq_getType)((char*)baseAddress + 0x7C30); + ENABLER_CREATEHOOK( hook, (char*)baseAddress + 0x26130, &CreateNewVMHook, reinterpret_cast(&ClientCreateNewVM)); // client createnewvm function @@ -175,6 +180,8 @@ void InitialiseServerSquirrel(HMODULE baseAddress) ServerSq_getfloat = (sq_getfloatType)((char*)baseAddress + 0x60E0); ServerSq_getbool = (sq_getboolType)((char*)baseAddress + 0x6110); + ServerSq_sq_get = (sq_getType)((char*)baseAddress + 0x7C00); + ENABLER_CREATEHOOK( hook, (char*)baseAddress + 0x1FE90, &SQPrintHook, reinterpret_cast(&ServerSQPrint)); // server print function diff --git a/NorthstarDedicatedTest/squirrel.h b/NorthstarDedicatedTest/squirrel.h index 8e851266..b01618f2 100644 --- a/NorthstarDedicatedTest/squirrel.h +++ b/NorthstarDedicatedTest/squirrel.h @@ -124,6 +124,10 @@ typedef SQBool (*sq_getboolType)(void*, SQInteger stackpos); extern sq_getboolType ClientSq_getbool; extern sq_getboolType ServerSq_getbool; +typedef SQRESULT (*sq_getType)(void* sqvm, SQInteger idx); +extern sq_getType ServerSq_sq_get; +extern sq_getType ClientSq_sq_get; + template class SquirrelManager { private: @@ -193,6 +197,66 @@ template class SquirrelManager } } + int setupfunc(const char* funcname) + { + int result = -2; + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + { + ClientSq_pushroottable(sqvm2); + ClientSq_pushstring(sqvm2, funcname, -1); + result = ClientSq_sq_get(sqvm2, -2); + ClientSq_pushroottable(sqvm2); + } + else if (context == ScriptContext::SERVER) + { + ServerSq_pushroottable(sqvm2); + ServerSq_pushstring(sqvm2, funcname, -1); + result = ServerSq_sq_get(sqvm2, -2); + ServerSq_pushroottable(sqvm2); + } + return result; + } + + void pusharg(int arg) + { + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + ClientSq_pushinteger(sqvm2, arg); + else if (context == ScriptContext::SERVER) + ServerSq_pushinteger(sqvm2, arg); + } + void pusharg(const char* arg) + { + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + ClientSq_pushstring(sqvm2, arg, -1); + else if (context == ScriptContext::SERVER) + ServerSq_pushstring(sqvm2, arg, -1); + } + void pusharg(float arg) + { + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + ClientSq_pushfloat(sqvm2, arg); + else if (context == ScriptContext::SERVER) + ServerSq_pushfloat(sqvm2, arg); + } + void pusharg(bool arg) + { + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + ClientSq_pushbool(sqvm2, arg); + else if (context == ScriptContext::SERVER) + ServerSq_pushbool(sqvm2, arg); + } + + int call(int args) + { + int result = -2; + if (context == ScriptContext::CLIENT || context == ScriptContext::UI) + result = ClientSq_call(sqvm2, args + 1, false, false); + else if (context == ScriptContext::SERVER) + result = ServerSq_call(sqvm2, args + 1, false, false); + + return result; + } + void AddFuncRegistration(std::string returnType, std::string name, std::string argTypes, std::string helpText, SQFunction func) { SQFuncRegistration* reg = new SQFuncRegistration; -- cgit v1.2.3