diff --git a/src/Components/Loader.cpp b/src/Components/Loader.cpp index 3b2ae383..7b76cb02 100644 --- a/src/Components/Loader.cpp +++ b/src/Components/Loader.cpp @@ -47,7 +47,6 @@ namespace Components Loader::Register(new Toast()); Loader::Register(new Party()); Loader::Register(new Zones()); - Loader::Register(new Colors()); Loader::Register(new D3D9Ex()); #if (!defined(VLD_RPTHOOK_INSTALL) || defined(VLDEnable)) && defined(COMPILE_IW4MVM) // IW4MVM uses detours which produces memory leaks, but those are not really relevant Loader::Register(new IW4MVM()); @@ -103,6 +102,8 @@ namespace Components Loader::Register(new StartupMessages()); Loader::Register(new SoundMutexFix()); Loader::Register(new Gamepad()); + Loader::Register(new Chat()); + Loader::Register(new TextRenderer()); Loader::Register(new Client()); diff --git a/src/Components/Loader.hpp b/src/Components/Loader.hpp index 92f95774..2265b729 100644 --- a/src/Components/Loader.hpp +++ b/src/Components/Loader.hpp @@ -73,7 +73,6 @@ namespace Components #include "Modules/Menus.hpp" #include "Modules/Toast.hpp" #include "Modules/Zones.hpp" -#include "Modules/Colors.hpp" #include "Modules/D3D9Ex.hpp" #include "Modules/Script.hpp" #include "Modules/Weapon.hpp" @@ -131,6 +130,8 @@ namespace Components #include "Modules/StartupMessages.hpp" #include "Modules/Stats.hpp" #include "Modules/SoundMutexFix.hpp" +#include "Modules/Chat.hpp" +#include "Modules/TextRenderer.hpp" #include "Modules/Gamepad.hpp" #include "Modules/Client.hpp" diff --git a/src/Components/Modules/Chat.cpp b/src/Components/Modules/Chat.cpp new file mode 100644 index 00000000..5c849c3b --- /dev/null +++ b/src/Components/Modules/Chat.cpp @@ -0,0 +1,209 @@ +#include "STDInclude.hpp" + +namespace Components +{ + Game::dvar_t** Chat::cg_chatHeight = reinterpret_cast(0x7ED398); + Dvar::Var Chat::cg_chatWidth; + Game::dvar_t** Chat::cg_chatTime = reinterpret_cast(0x9F5DE8); + + bool Chat::SendChat; + + const char* Chat::EvaluateSay(char* text, Game::gentity_t* player) + { + SendChat = true; + + if (text[1] == '/') + { + SendChat = false; + text[1] = text[0]; + ++text; + } + + TextRenderer::StripMaterialTextIcons(text, text, strlen(text) + 1); + + Game::Scr_AddEntity(player); + Game::Scr_AddString(text + 1); + Game::Scr_NotifyLevel(Game::SL_GetString("say", 0), 2); + + return text; + } + + __declspec(naked) void Chat::PreSayStub() + { + __asm + { + mov eax, [esp + 100h + 10h] + + push eax + pushad + + push[esp + 100h + 28h] + push eax + call Chat::EvaluateSay + add esp, 8h + + mov[esp + 20h], eax + popad + pop eax + + mov[esp + 100h + 10h], eax + + jmp PlayerName::CleanStrStub + } + } + + __declspec(naked) void Chat::PostSayStub() + { + __asm + { + // eax is used by the callee + push eax + + xor eax, eax + mov al, Chat::SendChat + + test al, al + jnz return + + // Don't send the chat + pop eax + retn + + return: + pop eax + + // Jump to the target + push 5DF620h + retn + } + } + + void Chat::CheckChatLineEnd(const char*& inputBuffer, char*& lineBuffer, float& len, const int chatHeight, const float chatWidth, char*& lastSpacePos, char*& lastFontIconPos, const int lastColor) + { + if (len > chatWidth) + { + if (lastSpacePos && lastSpacePos > lastFontIconPos) + { + inputBuffer += lastSpacePos - lineBuffer + 1; + lineBuffer = lastSpacePos; + } + else if (lastFontIconPos) + { + inputBuffer += lastFontIconPos - lineBuffer; + lineBuffer = lastFontIconPos; + } + *lineBuffer = 0; + len = 0.0f; + Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time; + + Game::cgsArray[0].teamChatPos++; + lineBuffer = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight]; + lineBuffer[0] = '^'; + lineBuffer[1] = CharForColorIndex(lastColor); + lineBuffer += 2; + lastSpacePos = nullptr; + lastFontIconPos = nullptr; + } + } + + void Chat::CG_AddToTeamChat(const char* text) + { + // Text can only be 150 characters maximum. This is bigger than the teamChatMsgs buffers with 160 characters + // Therefore it is not needed to check for buffer lengths + + const auto chatHeight = (*cg_chatHeight)->current.integer; + const auto chatWidth = static_cast(cg_chatWidth.get()); + const auto chatTime = (*cg_chatTime)->current.integer; + if (chatHeight < 0 || static_cast(chatHeight) > std::extent_v || chatWidth <= 0 || chatTime <= 0) + { + Game::cgsArray[0].teamLastChatPos = 0; + Game::cgsArray[0].teamChatPos = 0; + return; + } + + TextRenderer::FontIconInfo fontIconInfo{}; + auto len = 0.0f; + auto lastColor = static_cast(TEXT_COLOR_DEFAULT); + char* lastSpace = nullptr; + char* lastFontIcon = nullptr; + char* p = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight]; + p[0] = '\0'; + + while (*text) + { + CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor); + + const char* fontIconEndPos = &text[1]; + if(text[0] == TextRenderer::FONT_ICON_SEPARATOR_CHARACTER && TextRenderer::IsFontIcon(fontIconEndPos, fontIconInfo)) + { + // The game calculates width on a per character base. Since the width of a font icon is calculated based on the height of the font + // which is roughly double as much as the average width of a character without an additional multiplier the calculated len of the font icon + // would be less than it most likely would be rendered. Therefore apply a guessed 2.0f multiplier at this location which makes + // the calculated width of a font icon roughly comparable to the width of an average character of the font. + const auto normalizedFontIconWidth = TextRenderer::GetNormalizedFontIconWidth(fontIconInfo); + const auto fontIconWidth = normalizedFontIconWidth * FONT_ICON_CHAT_WIDTH_CALCULATION_MULTIPLIER; + len += fontIconWidth; + + lastFontIcon = p; + for(; text < fontIconEndPos; text++) + { + p[0] = text[0]; + p++; + } + + CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor); + } + else if (text[0] == '^' && text[1] != 0 && text[1] >= TextRenderer::COLOR_FIRST_CHAR && text[1] <= TextRenderer::COLOR_LAST_CHAR) + { + p[0] = '^'; + p[1] = text[1]; + lastColor = ColorIndexForChar(text[1]); + p += 2; + text += 2; + } + else + { + if (text[0] == ' ') + lastSpace = p; + *p++ = *text++; + len += 1.0f; + } + } + + *p = 0; + + Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time; + + Game::cgsArray[0].teamChatPos++; + if (Game::cgsArray[0].teamChatPos - Game::cgsArray[0].teamLastChatPos > chatHeight) + Game::cgsArray[0].teamLastChatPos = Game::cgsArray[0].teamChatPos + 1 - chatHeight; + } + + __declspec(naked) void Chat::CG_AddToTeamChat_Stub() + { + __asm + { + pushad + + push ecx + call CG_AddToTeamChat + add esp, 4h + + popad + ret + } + } + + Chat::Chat() + { + cg_chatWidth = Dvar::Register("cg_chatWidth", 52, 1, INT_MAX, Game::DVAR_FLAG_SAVED, "The normalized maximum width of a chat message"); + + // Intercept chat sending + Utils::Hook(0x4D000B, PreSayStub, HOOK_CALL).install()->quick(); + Utils::Hook(0x4D00D4, PostSayStub, HOOK_CALL).install()->quick(); + Utils::Hook(0x4D0110, PostSayStub, HOOK_CALL).install()->quick(); + + // Change logic that does word splitting with new lines for chat messages to support fonticons + Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick(); + } +} diff --git a/src/Components/Modules/Chat.hpp b/src/Components/Modules/Chat.hpp new file mode 100644 index 00000000..9cf35e9c --- /dev/null +++ b/src/Components/Modules/Chat.hpp @@ -0,0 +1,27 @@ +#pragma once + +namespace Components +{ + class Chat : public Component + { + static constexpr auto FONT_ICON_CHAT_WIDTH_CALCULATION_MULTIPLIER = 2.0f; + public: + Chat(); + + private: + static Game::dvar_t** cg_chatHeight; + static Dvar::Var cg_chatWidth; + static Game::dvar_t** cg_chatTime; + + static bool SendChat; + + static const char* EvaluateSay(char* text, Game::gentity_t* player); + + static void PreSayStub(); + static void PostSayStub(); + + static void CheckChatLineEnd(const char*& inputBuffer, char*& lineBuffer, float& len, int chatHeight, float chatWidth, char*& lastSpacePos, char*& lastFontIconPos, int lastColor); + static void CG_AddToTeamChat(const char* text); + static void CG_AddToTeamChat_Stub(); + }; +} diff --git a/src/Components/Modules/Colors.cpp b/src/Components/Modules/Colors.cpp deleted file mode 100644 index ef600954..00000000 --- a/src/Components/Modules/Colors.cpp +++ /dev/null @@ -1,337 +0,0 @@ -#include "STDInclude.hpp" - -namespace Components -{ - char Colors::LastColorIndex; - Dvar::Var Colors::NewColors; - Dvar::Var Colors::ColorBlind; - Game::dvar_t* Colors::ColorAllyColorBlind; - Game::dvar_t* Colors::ColorEnemyColorBlind; - - std::vector Colors::ColorTable; - - DWORD Colors::HsvToRgb(Colors::HsvColor hsv) - { - DWORD rgb; - unsigned char region, p, q, t; - unsigned int h, s, v, remainder; - - if (hsv.s == 0) - { - rgb = RGB(hsv.v, hsv.v, hsv.v); - return rgb; - } - - // converting to 16 bit to prevent overflow - h = hsv.h; - s = hsv.s; - v = hsv.v; - - region = static_cast(h / 43); - remainder = (h - (region * 43)) * 6; - - p = static_cast((v * (255 - s)) >> 8); - q = static_cast((v * (255 - ((s * remainder) >> 8))) >> 8); - t = static_cast((v * (255 - ((s * (255 - remainder)) >> 8))) >> 8); - - switch (region) - { - case 0: - rgb = RGB(v, t, p); - break; - case 1: - rgb = RGB(q, v, p); - break; - case 2: - rgb = RGB(p, v, t); - break; - case 3: - rgb = RGB(p, q, v); - break; - case 4: - rgb = RGB(t, p, v); - break; - default: - rgb = RGB(v, p, q); - break; - } - - return rgb; - } - - void Colors::Strip(const char* in, char* out, int max) - { - if (!in || !out) return; - - max--; - int current = 0; - while (*in != 0 && current < max) - { - char index = *(in + 1); - if (*in == '^' && (Colors::ColorIndex(index) != 7 || index == '7')) - { - ++in; - } - else - { - *out = *in; - ++out; - ++current; - } - - ++in; - } - *out = '\0'; - } - - std::string Colors::Strip(const std::string& in) - { - char buffer[1000] = { 0 }; // Should be more than enough - Colors::Strip(in.data(), buffer, sizeof(buffer)); - return std::string(buffer); - } - - void Colors::UserInfoCopy(char* buffer, const char* name, size_t size) - { - Utils::Memory::Allocator allocator; - - if (!Dvar::Var("sv_allowColoredNames").get()) - { - Colors::Strip(name, buffer, size); - } - else - { - strncpy_s(buffer, size, name, _TRUNCATE); - } - } - - __declspec(naked) void Colors::ClientUserinfoChanged() - { - __asm - { - mov eax, [esp + 4h] // length - //sub eax, 1 - push eax - - push ecx // name - push edx // buffer - - call Colors::UserInfoCopy - - add esp, 0Ch - retn - } - } - - char* Colors::GetClientName(int localClientNum, int index, char *buf, size_t size) - { - Game::CL_GetClientName(localClientNum, index, buf, size); - - // Append clantag to username & remove the colors - strncpy_s(buf, size, Colors::Strip(ClanTags::GetUserClantag(index, buf)).data(), size); - - return buf; - } - - void Colors::PatchColorLimit(char limit) - { - Utils::Hook::Set(0x535629, limit); // DrawText2d - Utils::Hook::Set(0x4C1BE4, limit); // No idea :P - Utils::Hook::Set(0x4863DD, limit); // No idea :P - Utils::Hook::Set(0x486429, limit); // No idea :P - Utils::Hook::Set(0x49A5A8, limit); // No idea :P - Utils::Hook::Set(0x505721, limit); // R_TextWidth - Utils::Hook::Set(0x505801, limit); // No idea :P - Utils::Hook::Set(0x50597F, limit); // No idea :P - Utils::Hook::Set(0x5815DB, limit); // No idea :P - Utils::Hook::Set(0x592ED0, limit); // No idea :P - Utils::Hook::Set(0x5A2E2E, limit); // No idea :P - - Utils::Hook::Set(0x5A2733, limit - '0'); // No idea :P - - LastColorIndex = limit; - } - - char Colors::Add(uint8_t r, uint8_t g, uint8_t b) - { - char index = '0' + static_cast(Colors::ColorTable.size()); - Colors::ColorTable.push_back(RGB(r, g, b)); - Colors::PatchColorLimit(index); - return index; - } - - unsigned int Colors::ColorIndex(char index) - { - char result = index - '0'; - if (static_cast(result) >= Colors::ColorTable.size() || result < 0) result = 7; - return result; - } - - void Colors::LookupColor(DWORD* color, char index) - { - *color = RGB(255, 255, 255); - - if (index == '8') // Color 8 - { - *color = *reinterpret_cast(0x66E5F70); - } - else if (index == '9') // Color 9 - { - *color = *reinterpret_cast(0x66E5F74); - } - else if (index == ':') - { - *color = Colors::HsvToRgb({ static_cast((Game::Sys_Milliseconds() / 200) % 256), 255,255 }); - } - else if (index == ';') - { - float fltColor[4]; - Game::Dvar_GetUnpackedColorByName("sv_customTextColor", fltColor); - *color = RGB(fltColor[0] * 255, fltColor[1] * 255, fltColor[2] * 255); - } - else - { - int clrIndex = Colors::ColorIndex(index); - - // Use native colors - if (clrIndex <= 7 && !Colors::NewColors.get()) - { - *color = reinterpret_cast(0x78DC70)[index - 48]; - } - else - { - *color = Colors::ColorTable[clrIndex]; - } - } - } - - char* Colors::CleanStrStub(char* string) - { - Colors::Strip(string, string, strlen(string) + 1); - return string; - } - - __declspec(naked) void Colors::LookupColorStub() - { - __asm - { - pushad - push [esp + 24h] // Index - push esi // Color ref - call Colors::LookupColor - add esp, 8h - popad - retn - } - } - - // Patches team overhead normally - bool Colors::Dvar_GetUnpackedColorByName(const char* name, float* expandedColor) - { - if (Colors::ColorBlind.get()) - { - const auto str = std::string(name); - if (str == "g_TeamColor_EnemyTeam") - { - // Dvar_GetUnpackedColor - auto* colorblindEnemy = Colors::ColorEnemyColorBlind->current.color; - expandedColor[0] = static_cast(colorblindEnemy[0]) / 255.0f; - expandedColor[1] = static_cast(colorblindEnemy[1]) / 255.0f; - expandedColor[2] = static_cast(colorblindEnemy[2]) / 255.0f; - expandedColor[3] = static_cast(colorblindEnemy[3]) / 255.0f; - return false; - } - else if (str == "g_TeamColor_MyTeam") - { - // Dvar_GetUnpackedColor - auto* colorblindAlly = Colors::ColorAllyColorBlind->current.color; - expandedColor[0] = static_cast(colorblindAlly[0]) / 255.0f; - expandedColor[1] = static_cast(colorblindAlly[1]) / 255.0f; - expandedColor[2] = static_cast(colorblindAlly[2]) / 255.0f; - expandedColor[3] = static_cast(colorblindAlly[3]) / 255.0f; - return false; - } - } - - return true; - } - - __declspec(naked) void Colors::GetUnpackedColorByNameStub() - { - __asm - { - push [esp + 8h] - push [esp + 8h] - call Colors::Dvar_GetUnpackedColorByName - add esp, 8h - - test al, al - jnz continue - - retn - - continue: - push edi - mov edi, [esp + 8h] - push 406535h - retn - } - } - - Colors::Colors() - { - // Add a colorblind mode for team colors - Colors::ColorBlind = Dvar::Register("r_colorBlindTeams", false, Game::dvar_flag::DVAR_FLAG_SAVED, "Use color-blindness-friendly colors for ingame team names"); - // A dark red - Colors::ColorEnemyColorBlind = Game::Dvar_RegisterColor("g_ColorBlind_EnemyTeam", 0.659f, 0.088f, 0.145f, 1, Game::dvar_flag::DVAR_FLAG_SAVED, "Enemy team color for colorblind mode"); - // A bright yellow - Colors::ColorAllyColorBlind = Game::Dvar_RegisterColor("g_ColorBlind_MyTeam", 1, 0.859f, 0.125f, 1, Game::dvar_flag::DVAR_FLAG_SAVED, "Ally team color for colorblind mode"); - Utils::Hook(0x406530, Colors::GetUnpackedColorByNameStub, HOOK_JUMP).install()->quick(); - - // Disable SV_UpdateUserinfo_f, to block changing the name ingame - Utils::Hook::Set(0x6258D0, 0xC3); - - // Allow colored names ingame - Utils::Hook(0x5D8B40, Colors::ClientUserinfoChanged, HOOK_JUMP).install()->quick(); - - // Though, don't apply that to overhead names. - Utils::Hook(0x581932, Colors::GetClientName, HOOK_CALL).install()->quick(); - - // Patch RB_LookupColor - Utils::Hook(0x534CD0, Colors::LookupColorStub, HOOK_JUMP).install()->quick(); - - // Patch ColorIndex - Utils::Hook(0x417770, Colors::ColorIndex, HOOK_JUMP).install()->quick(); - - // Patch I_CleanStr - Utils::Hook(0x4AD470, Colors::CleanStrStub, HOOK_JUMP).install()->quick(); - - // Register dvar - Colors::NewColors = Dvar::Register("cg_newColors", true, Game::dvar_flag::DVAR_FLAG_SAVED, "Use Warfare 2 color code style."); - Game::Dvar_RegisterColor("sv_customTextColor", 1, 0.7f, 0, 1, Game::dvar_flag::DVAR_FLAG_REPLICATED, "Color for the extended color code."); - Dvar::Register("sv_allowColoredNames", true, Game::dvar_flag::DVAR_FLAG_NONE, "Allow colored names on the server"); - - // Add our colors - Colors::Add(0, 0, 0); // 0 - Black - Colors::Add(255, 49, 49); // 1 - Red - Colors::Add(134, 192, 0); // 2 - Green - Colors::Add(255, 173, 34); // 3 - Yellow - Colors::Add(0, 135, 193); // 4 - Blue - Colors::Add(32, 197, 255); // 5 - Light Blue - Colors::Add(151, 80, 221); // 6 - Pink - - Colors::Add(255, 255, 255); // 7 - White - - Colors::Add(0, 0, 0); // 8 - Team color (axis?) - Colors::Add(0, 0, 0); // 9 - Team color (allies?) - - // Custom colors - Colors::Add(0, 0, 0); // 10 - Rainbow (:) - Colors::Add(0, 0, 0); // 11 - Server color (;) - using that color in infostrings (e.g. your name) fails, ';' is an illegal character! - } - - Colors::~Colors() - { - Colors::ColorTable.clear(); - } -} diff --git a/src/Components/Modules/Colors.hpp b/src/Components/Modules/Colors.hpp deleted file mode 100644 index bb783151..00000000 --- a/src/Components/Modules/Colors.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -namespace Components -{ - class Colors : public Component - { - public: - static char LastColorIndex; - - Colors(); - ~Colors(); - - static void Strip(const char* in, char* out, int max); - static std::string Strip(const std::string& in); - - static char Add(uint8_t r, uint8_t g, uint8_t b); - - private: - struct HsvColor - { - unsigned char h; - unsigned char s; - unsigned char v; - }; - - static Dvar::Var NewColors; - static Dvar::Var ColorBlind; - static Game::dvar_t* ColorAllyColorBlind; - static Game::dvar_t* ColorEnemyColorBlind; - - static DWORD HsvToRgb(HsvColor hsv); - - static void UserInfoCopy(char* buffer, const char* name, size_t size); - - static void ClientUserinfoChanged(); - static char* GetClientName(int localClientNum, int index, char *buf, size_t size); - static void PatchColorLimit(char limit); - - static unsigned int ColorIndex(char index); - static void LookupColor(DWORD* color, char index); - static void LookupColorStub(); - static char* CleanStrStub(char* string); - static bool Dvar_GetUnpackedColorByName(const char* name, float* expandedColor); - static void GetUnpackedColorByNameStub(); - static std::vector ColorTable; - }; -} diff --git a/src/Components/Modules/Console.cpp b/src/Components/Modules/Console.cpp index 01869043..2bd9f68e 100644 --- a/src/Components/Modules/Console.cpp +++ b/src/Components/Modules/Console.cpp @@ -45,7 +45,7 @@ namespace Components void Console::RefreshStatus() { std::string mapname = Dvar::Var("mapname").get(); - std::string hostname = Colors::Strip(Dvar::Var("sv_hostname").get()); + std::string hostname = TextRenderer::StripColors(Dvar::Var("sv_hostname").get()); if (Console::HasConsole) { diff --git a/src/Components/Modules/Dedicated.cpp b/src/Components/Modules/Dedicated.cpp index 35f36861..7ff6330c 100644 --- a/src/Components/Modules/Dedicated.cpp +++ b/src/Components/Modules/Dedicated.cpp @@ -4,8 +4,6 @@ namespace Components { SteamID Dedicated::PlayerGuids[18][2]; - bool Dedicated::SendChat; - bool Dedicated::IsEnabled() { static std::optional flag; @@ -76,99 +74,6 @@ namespace Components } } - void Dedicated::StripMaterialTextIcons(char* text) - { - char* currentChar = text; - bool isEscaped = false; - while (*currentChar) - { - if (*currentChar == '^') - { - isEscaped = true; - } - else if(isEscaped == true && (*currentChar == '\x01' || *currentChar == '\x02')) - { - *currentChar = ' '; - } - else - { - isEscaped = false; - } - - currentChar++; - } - } - - const char* Dedicated::EvaluateSay(char* text, Game::gentity_t* player) - { - Dedicated::SendChat = true; - - if (text[1] == '/') - { - Dedicated::SendChat = false; - text[1] = text[0]; - ++text; - } - - StripMaterialTextIcons(text); - - Game::Scr_AddEntity(player); - Game::Scr_AddString(text + 1); - Game::Scr_NotifyLevel(Game::SL_GetString("say", 0), 2); - - return text; - } - - __declspec(naked) void Dedicated::PreSayStub() - { - __asm - { - mov eax, [esp + 100h + 10h] - - push eax - pushad - - push [esp + 100h + 28h] - push eax - call Dedicated::EvaluateSay - add esp, 8h - - mov [esp + 20h], eax - popad - pop eax - - mov [esp + 100h + 10h], eax - - jmp Colors::CleanStrStub - } - } - - __declspec(naked) void Dedicated::PostSayStub() - { - __asm - { - // eax is used by the callee - push eax - - xor eax, eax - mov al, Dedicated::SendChat - - test al, al - jnz return - - // Don't send the chat - pop eax - retn - - return: - pop eax - - // Jump to the target - push 5DF620h - retn - } - } - void Dedicated::TransmitGuids() { std::string list = Utils::String::VA("%c", 20); @@ -329,11 +234,6 @@ namespace Components Dvar::Register("sv_dontrotate", false, Game::dvar_flag::DVAR_FLAG_CHEAT, ""); Dvar::Register("com_logFilter", true, Game::dvar_flag::DVAR_FLAG_LATCHED, "Removes ~95% of unneeded lines from the log"); - // Intercept chat sending - Utils::Hook(0x4D000B, Dedicated::PreSayStub, HOOK_CALL).install()->quick(); - Utils::Hook(0x4D00D4, Dedicated::PostSayStub, HOOK_CALL).install()->quick(); - Utils::Hook(0x4D0110, Dedicated::PostSayStub, HOOK_CALL).install()->quick(); - if (Dedicated::IsEnabled() || ZoneBuilder::IsEnabled()) { // Make sure all callbacks are handled diff --git a/src/Components/Modules/Dedicated.hpp b/src/Components/Modules/Dedicated.hpp index 0b362465..162e1a8f 100644 --- a/src/Components/Modules/Dedicated.hpp +++ b/src/Components/Modules/Dedicated.hpp @@ -14,22 +14,13 @@ namespace Components static void Heartbeat(); - static void StripMaterialTextIcons(char* text); - private: - static bool SendChat; - static void MapRotate(); static void InitDedicatedServer(); static void PostInitialization(); static void PostInitializationStub(); - static const char* EvaluateSay(char* text, Game::gentity_t* player); - - static void PreSayStub(); - static void PostSayStub(); - static void FrameStub(); static void TransmitGuids(); diff --git a/src/Components/Modules/Dvar.cpp b/src/Components/Modules/Dvar.cpp index c83a1f7c..d0892d2e 100644 --- a/src/Components/Modules/Dvar.cpp +++ b/src/Components/Modules/Dvar.cpp @@ -203,7 +203,7 @@ namespace Components // Don't perform any checks if name didn't change if (name == lastValidName) return; - std::string saneName = Colors::Strip(Utils::String::Trim(name)); + std::string saneName = TextRenderer::StripAllTextIcons(TextRenderer::StripColors(Utils::String::Trim(name))); if (saneName.size() < 3 || (saneName[0] == '[' && saneName[1] == '{')) { Logger::Print("Username '%s' is invalid. It must at least be 3 characters long and not appear empty!\n", name.data()); diff --git a/src/Components/Modules/Friends.cpp b/src/Components/Modules/Friends.cpp index 2350877b..5228537b 100644 --- a/src/Components/Modules/Friends.cpp +++ b/src/Components/Modules/Friends.cpp @@ -71,7 +71,7 @@ namespace Components entry->name = Steam::Proxy::SteamFriends->GetFriendPersonaName(user); entry->online = Steam::Proxy::SteamFriends->GetFriendPersonaState(user) != 0; - entry->cleanName = Utils::String::ToLower(Colors::Strip(entry->name)); + entry->cleanName = Utils::String::ToLower(TextRenderer::StripColors(entry->name)); std::string guid = Friends::GetPresence(user, "iw4x_guid"); std::string name = Friends::GetPresence(user, "iw4x_name"); diff --git a/src/Components/Modules/Materials.cpp b/src/Components/Modules/Materials.cpp index 173feefd..9fdd7f96 100644 --- a/src/Components/Modules/Materials.cpp +++ b/src/Components/Modules/Materials.cpp @@ -2,7 +2,6 @@ namespace Components { - int Materials::ImageNameLength; Utils::Hook Materials::ImageVersionCheckHook; std::vector Materials::ImageTable; @@ -151,66 +150,6 @@ namespace Components } } - Game::Material* Materials::ResolveMaterial(const char* stringPtr) - { - const char* imagePtr = stringPtr + 4; - unsigned int length = static_cast(stringPtr[3] & 0xFF); - - if (strlen(imagePtr) >= length) - { - Materials::ImageNameLength = 4 + length; - std::string image(imagePtr, length); - - auto* material = Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_MATERIAL, image.data()).material; - - if(material == nullptr || material->techniqueSet == nullptr || material->techniqueSet->name == nullptr || strcmp(material->techniqueSet->name, "2d") != 0) - return Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_MATERIAL, "default").material; - - return material; - } - - Materials::ImageNameLength = 4; - return Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_MATERIAL, "default").material; - } - - __declspec(naked) void Materials::PostDrawMaterialStub() - { - __asm - { - mov eax, Materials::ImageNameLength - add [esp + 30h], eax - - mov eax, 5358FFh - jmp eax - } - } - - __declspec(naked) void Materials::DrawMaterialStub() - { - __asm - { - push eax - pushad - - push ecx - call Materials::ResolveMaterial - add esp, 4h - - mov [esp + 20h], eax - - // Make all material text icons have white tint - mov eax,[esp + 0x50] - or eax,0x00FFFFFF - mov [esp + 0x50],eax - - popad - pop eax - - push 5310F0h - retn - } - } - int Materials::WriteDeathMessageIcon(char* string, int offset, Game::Material* material) { if (!material) @@ -291,77 +230,11 @@ namespace Components #endif - int Materials::R_TextWidth_Hk(const char* text, int maxChars, Game::Font_s* font) - { - auto lineWidth = 0; - auto maxWidth = 0; - - if (maxChars <= 0) - maxChars = 0x7FFFFFFF; - - if (text == nullptr) - return 0; - - auto count = 0; - while (text && *text && count < maxChars) - { - const auto letter = Game::SEH_ReadCharFromString(&text, nullptr); - if (letter == '\r' || letter == '\n') - { - lineWidth = 0; - } - else - { - if (letter == '^' && text) - { - if (*text >= '0' && *text <= Colors::LastColorIndex) - { - text++; - continue; - } - - if (*text >= '\x01' && *text <= '\x02' && text[1] != '\0' && text[2] != '\0' && text[3] != '\0') - { - const auto width = text[1]; - const auto materialNameLength = text[3]; - - // This is how the game calculates width and height. Probably some 1 byte floating point number. - auto v9 = font->pixelHeight * (width - 16) + 16; - auto w = ((((v9 >> 24) & 0x1F) + v9) >> 5); - - lineWidth += w; - - text += 4; - for (auto currentLength = 0; currentLength < materialNameLength && *text; currentLength++) - text++; - continue; - } - } - - lineWidth += R_GetCharacterGlyph(font, letter)->dx; - if (lineWidth > maxWidth) - maxWidth = lineWidth; - count++; - } - } - - return maxWidth; - } - Materials::Materials() { - Materials::ImageNameLength = 7; - // Allow codo images Materials::ImageVersionCheckHook.initialize(0x53A456, Materials::ImageVersionCheck, HOOK_CALL)->install(); - // Fix material pointer exploit - // Also make all material text icons have white tint - Utils::Hook(0x534E0C, Materials::DrawMaterialStub, HOOK_CALL).install()->quick(); - - // Increment string pointer accordingly - Utils::Hook(0x5358FA, Materials::PostDrawMaterialStub, HOOK_JUMP).install()->quick(); - // Adapt death message to IW5 material format Utils::Hook(0x5A30D9, Materials::DeathMessageStub, HOOK_JUMP).install()->quick(); @@ -371,9 +244,6 @@ namespace Components // Debug material comparison Utils::Hook::Set(0x523894, Materials::MaterialComparePrint); - // Consider material text icons when calculating text width - Utils::Hook(0x5056C0, Materials::R_TextWidth_Hk, HOOK_JUMP).install()->quick(); - #ifdef DEBUG if (Flags::HasFlag("dump")) { diff --git a/src/Components/Modules/Materials.hpp b/src/Components/Modules/Materials.hpp index a057cedb..38388ae6 100644 --- a/src/Components/Modules/Materials.hpp +++ b/src/Components/Modules/Materials.hpp @@ -21,20 +21,13 @@ namespace Components private: static std::vector ImageTable; static std::vector MaterialTable; - static int ImageNameLength; static Utils::Hook ImageVersionCheckHook; static void ImageVersionCheck(); - static Game::Material* ResolveMaterial(const char* stringPtr); - static void DrawMaterialStub(); - static void PostDrawMaterialStub(); - static int WriteDeathMessageIcon(char* string, int offset, Game::Material* material); static void DeathMessageStub(); - static int R_TextWidth_Hk(const char* text, int maxChars, Game::Font_s* font); - #ifdef DEBUG static void DumpImageCfg(int, const char*, const char* material); static void DumpImageCfgPath(int, const char*, const char* material); diff --git a/src/Components/Modules/PlayerName.cpp b/src/Components/Modules/PlayerName.cpp index 896853ee..79b3758d 100644 --- a/src/Components/Modules/PlayerName.cpp +++ b/src/Components/Modules/PlayerName.cpp @@ -2,37 +2,77 @@ namespace Components { - std::string PlayerName::PlayerNames[18]; + Dvar::Var PlayerName::sv_allowColoredNames; - int PlayerName::GetClientName(int /*localClientNum*/, int index, char *buf, int size) + void PlayerName::UserInfoCopy(char* buffer, const char* name, const size_t size) + { + if (!sv_allowColoredNames.get()) + { + char nameBuffer[64] = { 0 }; + TextRenderer::StripColors(name, nameBuffer, sizeof(nameBuffer)); + TextRenderer::StripAllTextIcons(nameBuffer, buffer, size); + } + else + { + TextRenderer::StripAllTextIcons(name, buffer, size); + } + + std::string readablePlayerName(buffer); + readablePlayerName = Utils::String::Trim(readablePlayerName); + + if (readablePlayerName.size() < 3) + { + strncpy(buffer, "Unknown Soldier", size); + } + } + + __declspec(naked) void PlayerName::ClientUserinfoChanged() + { + __asm + { + mov eax, [esp + 4h] // length + //sub eax, 1 + push eax + + push ecx // name + push edx // buffer + + call UserInfoCopy + + add esp, 0Ch + retn + } + } + + char* PlayerName::GetClientName(int localClientNum, int index, char* buf, size_t size) + { + Game::CL_GetClientName(localClientNum, index, buf, size); + + // Append clantag to username & remove the colors + strncpy_s(buf, size, TextRenderer::StripColors(ClanTags::GetUserClantag(index, buf)).data(), size); + + return buf; + } + char* PlayerName::CleanStrStub(char* string) { - if (index < 0 || index >= 18) return 0; - return strncpy_s(buf, size, PlayerName::PlayerNames[index].data(), PlayerName::PlayerNames[index].size()) == 0; + TextRenderer::StripColors(string, string, strlen(string) + 1); + return string; } PlayerName::PlayerName() { -#if(0) // Disabled for now - { - for (int i = 0; i < ARRAYSIZE(PlayerName::PlayerNames); ++i) - { - PlayerName::PlayerNames[i] = "mumu"; - } + sv_allowColoredNames = Dvar::Register("sv_allowColoredNames", true, Game::dvar_flag::DVAR_FLAG_NONE, "Allow colored names on the server"); - Utils::Hook(Game::CL_GetClientName, PlayerName::GetClientName, HOOK_JUMP).install()->quick(); - } -#endif + // Disable SV_UpdateUserinfo_f, to block changing the name ingame + Utils::Hook::Set(0x6258D0, 0xC3); - //const char* clanname = "ZOB"; - //Utils::Hook::Set(0x497656, clanname); - //Utils::Hook::Set(0x497679, clanname); - } + // Allow colored names ingame + Utils::Hook(0x5D8B40, ClientUserinfoChanged, HOOK_JUMP).install()->quick(); - PlayerName::~PlayerName() - { - for (int i = 0; i < ARRAYSIZE(PlayerName::PlayerNames); ++i) - { - PlayerName::PlayerNames[i].clear(); - } + // Though, don't apply that to overhead names. + Utils::Hook(0x581932, GetClientName, HOOK_CALL).install()->quick(); + + // Patch I_CleanStr + Utils::Hook(0x4AD470, CleanStrStub, HOOK_JUMP).install()->quick(); } } diff --git a/src/Components/Modules/PlayerName.hpp b/src/Components/Modules/PlayerName.hpp index 68c3c74d..8335afda 100644 --- a/src/Components/Modules/PlayerName.hpp +++ b/src/Components/Modules/PlayerName.hpp @@ -6,11 +6,14 @@ namespace Components { public: PlayerName(); - ~PlayerName(); + + static void UserInfoCopy(char* buffer, const char* name, size_t size); private: - static std::string PlayerNames[18]; + static Dvar::Var sv_allowColoredNames; - static int GetClientName(int localClientNum, int index, char *buf, int size); + static char* CleanStrStub(char* string); + static void ClientUserinfoChanged(); + static char* GetClientName(int localClientNum, int index, char* buf, size_t size); }; } diff --git a/src/Components/Modules/QuickPatch.cpp b/src/Components/Modules/QuickPatch.cpp index 06ec7545..1ab8479b 100644 --- a/src/Components/Modules/QuickPatch.cpp +++ b/src/Components/Modules/QuickPatch.cpp @@ -407,6 +407,32 @@ namespace Components // Passthrough to the game's own IsDynClassname return Utils::Hook::Call(0x444810)(a1); } + + void QuickPatch::CL_KeyEvent_OnEscape() + { + if (Game::Con_CancelAutoComplete()) + return; + + if (TextRenderer::HandleFontIconAutocompleteKey(0, TextRenderer::FONT_ICON_ACI_CONSOLE, Game::K_ESCAPE)) + return; + + // Close console + Game::Key_RemoveCatcher(0, ~Game::KEYCATCH_CONSOLE); + } + + __declspec(naked) void QuickPatch::CL_KeyEvent_ConsoleEscape_Stub() + { + __asm + { + pushad + call CL_KeyEvent_OnEscape + popad + + // Exit CL_KeyEvent function + mov ebx, 0x4F66F2 + jmp ebx + } + } QuickPatch::QuickPatch() { @@ -428,6 +454,9 @@ namespace Components // Filtering any mapents that is intended for Spec:Ops gamemode (CODO) and prevent them from spawning Utils::Hook(0x5FBD6E, QuickPatch::IsDynClassnameStub, HOOK_CALL).install()->quick(); + // Hook escape handling on open console to change behaviour to close the console instead of only canceling autocomplete + Utils::Hook(0x4F66A3, CL_KeyEvent_ConsoleEscape_Stub, HOOK_JUMP).install()->quick(); + // bounce dvar sv_enableBounces = Game::Dvar_RegisterBool("sv_enableBounces", false, Game::DVAR_FLAG_REPLICATED, "Enables bouncing on the server"); Utils::Hook(0x4B1B2D, QuickPatch::BounceStub, HOOK_JUMP).install()->quick(); diff --git a/src/Components/Modules/QuickPatch.hpp b/src/Components/Modules/QuickPatch.hpp index 7345b994..008f4a05 100644 --- a/src/Components/Modules/QuickPatch.hpp +++ b/src/Components/Modules/QuickPatch.hpp @@ -48,5 +48,8 @@ namespace Components static Game::dvar_t* g_playerEjection; static void PlayerEjectionStub(); static BOOL IsDynClassnameStub(char* a1); + + static void CL_KeyEvent_OnEscape(); + static void CL_KeyEvent_ConsoleEscape_Stub(); }; } diff --git a/src/Components/Modules/ServerInfo.cpp b/src/Components/Modules/ServerInfo.cpp index c5b8ffe9..04f08bb5 100644 --- a/src/Components/Modules/ServerInfo.cpp +++ b/src/Components/Modules/ServerInfo.cpp @@ -71,10 +71,10 @@ namespace Components } } - void ServerInfo::DrawScoreboardInfo(void* a1) + void ServerInfo::DrawScoreboardInfo(int localClientNum) { Game::Font_s* font = Game::R_RegisterFont("fonts/bigfont", 0); - void* cxt = Game::UI_GetContext(a1); + void* cxt = Game::ScrPlace_GetActivePlacement(localClientNum); std::string addressText = Network::Address(*Game::connectedHost).getString(); if (addressText == "0.0.0.0:0" || addressText == "loopback") addressText = "Listen Server"; diff --git a/src/Components/Modules/ServerInfo.hpp b/src/Components/Modules/ServerInfo.hpp index 8fceb827..36dd8cf7 100644 --- a/src/Components/Modules/ServerInfo.hpp +++ b/src/Components/Modules/ServerInfo.hpp @@ -36,7 +36,7 @@ namespace Components static const char* GetPlayerText(unsigned int index, int column); static void SelectPlayer(unsigned int index); - static void DrawScoreboardInfo(void* a1); + static void DrawScoreboardInfo(int localClientNum); static void DrawScoreboardStub(); }; } diff --git a/src/Components/Modules/ServerList.cpp b/src/Components/Modules/ServerList.cpp index 3f181de4..166f444f 100644 --- a/src/Components/Modules/ServerList.cpp +++ b/src/Components/Modules/ServerList.cpp @@ -469,10 +469,10 @@ namespace Components server.ping = (Game::Sys_Milliseconds() - i->sendTime); server.addr = address; - Dedicated::StripMaterialTextIcons(server.hostname.data()); - Dedicated::StripMaterialTextIcons(server.mapname.data()); - Dedicated::StripMaterialTextIcons(server.gametype.data()); - Dedicated::StripMaterialTextIcons(server.mod.data()); + server.hostname = TextRenderer::StripMaterialTextIcons(server.hostname); + server.mapname = TextRenderer::StripMaterialTextIcons(server.mapname); + server.gametype = TextRenderer::StripMaterialTextIcons(server.gametype); + server.mod = TextRenderer::StripMaterialTextIcons(server.mod); // Remove server from queue i = ServerList::RefreshContainer.servers.erase(i); @@ -592,8 +592,8 @@ namespace Components return info1->clients < info2->clients; } - std::string text1 = Utils::String::ToLower(Colors::Strip(ServerList::GetServerInfoText(info1, ServerList::SortKey, true))); - std::string text2 = Utils::String::ToLower(Colors::Strip(ServerList::GetServerInfoText(info2, ServerList::SortKey, true))); + std::string text1 = Utils::String::ToLower(TextRenderer::StripColors(ServerList::GetServerInfoText(info1, ServerList::SortKey, true))); + std::string text2 = Utils::String::ToLower(TextRenderer::StripColors(ServerList::GetServerInfoText(info2, ServerList::SortKey, true))); // ASCII-based comparison return text1.compare(text2) < 0; diff --git a/src/Components/Modules/TextRenderer.cpp b/src/Components/Modules/TextRenderer.cpp new file mode 100644 index 00000000..599244d8 --- /dev/null +++ b/src/Components/Modules/TextRenderer.cpp @@ -0,0 +1,1645 @@ +#include "STDInclude.hpp" + +namespace Game +{ + float* con_screenMin = reinterpret_cast(0xA15F48); +} + +namespace Components +{ + unsigned TextRenderer::colorTableDefault[TEXT_COLOR_COUNT] + { + ColorRgb(0, 0, 0), // TEXT_COLOR_BLACK + ColorRgb(255, 92, 92), // TEXT_COLOR_RED + ColorRgb(0, 255, 0), // TEXT_COLOR_GREEN + ColorRgb(255, 255, 0), // TEXT_COLOR_YELLOW + ColorRgb(0, 0, 255), // TEXT_COLOR_BLUE + ColorRgb(0, 255, 255), // TEXT_COLOR_LIGHT_BLUE + ColorRgb(255, 92, 255), // TEXT_COLOR_PINK + ColorRgb(255, 255, 255), // TEXT_COLOR_DEFAULT + ColorRgb(255, 255, 255), // TEXT_COLOR_AXIS + ColorRgb(255, 255, 255), // TEXT_COLOR_ALLIES + ColorRgb(255, 255, 255), // TEXT_COLOR_RAINBOW + ColorRgb(255, 255, 255), // TEXT_COLOR_SERVER + }; + + unsigned TextRenderer::colorTableNew[TEXT_COLOR_COUNT] + { + ColorRgb(0, 0, 0), // TEXT_COLOR_BLACK + ColorRgb(255, 49, 49), // TEXT_COLOR_RED + ColorRgb(134, 192, 0), // TEXT_COLOR_GREEN + ColorRgb(255, 173, 34), // TEXT_COLOR_YELLOW + ColorRgb(0, 135, 193), // TEXT_COLOR_BLUE + ColorRgb(32, 197, 255), // TEXT_COLOR_LIGHT_BLUE + ColorRgb(151, 80, 221), // TEXT_COLOR_PINK + ColorRgb(255, 255, 255), // TEXT_COLOR_DEFAULT + ColorRgb(255, 255, 255), // TEXT_COLOR_AXIS + ColorRgb(255, 255, 255), // TEXT_COLOR_ALLIES + ColorRgb(255, 255, 255), // TEXT_COLOR_RAINBOW + ColorRgb(255, 255, 255), // TEXT_COLOR_SERVER + }; + + unsigned(*TextRenderer::currentColorTable)[TEXT_COLOR_COUNT]; + TextRenderer::FontIconAutocompleteContext TextRenderer::autocompleteContextArray[FONT_ICON_ACI_COUNT]; + std::map TextRenderer::fontIconLookup; + std::vector TextRenderer::fontIconList; + + TextRenderer::BufferedLocalizedString TextRenderer::stringHintAutoComplete(REFERENCE_HINT_AUTO_COMPLETE, STRING_BUFFER_SIZE_SMALL); + TextRenderer::BufferedLocalizedString TextRenderer::stringHintModifier(REFERENCE_HINT_MODIFIER, STRING_BUFFER_SIZE_SMALL); + TextRenderer::BufferedLocalizedString TextRenderer::stringListHeader(REFERENCE_MODIFIER_LIST_HEADER, STRING_BUFFER_SIZE_SMALL); + TextRenderer::BufferedLocalizedString TextRenderer::stringListFlipHorizontal(REFERENCE_MODIFIER_LIST_FLIP_HORIZONTAL, STRING_BUFFER_SIZE_SMALL); + TextRenderer::BufferedLocalizedString TextRenderer::stringListFlipVertical(REFERENCE_MODIFIER_LIST_FLIP_VERTICAL, STRING_BUFFER_SIZE_SMALL); + TextRenderer::BufferedLocalizedString TextRenderer::stringListBig(REFERENCE_MODIFIER_LIST_BIG, STRING_BUFFER_SIZE_SMALL); + + Dvar::Var TextRenderer::cg_newColors; + Dvar::Var TextRenderer::cg_fontIconAutocomplete; + Dvar::Var TextRenderer::cg_fontIconAutocompleteHint; + Game::dvar_t* TextRenderer::sv_customTextColor; + Dvar::Var TextRenderer::r_colorBlind; + Game::dvar_t* TextRenderer::g_ColorBlind_MyTeam; + Game::dvar_t* TextRenderer::g_ColorBlind_EnemyTeam; + Game::dvar_t** TextRenderer::con_inputBoxColor = reinterpret_cast(0x9FD4BC); + + TextRenderer::BufferedLocalizedString::BufferedLocalizedString(const char* reference, const size_t bufferSize) + : stringReference(reference), + stringBuffer(std::make_unique(bufferSize)), + stringBufferSize(bufferSize), + stringWidth{-1} + { + + } + + void TextRenderer::BufferedLocalizedString::Cache() + { + const auto* formattingString = Game::UI_SafeTranslateString(stringReference); + + if (formattingString != nullptr) + { + strncpy(stringBuffer.get(), formattingString, stringBufferSize); + for (auto& width : stringWidth) + width = -1; + } + } + + const char* TextRenderer::BufferedLocalizedString::Format(const char* value) + { + const auto* formattingString = Game::UI_SafeTranslateString(stringReference); + if (formattingString == nullptr) + { + stringBuffer[0] = '\0'; + return stringBuffer.get(); + } + + Game::ConversionArguments conversionArguments{}; + conversionArguments.args[conversionArguments.argCount++] = value; + Game::UI_ReplaceConversions(formattingString, &conversionArguments, stringBuffer.get(), stringBufferSize); + + for (auto& width : stringWidth) + width = -1; + return stringBuffer.get(); + } + + const char* TextRenderer::BufferedLocalizedString::GetString() const + { + return stringBuffer.get(); + } + + int TextRenderer::BufferedLocalizedString::GetWidth(const FontIconAutocompleteInstance autocompleteInstance, Game::Font_s* font) + { + assert(autocompleteInstance < FONT_ICON_ACI_COUNT); + if (stringWidth[autocompleteInstance] < 0) + stringWidth[autocompleteInstance] = Game::R_TextWidth(GetString(), std::numeric_limits::max(), font); + + return stringWidth[autocompleteInstance]; + } + + TextRenderer::FontIconAutocompleteContext::FontIconAutocompleteContext() + : autocompleteActive(false), + inModifiers(false), + userClosed(false), + lastHash(0u), + results{}, + resultCount(0u), + hasMoreResults(false), + resultOffset(0u), + lastResultOffset(0u), + selectedOffset(0u), + maxFontIconWidth(0.0f), + maxMaterialNameWidth(0.0f), + stringSearchStartWith(REFERENCE_SEARCH_START_WITH, STRING_BUFFER_SIZE_BIG) + { + + } + + unsigned TextRenderer::HsvToRgb(HsvColor hsv) + { + unsigned rgb; + unsigned char region, p, q, t; + unsigned int h, s, v, remainder; + + if (hsv.s == 0) + { + rgb = ColorRgb(hsv.v, hsv.v, hsv.v); + return rgb; + } + + // converting to 16 bit to prevent overflow + h = hsv.h; + s = hsv.s; + v = hsv.v; + + region = static_cast(h / 43); + remainder = (h - (region * 43)) * 6; + + p = static_cast((v * (255 - s)) >> 8); + q = static_cast((v * (255 - ((s * remainder) >> 8))) >> 8); + t = static_cast((v * (255 - ((s * (255 - remainder)) >> 8))) >> 8); + + switch (region) + { + case 0: + rgb = ColorRgb(static_cast(v), t, p); + break; + case 1: + rgb = ColorRgb(q, static_cast(v), p); + break; + case 2: + rgb = ColorRgb(p, static_cast(v), t); + break; + case 3: + rgb = ColorRgb(p, q, static_cast(v)); + break; + case 4: + rgb = ColorRgb(t, p, static_cast(v)); + break; + default: + rgb = ColorRgb(static_cast(v), p, q); + break; + } + + return rgb; + } + + void TextRenderer::DrawAutocompleteBox(const FontIconAutocompleteContext& context, const float x, const float y, const float w, const float h, const float* color) + { + static constexpr float colorWhite[4] + { + 1.0f, + 1.0f, + 1.0f, + 1.0f + }; + const float borderColor[4] + { + color[0] * 0.5f, + color[1] * 0.5f, + color[2] * 0.5f, + color[3] + }; + + Game::R_AddCmdDrawStretchPic(x, y, w, h, 0.0, 0.0, 0.0, 0.0, color, Game::cls->whiteMaterial); + Game::R_AddCmdDrawStretchPic(x, y, FONT_ICON_AUTOCOMPLETE_BOX_BORDER, h, 0.0, 0.0, 0.0, 0.0, borderColor, Game::cls->whiteMaterial); + Game::R_AddCmdDrawStretchPic(x + w - FONT_ICON_AUTOCOMPLETE_BOX_BORDER, y, FONT_ICON_AUTOCOMPLETE_BOX_BORDER, h, 0.0, 0.0, 0.0, 0.0, borderColor, Game::cls->whiteMaterial); + Game::R_AddCmdDrawStretchPic(x, y, w, FONT_ICON_AUTOCOMPLETE_BOX_BORDER, 0.0, 0.0, 0.0, 0.0, borderColor, Game::cls->whiteMaterial); + Game::R_AddCmdDrawStretchPic(x, y + h - FONT_ICON_AUTOCOMPLETE_BOX_BORDER, w, FONT_ICON_AUTOCOMPLETE_BOX_BORDER, 0.0, 0.0, 0.0, 0.0, borderColor, Game::cls->whiteMaterial); + + if (context.resultOffset > 0) + { + Game::R_AddCmdDrawStretchPic(x + w - FONT_ICON_AUTOCOMPLETE_BOX_BORDER - FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + y + FONT_ICON_AUTOCOMPLETE_BOX_BORDER, + FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + 1.0f, 1.0f, 0.0f, 0.0f, colorWhite, Game::sharedUiInfo->assets.scrollBarArrowDown); + } + if(context.hasMoreResults) + { + Game::R_AddCmdDrawStretchPic(x + w - FONT_ICON_AUTOCOMPLETE_BOX_BORDER - FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + y + h - FONT_ICON_AUTOCOMPLETE_BOX_BORDER - FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + FONT_ICON_AUTOCOMPLETE_ARROW_SIZE, + 1.0f, 1.0f, 0.0f, 0.0f, colorWhite, Game::sharedUiInfo->assets.scrollBarArrowUp); + } + } + + void TextRenderer::UpdateAutocompleteContextResults(FontIconAutocompleteContext& context, Game::Font_s* font, const float textXScale) + { + context.resultCount = 0; + context.hasMoreResults = false; + context.lastResultOffset = context.resultOffset; + + auto skipCount = context.resultOffset; + + const auto queryLen = context.lastQuery.size(); + for(const auto& fontIconEntry : fontIconList) + { + const auto compareValue = fontIconEntry.iconName.compare(0, queryLen, context.lastQuery); + + if (compareValue == 0) + { + if (skipCount > 0) + { + skipCount--; + } + else if (context.resultCount < FontIconAutocompleteContext::MAX_RESULTS) + { + context.results[context.resultCount++] = { + Utils::String::VA(":%s:", fontIconEntry.iconName.data()), + fontIconEntry.iconName + }; + } + else + context.hasMoreResults = true; + } + else if (compareValue > 0) + break; + } + + context.maxFontIconWidth = 0; + context.maxMaterialNameWidth = 0; + for(auto resultIndex = 0u; resultIndex < context.resultCount; resultIndex++) + { + const auto& result = context.results[resultIndex]; + const auto fontIconWidth = static_cast(Game::R_TextWidth(result.fontIconName.c_str(), std::numeric_limits::max(), font)) * textXScale; + const auto materialNameWidth = static_cast(Game::R_TextWidth(result.materialName.c_str(), std::numeric_limits::max(), font)) * textXScale; + + if (fontIconWidth > context.maxFontIconWidth) + context.maxFontIconWidth = fontIconWidth; + if (materialNameWidth > context.maxMaterialNameWidth) + context.maxMaterialNameWidth = materialNameWidth; + } + } + + void TextRenderer::UpdateAutocompleteContext(FontIconAutocompleteContext& context, const Game::field_t* edit, Game::Font_s* font, const float textXScale) + { + int fontIconStart = -1; + auto inModifiers = false; + + for(auto i = 0; i < edit->cursor; i++) + { + const auto c = static_cast(edit->buffer[i]); + if (c == FONT_ICON_SEPARATOR_CHARACTER) + { + if(fontIconStart < 0) + { + fontIconStart = i + 1; + inModifiers = false; + } + else + { + fontIconStart = -1; + inModifiers = false; + } + } + else if(isspace(c)) + { + fontIconStart = -1; + inModifiers = false; + } + else if(c == FONT_ICON_MODIFIER_SEPARATOR_CHARACTER) + { + if (fontIconStart >= 0 && !inModifiers) + { + inModifiers = true; + } + else + { + fontIconStart = -1; + inModifiers = false; + } + } + } + + if(fontIconStart < 0 // Not in fonticon sequence + || fontIconStart == edit->cursor // Did not type the first letter yet + || !isalpha(static_cast(edit->buffer[fontIconStart])) // First letter of the icon is not alphabetic + || (fontIconStart > 1 && isalnum(static_cast(edit->buffer[fontIconStart - 2]))) // Letter before sequence is alnum + ) + { + context.autocompleteActive = false; + context.userClosed = false; + context.lastHash = 0; + context.resultCount = 0; + return; + } + + context.inModifiers = inModifiers; + + // Update scroll + if(context.selectedOffset < context.resultOffset) + context.resultOffset = context.selectedOffset; + else if(context.selectedOffset >= context.resultOffset + FontIconAutocompleteContext::MAX_RESULTS) + context.resultOffset = context.selectedOffset - (FontIconAutocompleteContext::MAX_RESULTS - 1); + + // If the user closed the context do not draw or update + if (context.userClosed) + return; + + context.autocompleteActive = true; + + // No need to update results when in modifiers + if (context.inModifiers) + return; + + // Check if results need updates + const auto currentFontIconHash = Game::R_HashString(&edit->buffer[fontIconStart], edit->cursor - fontIconStart); + if (currentFontIconHash == context.lastHash && context.lastResultOffset == context.resultOffset) + return; + + // If query was updated then reset scroll parameters + if(currentFontIconHash != context.lastHash) + { + context.resultOffset = 0; + context.selectedOffset = 0; + context.lastHash = currentFontIconHash; + } + + // Update results for query and scroll and update search string + context.lastQuery = std::string(&edit->buffer[fontIconStart], edit->cursor - fontIconStart); + context.stringSearchStartWith.Format(context.lastQuery.c_str()); + UpdateAutocompleteContextResults(context, font, textXScale); + } + + void TextRenderer::DrawAutocompleteModifiers(const FontIconAutocompleteInstance instance, const float x, const float y, Game::Font_s* font, const float textXScale, const float textYScale) + { + assert(instance < FONT_ICON_ACI_COUNT); + const auto& context = autocompleteContextArray[instance]; + + // Check which is the longest string to be able to calculate how big the box needs to be + const auto longestStringLength = std::max(std::max(std::max(stringListHeader.GetWidth(instance, font), stringListFlipHorizontal.GetWidth(instance, font)), + stringListFlipVertical.GetWidth(instance, font)), + stringListBig.GetWidth(instance, font)); + + // Draw background box + const auto boxWidth = static_cast(longestStringLength) * textXScale; + constexpr auto totalLines = 4u; + const auto lineHeight = static_cast(font->pixelHeight) * textYScale; + DrawAutocompleteBox(context, + x - FONT_ICON_AUTOCOMPLETE_BOX_PADDING, + y - FONT_ICON_AUTOCOMPLETE_BOX_PADDING, + boxWidth + FONT_ICON_AUTOCOMPLETE_BOX_PADDING * 2, + static_cast(totalLines) * lineHeight + FONT_ICON_AUTOCOMPLETE_BOX_PADDING * 2, + (*con_inputBoxColor)->current.vector); + + auto currentY = y + lineHeight; + + // Draw header line: "Following modifiers are available:" + Game::R_AddCmdDrawText(stringListHeader.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + currentY += lineHeight; + + // Draw modifier hints + Game::R_AddCmdDrawText(stringListFlipHorizontal.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + currentY += lineHeight; + Game::R_AddCmdDrawText(stringListFlipVertical.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + currentY += lineHeight; + Game::R_AddCmdDrawText(stringListBig.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + } + + void TextRenderer::DrawAutocompleteResults(const FontIconAutocompleteInstance instance, const float x, const float y, Game::Font_s* font, const float textXScale, const float textYScale) + { + assert(instance < FONT_ICON_ACI_COUNT); + auto& context = autocompleteContextArray[instance]; + + const auto hintEnabled = cg_fontIconAutocompleteHint.get(); + + // Check which is the longest string to be able to calculate how big the box needs to be + auto longestStringLength = context.stringSearchStartWith.GetWidth(instance, font); + if(hintEnabled) + longestStringLength = std::max(std::max(longestStringLength, stringHintAutoComplete.GetWidth(instance, font)), stringHintModifier.GetWidth(instance, font)); + + const auto colSpacing = FONT_ICON_AUTOCOMPLETE_COL_SPACING * textXScale; + const auto boxWidth = std::max(context.maxFontIconWidth + context.maxMaterialNameWidth + colSpacing, static_cast(longestStringLength) * textXScale); + const auto lineHeight = static_cast(font->pixelHeight) * textYScale; + + // Draw background box + const auto totalLines = 1u + context.resultCount + (hintEnabled ? 2u : 0u); + const auto arrowPadding = context.resultOffset > 0 || context.hasMoreResults ? FONT_ICON_AUTOCOMPLETE_ARROW_SIZE : 0.0f; + DrawAutocompleteBox(context, + x - FONT_ICON_AUTOCOMPLETE_BOX_PADDING, + y - FONT_ICON_AUTOCOMPLETE_BOX_PADDING, + boxWidth + FONT_ICON_AUTOCOMPLETE_BOX_PADDING * 2 + arrowPadding, + static_cast(totalLines) * lineHeight + FONT_ICON_AUTOCOMPLETE_BOX_PADDING * 2, + (*con_inputBoxColor)->current.vector); + + // Draw header line "Search results for: xyz" + auto currentY = y + lineHeight; + Game::R_AddCmdDrawText(context.stringSearchStartWith.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + currentY += lineHeight; + + // Draw search results + const auto selectedIndex = context.selectedOffset - context.resultOffset; + for(auto resultIndex = 0u; resultIndex < context.resultCount; resultIndex++) + { + const auto& result = context.results[resultIndex]; + Game::R_AddCmdDrawText(result.fontIconName.c_str(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + + if (selectedIndex == resultIndex) + Game::R_AddCmdDrawText(Utils::String::VA("^2%s", result.materialName.c_str()), std::numeric_limits::max(), font, x + context.maxFontIconWidth + colSpacing, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + else + Game::R_AddCmdDrawText(result.materialName.c_str(), std::numeric_limits::max(), font, x + context.maxFontIconWidth + colSpacing, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); + currentY += lineHeight; + } + + // Draw extra hint if enabled + if(hintEnabled) + { + Game::R_AddCmdDrawText(stringHintAutoComplete.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, HINT_COLOR, 0); + currentY += lineHeight; + Game::R_AddCmdDrawText(stringHintModifier.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, HINT_COLOR, 0); + } + } + + void TextRenderer::DrawAutocomplete(const FontIconAutocompleteInstance instance, const float x, const float y, Game::Font_s* font, const float textXScale, const float textYScale) + { + assert(instance < FONT_ICON_ACI_COUNT); + const auto& context = autocompleteContextArray[instance]; + + if (context.inModifiers) + DrawAutocompleteModifiers(instance, x, y, font, textXScale, textYScale); + else + DrawAutocompleteResults(instance, x, y, font, textXScale, textYScale); + } + + void TextRenderer::Con_DrawInput_Hk(const int localClientNum) + { + // Call original function + Utils::Hook::Call(0x5A4480)(localClientNum); + + auto& autocompleteContext = autocompleteContextArray[FONT_ICON_ACI_CONSOLE]; + if (cg_fontIconAutocomplete.get() == false) + { + autocompleteContext.autocompleteActive = false; + return; + } + + UpdateAutocompleteContext(autocompleteContext, Game::g_consoleField, Game::cls->consoleFont, 1.0f); + if (autocompleteContext.autocompleteActive) + { + const auto x = Game::conDrawInputGlob->leftX; + const auto y = Game::con_screenMin[1] + 6.0f + static_cast(2 * Game::R_TextHeight(Game::cls->consoleFont)); + DrawAutocomplete(FONT_ICON_ACI_CONSOLE, x, y, Game::cls->consoleFont, 1.0f, 1.0f); + } + } + + void TextRenderer::Field_Draw_Say(const int localClientNum, Game::field_t* edit, const int x, const int y, const int horzAlign, const int vertAlign) + { + Game::Field_Draw(localClientNum, edit, x, y, horzAlign, vertAlign); + + auto& autocompleteContext = autocompleteContextArray[FONT_ICON_ACI_CHAT]; + if (cg_fontIconAutocomplete.get() == false) + { + autocompleteContext.autocompleteActive = false; + return; + } + + auto* screenPlacement = Game::ScrPlace_GetActivePlacement(localClientNum); + const auto scale = edit->charHeight / 48.0f; + auto* font = Game::UI_GetFontHandle(screenPlacement, 0, scale); + const auto normalizedScale = Game::R_NormalizedTextScale(font, scale); + auto xx = static_cast(x); + auto yy = static_cast(y); + yy += static_cast(Game::R_TextHeight(font)) * normalizedScale * 1.5f; + auto ww = normalizedScale; + auto hh = normalizedScale; + Game::ScrPlace_ApplyRect(screenPlacement, &xx, &yy, &ww, &hh, horzAlign, vertAlign); + + UpdateAutocompleteContext(autocompleteContext, edit, font, ww); + if (autocompleteContext.autocompleteActive) + { + DrawAutocomplete(FONT_ICON_ACI_CHAT, std::floor(xx), std::floor(yy), font, ww, hh); + } + } + + void TextRenderer::AutocompleteUp(FontIconAutocompleteContext& context) + { + if (context.selectedOffset > 0) + context.selectedOffset--; + } + + void TextRenderer::AutocompleteDown(FontIconAutocompleteContext& context) + { + if (context.resultCount < FontIconAutocompleteContext::MAX_RESULTS) + { + if (context.resultCount > 0 && context.selectedOffset < context.resultOffset + context.resultCount - 1) + context.selectedOffset++; + } + else if (context.selectedOffset == context.resultOffset + context.resultCount - 1) + { + if (context.hasMoreResults) + context.selectedOffset++; + } + else + { + context.selectedOffset++; + } + } + + void TextRenderer::AutocompleteFill(const FontIconAutocompleteContext& context, Game::ScreenPlacement* scrPlace, Game::field_t* edit, const bool closeFontIcon) + { + if (context.selectedOffset >= context.resultOffset + context.resultCount) + return; + + const auto selectedResultIndex = context.selectedOffset - context.resultOffset; + std::string remainingFillData = context.results[selectedResultIndex].materialName.substr(context.lastQuery.size()); + if (closeFontIcon) + remainingFillData += ":"; + const std::string moveData(&edit->buffer[edit->cursor]); + + const auto remainingBufferCharacters = std::extent_v - edit->cursor - moveData.size() - 1; + if(remainingFillData.size() > remainingBufferCharacters) + remainingFillData = remainingFillData.erase(remainingBufferCharacters); + + if(!remainingFillData.empty()) + { + strncpy(&edit->buffer[edit->cursor], remainingFillData.c_str(), remainingFillData.size()); + strncpy(&edit->buffer[edit->cursor + remainingFillData.size()], moveData.c_str(), moveData.size()); + edit->buffer[std::extent_v - 1] = '\0'; + edit->cursor += static_cast(remainingFillData.size()); + Game::Field_AdjustScroll(scrPlace, edit); + } + } + + bool TextRenderer::AutocompleteHandleKeyDown(FontIconAutocompleteContext& context, const int key, Game::ScreenPlacement* scrPlace, Game::field_t* edit) + { + switch (key) + { + case Game::K_UPARROW: + case Game::K_KP_UPARROW: + AutocompleteUp(context); + return true; + + case Game::K_DOWNARROW: + case Game::K_KP_DOWNARROW: + AutocompleteDown(context); + return true; + + case Game::K_ENTER: + case Game::K_KP_ENTER: + if(context.resultCount > 0) + { + AutocompleteFill(context, scrPlace, edit, true); + return true; + } + return false; + + case Game::K_TAB: + AutocompleteFill(context, scrPlace, edit, false); + return true; + + case Game::K_ESCAPE: + if (!context.userClosed) + { + context.autocompleteActive = false; + context.userClosed = true; + return true; + } + return false; + + default: + return false; + } + } + + bool TextRenderer::HandleFontIconAutocompleteKey(const int localClientNum, const FontIconAutocompleteInstance autocompleteInstance, const int key) + { + assert(autocompleteInstance < FONT_ICON_ACI_COUNT); + if (autocompleteInstance >= FONT_ICON_ACI_COUNT) + return false; + + auto& autocompleteContext = autocompleteContextArray[autocompleteInstance]; + if (!autocompleteContext.autocompleteActive) + return false; + + if(autocompleteInstance == FONT_ICON_ACI_CONSOLE) + return AutocompleteHandleKeyDown(autocompleteContext, key, Game::scrPlaceFull, Game::g_consoleField); + + if(autocompleteInstance == FONT_ICON_ACI_CHAT) + return AutocompleteHandleKeyDown(autocompleteContext, key, &Game::scrPlaceView[localClientNum], &Game::playerKeys[localClientNum].chatField); + + return false; + } + + void TextRenderer::Console_Key_Hk(const int localClientNum, const int key) + { + if (HandleFontIconAutocompleteKey(localClientNum, FONT_ICON_ACI_CONSOLE, key)) + return; + + Utils::Hook::Call(0x4311E0)(localClientNum, key); + } + + bool TextRenderer::ChatHandleKeyDown(const int localClientNum, const int key) + { + return HandleFontIconAutocompleteKey(localClientNum, FONT_ICON_ACI_CHAT, key); + } + + constexpr auto Message_Key = 0x5A7E50; + __declspec(naked) void TextRenderer::Message_Key_Stub() + { + __asm + { + pushad + + push eax + push edi + call ChatHandleKeyDown + add esp, 0x8 + test al,al + jnz skipHandling + + popad + call Message_Key + ret + + skipHandling: + popad + mov al, 1 + ret + } + } + + float TextRenderer::GetMonospaceWidth(Game::Font_s* font, int rendererFlags) + { + if(rendererFlags & Game::TEXT_RENDERFLAG_FORCEMONOSPACE) + return Game::R_GetCharacterGlyph(font, 'o')->dx; + + return 0.0f; + } + + void TextRenderer::GlowColor(Game::GfxColor* result, const Game::GfxColor baseColor, const Game::GfxColor forcedGlowColor, int renderFlags) + { + if (renderFlags & Game::TEXT_RENDERFLAG_GLOW_FORCE_COLOR) + { + result->array[0] = forcedGlowColor.array[0]; + result->array[1] = forcedGlowColor.array[1]; + result->array[2] = forcedGlowColor.array[2]; + } + else + { + result->array[0] = static_cast(std::floor(static_cast(static_cast(baseColor.array[0])) * 0.06f)); + result->array[1] = static_cast(std::floor(static_cast(static_cast(baseColor.array[1])) * 0.06f)); + result->array[2] = static_cast(std::floor(static_cast(static_cast(baseColor.array[2])) * 0.06f)); + } + } + + unsigned TextRenderer::R_FontGetRandomLetter(const int seed) + { + static constexpr char RANDOM_CHARACTERS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + return RANDOM_CHARACTERS[seed % (std::extent_v -1)]; + } + + void TextRenderer::DrawTextFxExtraCharacter(Game::Material* material, const int charIndex, const float x, const float y, const float w, const float h, const float sinAngle, const float cosAngle, const unsigned color) + { + Game::RB_DrawStretchPicRotate(material, x, y, w, h, static_cast(charIndex % 16) * 0.0625f, 0.0f, static_cast(charIndex % 16) * 0.0625f + 0.0625f, 1.0f, sinAngle, cosAngle, color); + } + + Game::GfxImage* TextRenderer::GetFontIconColorMap(const Game::Material* fontIconMaterial) + { + for (auto i = 0u; i < fontIconMaterial->textureCount; i++) + { + if (fontIconMaterial->textureTable[i].nameHash == COLOR_MAP_HASH) + return fontIconMaterial->textureTable[i].u.image; + } + + return nullptr; + } + + bool TextRenderer::IsFontIcon(const char*& text, FontIconInfo& fontIcon) + { + const auto* curPos = text; + + while (*curPos != ' ' && *curPos != FONT_ICON_SEPARATOR_CHARACTER && *curPos != 0 && *curPos != FONT_ICON_MODIFIER_SEPARATOR_CHARACTER) + curPos++; + + const auto* nameEnd = curPos; + + if(*curPos == FONT_ICON_MODIFIER_SEPARATOR_CHARACTER) + { + auto breakArgs = false; + while(!breakArgs) + { + curPos++; + switch(*curPos) + { + case FONT_ICON_MODIFIER_FLIP_HORIZONTALLY: + fontIcon.flipHorizontal = true; + break; + + case FONT_ICON_MODIFIER_FLIP_VERTICALLY: + fontIcon.flipVertical = true; + break; + + case FONT_ICON_MODIFIER_BIG: + fontIcon.big = true; + break; + + case FONT_ICON_SEPARATOR_CHARACTER: + breakArgs = true; + break; + + default: + return false; + } + } + } + + if (*curPos != FONT_ICON_SEPARATOR_CHARACTER) + return false; + + const std::string fontIconName(text, nameEnd - text); + + const auto foundFontIcon = fontIconLookup.find(fontIconName); + if (foundFontIcon == fontIconLookup.end()) + return false; + + auto& entry = foundFontIcon->second; + if(entry.material == nullptr) + { + auto* materialEntry = Game::DB_FindXAssetEntry(Game::XAssetType::ASSET_TYPE_MATERIAL, entry.materialName.data()); + if (materialEntry == nullptr) + return false; + auto* material = materialEntry->asset.header.material; + if (material == nullptr || material->techniqueSet == nullptr || material->techniqueSet->name == nullptr) + return false; + + if(strcmp(material->techniqueSet->name, "2d") != 0) + { + Logger::Print("^1Fonticon material '%s' does not have 2d techset!\n", material->info.name); + material = Game::DB_FindXAssetHeader(Game::ASSET_TYPE_MATERIAL, "default").material; + } + + entry.material = material; + } + + text = curPos + 1; + fontIcon.material = entry.material; + return true; + } + + float TextRenderer::GetNormalizedFontIconWidth(const FontIconInfo& fontIcon) + { + const auto* colorMap = GetFontIconColorMap(fontIcon.material); + if (colorMap == nullptr) + return 0; + const auto sizeMultiplier = fontIcon.big ? 1.5f : 1.0f; + auto colWidth = static_cast(colorMap->width); + auto colHeight = static_cast(colorMap->height); + if (fontIcon.material->info.textureAtlasColumnCount > 1) + colWidth /= static_cast(fontIcon.material->info.textureAtlasColumnCount); + if (fontIcon.material->info.textureAtlasRowCount > 1) + colHeight /= static_cast(fontIcon.material->info.textureAtlasRowCount); + return (colWidth / colHeight) * sizeMultiplier; + } + + float TextRenderer::GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, const float xScale) + { + const auto* colorMap = GetFontIconColorMap(fontIcon.material); + if (colorMap == nullptr) + return 0; + const auto sizeMultiplier = fontIcon.big ? 1.5f : 1.0f; + auto colWidth = static_cast(colorMap->width); + auto colHeight = static_cast(colorMap->height); + if (fontIcon.material->info.textureAtlasColumnCount > 1) + colWidth /= static_cast(fontIcon.material->info.textureAtlasColumnCount); + if (fontIcon.material->info.textureAtlasRowCount > 1) + colHeight /= static_cast(fontIcon.material->info.textureAtlasRowCount); + return static_cast(font->pixelHeight) * (colWidth / colHeight) * xScale * sizeMultiplier; + } + + float TextRenderer::DrawFontIcon(const FontIconInfo& fontIcon, const float x, const float y, const float sinAngle, const float cosAngle, const Game::Font_s* font, const float xScale, const float yScale, const unsigned color) + { + const auto* colorMap = GetFontIconColorMap(fontIcon.material); + if (colorMap == nullptr) + return 0; + + float s0, t0, s1, t1; + if(fontIcon.flipHorizontal) + { + s0 = 1.0f; + s1 = 0.0f; + } + else + { + s0 = 0.0f; + s1 = 1.0f; + } + if(fontIcon.flipVertical) + { + t0 = 1.0f; + t1 = 0.0f; + } + else + { + t0 = 0.0f; + t1 = 1.0f; + } + Game::Material_Process2DTextureCoordsForAtlasing(fontIcon.material, &s0, &s1, &t0, &t1); + const auto sizeMultiplier = fontIcon.big ? 1.5f : 1.0f; + + auto colWidth = static_cast(colorMap->width); + auto colHeight = static_cast(colorMap->height); + if (fontIcon.material->info.textureAtlasColumnCount > 1) + colWidth /= static_cast(fontIcon.material->info.textureAtlasColumnCount); + if (fontIcon.material->info.textureAtlasRowCount > 1) + colHeight /= static_cast(fontIcon.material->info.textureAtlasRowCount); + + const auto h = static_cast(font->pixelHeight) * yScale * sizeMultiplier; + const auto w = static_cast(font->pixelHeight) * (colWidth / colHeight) * xScale * sizeMultiplier; + + const auto yy = y - (h + yScale * static_cast(font->pixelHeight)) * 0.5f; + Game::RB_DrawStretchPicRotate(fontIcon.material, x, yy, w, h, s0, t0, s1, t1, sinAngle, cosAngle, color); + + return w; + } + + float TextRenderer::DrawHudIcon(const char*& text, const float x, const float y, const float sinAngle, const float cosAngle, const Game::Font_s* font, const float xScale, const float yScale, const unsigned color) + { + float s0, s1, t0, t1; + + if(*text == '\x01') + { + s0 = 0.0; + t0 = 0.0; + s1 = 1.0; + t1 = 1.0; + } + else + { + s0 = 1.0; + t0 = 0.0; + s1 = 0.0; + t1 = 1.0; + } + text++; + + if (*text == 0) + return 0; + + const auto v12 = font->pixelHeight * (*text - 16) + 16; + const auto w = static_cast((((v12 >> 24) & 0x1F) + v12) >> 5) * xScale; + text++; + + if (*text == 0) + return 0; + + const auto h = static_cast((font->pixelHeight * (*text - 16) + 16) >> 5) * yScale; + text++; + + if (*text == 0) + return 0; + + const auto materialNameLen = static_cast(*text); + text++; + + for(auto i = 0u; i < materialNameLen; i++) + { + if (text[i] == 0) + return 0; + } + + const std::string materialName(text, materialNameLen); + text += materialNameLen; + + auto* material = Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_MATERIAL, materialName.data()).material; + if (material == nullptr || material->techniqueSet == nullptr || material->techniqueSet->name == nullptr || strcmp(material->techniqueSet->name, "2d") != 0) + material = Game::DB_FindXAssetHeader(Game::XAssetType::ASSET_TYPE_MATERIAL, "default").material; + + const auto yy = y - (h + yScale * static_cast(font->pixelHeight)) * 0.5f; + + Game::RB_DrawStretchPicRotate(material, x, yy, w, h, s0, t0, s1, t1, sinAngle, cosAngle, color); + + return w; + } + + void TextRenderer::RotateXY(const float cosAngle, const float sinAngle, const float pivotX, const float pivotY, const float x, const float y, float* outX, float* outY) + { + *outX = (x - pivotX) * cosAngle + pivotX - (y - pivotY) * sinAngle; + *outY = (y - pivotY) * cosAngle + pivotY + (x - pivotX) * sinAngle; + } + + void TextRenderer::DrawText2D(const char* text, float x, float y, Game::Font_s* font, float xScale, float yScale, float sinAngle, float cosAngle, Game::GfxColor color, int maxLength, int renderFlags, int cursorPos, char cursorLetter, float padding, Game::GfxColor glowForcedColor, int fxBirthTime, int fxLetterTime, int fxDecayStartTime, int fxDecayDuration, Game::Material* fxMaterial, Game::Material* fxMaterialGlow) + { + UpdateColorTable(); + + Game::GfxColor dropShadowColor{0}; + dropShadowColor.array[3] = color.array[3]; + + int randSeed = 1; + bool drawRandomCharAtEnd = false; + const auto forceMonospace = renderFlags & Game::TEXT_RENDERFLAG_FORCEMONOSPACE; + const auto monospaceWidth = GetMonospaceWidth(font, renderFlags); + auto* material = font->material; + Game::Material* glowMaterial = nullptr; + + bool decaying; + int decayTimeElapsed; + if(renderFlags & Game::TEXT_RENDERFLAG_FX_DECODE) + { + if (!Game::SetupPulseFXVars(text, maxLength, fxBirthTime, fxLetterTime, fxDecayStartTime, fxDecayDuration, &drawRandomCharAtEnd, &randSeed, &maxLength, &decaying, &decayTimeElapsed)) + return; + } + else + { + drawRandomCharAtEnd = false; + randSeed = 1; + decaying = false; + decayTimeElapsed = 0; + } + + Game::FontPassType passes[Game::FONTPASS_COUNT]; + unsigned passCount = 0; + + if(renderFlags & Game::TEXT_RENDERFLAG_OUTLINE) + { + if(renderFlags & Game::TEXT_RENDERFLAG_GLOW) + { + glowMaterial = font->glowMaterial; + passes[passCount++] = Game::FONTPASS_GLOW; + } + + passes[passCount++] = Game::FONTPASS_OUTLINE; + passes[passCount++] = Game::FONTPASS_NORMAL; + } + else + { + passes[passCount++] = Game::FONTPASS_NORMAL; + + if (renderFlags & Game::TEXT_RENDERFLAG_GLOW) + { + glowMaterial = font->glowMaterial; + passes[passCount++] = Game::FONTPASS_GLOW; + } + } + + const auto startX = x - xScale * 0.5f; + const auto startY = y - 0.5f * yScale; + + for(auto passIndex = 0u; passIndex < passCount; passIndex++) + { + float xRot, yRot; + const char* curText = text; + auto maxLengthRemaining = maxLength; + auto currentColor = color; + auto subtitleAllowGlow = false; + auto extraFxChar = 0; + auto drawExtraFxChar = false; + auto passRandSeed = randSeed; + auto count = 0; + auto xa = startX; + auto xy = startY; + + while(*curText && maxLengthRemaining) + { + if (passes[passIndex] == Game::FONTPASS_NORMAL && renderFlags & Game::TEXT_RENDERFLAG_CURSOR && count == cursorPos) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa, xy, &xRot, &yRot); + Game::RB_DrawCursor(material, cursorLetter, xRot, yRot, sinAngle, cosAngle, font, xScale, yScale, color.packed); + } + + auto letter = Game::SEH_ReadCharFromString(&curText, nullptr); + + if(letter == '^' && *curText >= COLOR_FIRST_CHAR && *curText <= COLOR_LAST_CHAR) + { + const auto colorIndex = ColorIndexForChar(*curText); + subtitleAllowGlow = false; + if (colorIndex == TEXT_COLOR_DEFAULT) + { + currentColor = color; + } + else if (renderFlags & Game::TEXT_RENDERFLAG_SUBTITLETEXT && colorIndex == TEXT_COLOR_GREEN) + { + constexpr Game::GfxColor altColor{ MY_ALTCOLOR_TWO }; + subtitleAllowGlow = true; + // Swap r and b for whatever reason + currentColor.packed = ColorRgba(altColor.array[2], altColor.array[1], altColor.array[0], Game::ModulateByteColors(altColor.array[3], color.array[3])); + } + else + { + const Game::GfxColor colorTableColor{ (*currentColorTable)[colorIndex] }; + // Swap r and b for whatever reason + currentColor.packed = ColorRgba(colorTableColor.array[2], colorTableColor.array[1], colorTableColor.array[0], color.array[3]); + } + + if(!(renderFlags & Game::TEXT_RENDERFLAG_CURSOR && cursorPos > count && cursorPos < count + 2)) + { + curText++; + count += 2; + continue; + } + } + + auto finalColor = currentColor; + + if(letter == '^' && (*curText == '\x01' || *curText == '\x02')) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa, xy, &xRot, &yRot); + xa += DrawHudIcon(curText, xRot, yRot, sinAngle, cosAngle, font, xScale, yScale, ColorRgba(255, 255, 255, finalColor.array[3])); + + if (renderFlags & Game::TEXT_RENDERFLAG_PADDING) + xa += xScale * padding; + ++count; + maxLengthRemaining--; + continue; + } + + if(letter == FONT_ICON_SEPARATOR_CHARACTER) + { + FontIconInfo fontIconInfo{}; + const char* fontIconEnd = curText; + if(IsFontIcon(fontIconEnd, fontIconInfo) && !(renderFlags & Game::TEXT_RENDERFLAG_CURSOR && cursorPos > count && cursorPos <= count + (fontIconEnd - curText))) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa, xy, &xRot, &yRot); + + if(passes[passIndex] == Game::FONTPASS_NORMAL) + xa += DrawFontIcon(fontIconInfo, xRot, yRot, sinAngle, cosAngle, font, xScale, yScale, ColorRgba(255, 255, 255, finalColor.array[3])); + else + xa += GetFontIconWidth(fontIconInfo, font, xScale); + + if (renderFlags & Game::TEXT_RENDERFLAG_PADDING) + xa += xScale * padding; + count += (fontIconEnd - curText) + 1; + maxLengthRemaining--; + curText = fontIconEnd; + continue; + } + } + + if(drawRandomCharAtEnd && maxLengthRemaining == 1) + { + letter = R_FontGetRandomLetter(Game::RandWithSeed(&passRandSeed)); + + if(Game::RandWithSeed(&passRandSeed) % 2) + { + drawExtraFxChar = true; + letter = 'O'; + } + } + + if(letter == '\n') + { + xa = startX; + xy += static_cast(font->pixelHeight) * yScale; + continue; + } + + if(letter == '\r') + { + xy += static_cast(font->pixelHeight) * yScale; + continue; + } + + auto skipDrawing = false; + if(decaying) + { + char decayAlpha; + Game::GetDecayingLetterInfo(letter, &passRandSeed, decayTimeElapsed, fxBirthTime, fxDecayDuration, currentColor.array[3], &skipDrawing, &decayAlpha, &letter, &drawExtraFxChar); + finalColor.array[3] = decayAlpha; + } + + if(drawExtraFxChar) + { + auto tempSeed = passRandSeed; + extraFxChar = Game::RandWithSeed(&tempSeed); + } + + auto glyph = Game::R_GetCharacterGlyph(font, letter); + auto xAdj = static_cast(glyph->x0) * xScale; + auto yAdj = static_cast(glyph->y0) * yScale; + + if(!skipDrawing) + { + if (passes[passIndex] == Game::FONTPASS_NORMAL) + { + if (renderFlags & Game::TEXT_RENDERFLAG_DROPSHADOW) + { + auto ofs = 1.0f; + if (renderFlags & Game::TEXT_RENDERFLAG_DROPSHADOW_EXTRA) + ofs += 1.0f; + + xRot = xa + xAdj + ofs; + yRot = xy + yAdj + ofs; + RotateXY(cosAngle, sinAngle, startX, startY, xRot, yRot, &xRot, &yRot); + if (drawExtraFxChar) + DrawTextFxExtraCharacter(fxMaterial, extraFxChar, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, dropShadowColor.packed); + else + Game::RB_DrawChar(material, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, glyph, dropShadowColor.packed); + } + + RotateXY(cosAngle, sinAngle, startX, startY, xa + xAdj, xy + yAdj, &xRot, &yRot); + if (drawExtraFxChar) + DrawTextFxExtraCharacter(fxMaterial, extraFxChar, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, finalColor.packed); + else + Game::RB_DrawChar(material, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, glyph, finalColor.packed); + } + else if(passes[passIndex] == Game::FONTPASS_OUTLINE) + { + auto outlineSize = 1.0f; + if (renderFlags & Game::TEXT_RENDERFLAG_OUTLINE_EXTRA) + outlineSize = 1.3f; + + for (const auto offset : MY_OFFSETS) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa + xAdj + outlineSize * offset[0], xy + yAdj + outlineSize * offset[1], &xRot, &yRot); + if (drawExtraFxChar) + DrawTextFxExtraCharacter(fxMaterial, extraFxChar, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, dropShadowColor.packed); + else + Game::RB_DrawChar(material, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, glyph, dropShadowColor.packed); + } + } + else if(passes[passIndex] == Game::FONTPASS_GLOW && ((renderFlags & Game::TEXT_RENDERFLAG_SUBTITLETEXT) == 0 || subtitleAllowGlow)) + { + GlowColor(&finalColor, finalColor, glowForcedColor, renderFlags); + + for (const auto offset : MY_OFFSETS) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa + xAdj + 2.0f * offset[0] * xScale, xy + yAdj + 2.0f * offset[1] * yScale, &xRot, &yRot); + if (drawExtraFxChar) + DrawTextFxExtraCharacter(fxMaterialGlow, extraFxChar, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, finalColor.packed); + else + Game::RB_DrawChar(glowMaterial, xRot, yRot, static_cast(glyph->pixelWidth) * xScale, static_cast(glyph->pixelHeight) * yScale, sinAngle, cosAngle, glyph, finalColor.packed); + } + } + } + + if(forceMonospace) + xa += monospaceWidth * xScale; + else + xa += static_cast(glyph->dx) * xScale; + + if (renderFlags & Game::TEXT_RENDERFLAG_PADDING) + xa += xScale * padding; + + count++; + maxLengthRemaining--; + } + + if(renderFlags & Game::TEXT_RENDERFLAG_CURSOR && count == cursorPos) + { + RotateXY(cosAngle, sinAngle, startX, startY, xa, xy, &xRot, &yRot); + Game::RB_DrawCursor(material, cursorLetter, xRot, yRot, sinAngle, cosAngle, font, xScale, yScale, color.packed); + } + } + } + + int TextRenderer::R_TextWidth_Hk(const char* text, int maxChars, Game::Font_s* font) + { + auto lineWidth = 0; + auto maxWidth = 0; + + if (maxChars <= 0) + maxChars = std::numeric_limits::max(); + + if (text == nullptr) + return 0; + + auto count = 0; + while (text && *text && count < maxChars) + { + const auto letter = Game::SEH_ReadCharFromString(&text, nullptr); + if (letter == '\r' || letter == '\n') + { + lineWidth = 0; + } + else + { + if (letter == '^' && text) + { + if (*text >= COLOR_FIRST_CHAR && *text <= COLOR_LAST_CHAR) + { + text++; + continue; + } + + if (*text >= '\x01' && *text <= '\x02' && text[1] != '\0' && text[2] != '\0' && text[3] != '\0') + { + const auto width = text[1]; + const auto materialNameLength = text[3]; + + // This is how the game calculates width and height. Probably some 1 byte floating point number. + // Details to be investigated if necessary. + const auto v9 = font->pixelHeight * (width - 16) + 16; + const auto w = ((((v9 >> 24) & 0x1F) + v9) >> 5); + + lineWidth += w; + if (lineWidth > maxWidth) + maxWidth = lineWidth; + + text += 4; + for (auto currentLength = 0; currentLength < materialNameLength && *text; currentLength++) + text++; + continue; + } + } + + if (letter == FONT_ICON_SEPARATOR_CHARACTER) + { + FontIconInfo fontIconInfo{}; + const char* fontIconEnd = text; + if (IsFontIcon(fontIconEnd, fontIconInfo)) + { + lineWidth += static_cast(GetFontIconWidth(fontIconInfo, font, 1.0f)); + if (lineWidth > maxWidth) + maxWidth = lineWidth; + text = fontIconEnd; + continue; + } + } + + lineWidth += R_GetCharacterGlyph(font, letter)->dx; + if (lineWidth > maxWidth) + maxWidth = lineWidth; + count++; + } + } + + return maxWidth; + } + + unsigned int TextRenderer::ColorIndex(const char index) + { + auto result = index - '0'; + if (static_cast(result) >= TEXT_COLOR_COUNT || result < 0) result = 7; + return result; + } + + void TextRenderer::StripColors(const char* in, char* out, size_t max) + { + if (!in || !out) return; + + max--; + size_t current = 0; + while (*in != 0 && current < max) + { + const char index = *(in + 1); + if (*in == '^' && (ColorIndex(index) != 7 || index == '7')) + { + ++in; + } + else + { + *out = *in; + ++out; + ++current; + } + + ++in; + } + *out = '\0'; + } + + std::string TextRenderer::StripColors(const std::string& in) + { + char buffer[1000] = { 0 }; // Should be more than enough + StripColors(in.data(), buffer, sizeof(buffer)); + return std::string(buffer); + } + + void TextRenderer::StripMaterialTextIcons(const char* in, char* out, size_t max) + { + if (!in || !out) return; + + max--; + size_t current = 0; + while (*in != 0 && current < max) + { + if (*in == '^' && (in[1] == '\x01' || in[1] == '\x02')) + { + in += 2; + + if (*in) // width + in++; + if (*in) // height + in++; + + if(*in) // material name length + material name characters + { + const auto materialNameLength = *in; + in++; + for(auto i = 0; i < materialNameLength; i++) + { + if (*in) + in++; + } + } + } + else + { + *out = *in; + ++out; + ++current; + ++in; + } + + } + *out = '\0'; + } + + std::string TextRenderer::StripMaterialTextIcons(const std::string& in) + { + char buffer[1000] = { 0 }; // Should be more than enough + StripAllTextIcons(in.data(), buffer, sizeof(buffer)); + return std::string(buffer); + } + + void TextRenderer::StripAllTextIcons(const char* in, char* out, size_t max) + { + if (!in || !out) return; + + max--; + size_t current = 0; + while (*in != 0 && current < max) + { + if (*in == '^' && (in[1] == '\x01' || in[1] == '\x02')) + { + in += 2; + + if (*in) // width + in++; + if (*in) // height + in++; + + if(*in) // material name length + material name characters + { + const auto materialNameLength = *in; + in++; + for(auto i = 0; i < materialNameLength; i++) + { + if (*in) + in++; + } + } + + continue; + } + + if(*in == FONT_ICON_SEPARATOR_CHARACTER) + { + const auto* fontIconEndPos = &in[1]; + FontIconInfo fontIcon{}; + if(IsFontIcon(fontIconEndPos, fontIcon)) + { + in = fontIconEndPos; + continue; + } + } + + *out = *in; + ++out; + ++current; + ++in; + } + *out = '\0'; + } + + std::string TextRenderer::StripAllTextIcons(const std::string& in) + { + char buffer[1000] = { 0 }; // Should be more than enough + StripAllTextIcons(in.data(), buffer, sizeof(buffer)); + return std::string(buffer); + } + + int TextRenderer::SEH_PrintStrlenWithCursor(const char* string, const Game::field_t* field) + { + if (!string) + return 0; + + const auto cursorPos = field->cursor; + auto len = 0; + auto lenWithInvisibleTail = 0; + auto count = 0; + const auto* curText = string; + while(*curText) + { + const auto c = Game::SEH_ReadCharFromString(&curText, nullptr); + lenWithInvisibleTail = len; + + if (c == '^' && *curText >= COLOR_FIRST_CHAR && *curText <= COLOR_LAST_CHAR && !(cursorPos > count && cursorPos < count + 2)) + { + curText++; + count++; + } + else if(c != '\r' && c != '\n') + { + len++; + } + + count++; + lenWithInvisibleTail++; + } + + return lenWithInvisibleTail; + } + + __declspec(naked) void TextRenderer::Field_AdjustScroll_PrintLen_Stub() + { + __asm + { + push eax + pushad + + push esi + push [esp + 0x8 + 0x24] + call SEH_PrintStrlenWithCursor + add esp, 0x8 + mov [esp + 0x20], eax + + popad + pop eax + ret + } + } + + void TextRenderer::PatchColorLimit(const char limit) + { + Utils::Hook::Set(0x535629, limit); // DrawText2d + Utils::Hook::Set(0x4C1BE4, limit); // SEH_PrintStrlen + Utils::Hook::Set(0x4863DD, limit); // No idea :P + Utils::Hook::Set(0x486429, limit); // No idea :P + Utils::Hook::Set(0x49A5A8, limit); // No idea :P + Utils::Hook::Set(0x505721, limit); // R_TextWidth + Utils::Hook::Set(0x505801, limit); // No idea :P + Utils::Hook::Set(0x50597F, limit); // No idea :P + Utils::Hook::Set(0x5815DB, limit); // No idea :P + Utils::Hook::Set(0x592ED0, limit); // No idea :P + Utils::Hook::Set(0x5A2E2E, limit); // No idea :P + + Utils::Hook::Set(0x5A2733, static_cast(ColorIndexForChar(limit))); // No idea :P + } + + // Patches team overhead normally + bool TextRenderer::Dvar_GetUnpackedColorByName(const char* name, float* expandedColor) + { + if (r_colorBlind.get()) + { + const auto str = std::string(name); + if (str == "g_TeamColor_EnemyTeam") + { + // Dvar_GetUnpackedColor + const auto* colorblindEnemy = g_ColorBlind_EnemyTeam->current.color; + expandedColor[0] = static_cast(colorblindEnemy[0]) / 255.0f; + expandedColor[1] = static_cast(colorblindEnemy[1]) / 255.0f; + expandedColor[2] = static_cast(colorblindEnemy[2]) / 255.0f; + expandedColor[3] = static_cast(colorblindEnemy[3]) / 255.0f; + return false; + } + else if (str == "g_TeamColor_MyTeam") + { + // Dvar_GetUnpackedColor + const auto* colorblindAlly = g_ColorBlind_MyTeam->current.color; + expandedColor[0] = static_cast(colorblindAlly[0]) / 255.0f; + expandedColor[1] = static_cast(colorblindAlly[1]) / 255.0f; + expandedColor[2] = static_cast(colorblindAlly[2]) / 255.0f; + expandedColor[3] = static_cast(colorblindAlly[3]) / 255.0f; + return false; + } + } + + return true; + } + + __declspec(naked) void TextRenderer::GetUnpackedColorByNameStub() + { + __asm + { + push[esp + 8h] + push[esp + 8h] + call TextRenderer::Dvar_GetUnpackedColorByName + add esp, 8h + + test al, al + jnz continue + + retn + + continue: + push edi + mov edi, [esp + 8h] + push 406535h + retn + } + } + + void TextRenderer::UpdateColorTable() + { + if (cg_newColors.get()) + currentColorTable = &colorTableNew; + else + currentColorTable = &colorTableDefault; + + (*currentColorTable)[TEXT_COLOR_AXIS] = *reinterpret_cast(0x66E5F70); + (*currentColorTable)[TEXT_COLOR_ALLIES] = *reinterpret_cast(0x66E5F74); + (*currentColorTable)[TEXT_COLOR_RAINBOW] = HsvToRgb({ static_cast((Game::Sys_Milliseconds() / 200) % 256), 255,255 }); + (*currentColorTable)[TEXT_COLOR_SERVER] = sv_customTextColor->current.unsignedInt; + } + + void TextRenderer::InitFontIconStrings() + { + stringHintAutoComplete.Format("TAB"); + stringHintModifier.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_SEPARATOR_CHARACTER)); + stringListHeader.Cache(); + stringListFlipHorizontal.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_FLIP_HORIZONTALLY)); + stringListFlipVertical.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_FLIP_VERTICALLY)); + stringListBig.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_BIG)); + } + + void TextRenderer::InitFontIcons() + { + InitFontIconStrings(); + + fontIconList.clear(); + fontIconLookup.clear(); + + const auto fontIconTable = Game::DB_FindXAssetHeader(Game::ASSET_TYPE_STRINGTABLE, "mp/fonticons.csv").stringTable; + + if(fontIconTable->columnCount < 2 || fontIconTable->rowCount <= 0) + { + Logger::Print("^1Failed to load font icon table\n"); + return; + } + + fontIconList.reserve(fontIconTable->rowCount); + for(auto rowIndex = 0; rowIndex < fontIconTable->rowCount; rowIndex++) + { + const auto* columns = &fontIconTable->values[rowIndex * fontIconTable->columnCount]; + + if(columns[0].string == nullptr || columns[1].string == nullptr) + continue; + + if (columns[0].string[0] == '\0' || columns[1].string[1] == '\0') + continue; + + if (columns[0].string[0] == '#') + continue; + + FontIconTableEntry entry + { + columns[0].string, + columns[1].string, + nullptr + }; + + fontIconList.emplace_back(entry); + fontIconLookup.emplace(std::make_pair(entry.iconName, entry)); + } + + std::sort(fontIconList.begin(), fontIconList.end(), [](const FontIconTableEntry& a, const FontIconTableEntry& b) + { + return a.iconName < b.iconName; + }); + } + + void TextRenderer::UI_Init_Hk(const int localClientNum) + { + // Call original method + Utils::Hook::Call(0x4A57D0)(localClientNum); + + InitFontIcons(); + } + + TextRenderer::TextRenderer() + { + currentColorTable = &colorTableDefault; + + cg_newColors = Dvar::Register("cg_newColors", true, Game::dvar_flag::DVAR_FLAG_SAVED, "Use Warfare 2 color code style."); + cg_fontIconAutocomplete = Dvar::Register("cg_fontIconAutocomplete", true, Game::dvar_flag::DVAR_FLAG_SAVED, "Show autocomplete for fonticons when typing."); + cg_fontIconAutocompleteHint = Dvar::Register("cg_fontIconAutocompleteHint", true, Game::dvar_flag::DVAR_FLAG_SAVED, "Show hint text in autocomplete for fonticons."); + sv_customTextColor = Game::Dvar_RegisterColor("sv_customTextColor", 1, 0.7f, 0, 1, Game::dvar_flag::DVAR_FLAG_REPLICATED, "Color for the extended color code."); + + // Initialize font icons when initializing UI + Utils::Hook(0x4B5422, UI_Init_Hk, HOOK_CALL).install()->quick(); + + // Replace vanilla text drawing function with a reimplementation with extensions + Utils::Hook(0x535410, DrawText2D, HOOK_JUMP).install()->quick(); + + // Consider material text icons and font icons when calculating text width + Utils::Hook(0x5056C0, R_TextWidth_Hk, HOOK_JUMP).install()->quick(); + + // Patch ColorIndex + Utils::Hook(0x417770, ColorIndex, HOOK_JUMP).install()->quick(); + + // Add a colorblind mode for team colors + r_colorBlind = Dvar::Register("r_colorBlind", false, Game::dvar_flag::DVAR_FLAG_SAVED, "Use color-blindness-friendly colors"); + // A dark red + g_ColorBlind_EnemyTeam = Game::Dvar_RegisterColor("g_ColorBlind_EnemyTeam", 0.659f, 0.088f, 0.145f, 1, Game::dvar_flag::DVAR_FLAG_SAVED, "Enemy team color for colorblind mode"); + // A bright yellow + g_ColorBlind_MyTeam = Game::Dvar_RegisterColor("g_ColorBlind_MyTeam", 1, 0.859f, 0.125f, 1, Game::dvar_flag::DVAR_FLAG_SAVED, "Ally team color for colorblind mode"); + + // Replace team colors with colorblind team colors when colorblind is enabled + Utils::Hook(0x406530, GetUnpackedColorByNameStub, HOOK_JUMP).install()->quick(); + + // Consider the cursor being inside the color escape sequence when getting the print length for a field + Utils::Hook(0x488CBD, Field_AdjustScroll_PrintLen_Stub, HOOK_CALL).install()->quick(); + + // Draw fonticon autocompletion for say field + Utils::Hook(0x4CA1BD, Field_Draw_Say, HOOK_CALL).install()->quick(); + + // Draw fonticon autocompletion for console field + Utils::Hook(0x5A50A5, Con_DrawInput_Hk, HOOK_CALL).install()->quick(); + Utils::Hook(0x5A50BB, Con_DrawInput_Hk, HOOK_CALL).install()->quick(); + + // Handle key inputs for console and chat + Utils::Hook(0x4F685C, Console_Key_Hk, HOOK_CALL).install()->quick(); + Utils::Hook(0x4F6694, Message_Key_Stub, HOOK_CALL).install()->quick(); + Utils::Hook(0x4F684C, Message_Key_Stub, HOOK_CALL).install()->quick(); + + PatchColorLimit(COLOR_LAST_CHAR); + } +} diff --git a/src/Components/Modules/TextRenderer.hpp b/src/Components/Modules/TextRenderer.hpp new file mode 100644 index 00000000..2ffe44f2 --- /dev/null +++ b/src/Components/Modules/TextRenderer.hpp @@ -0,0 +1,262 @@ +#pragma once + +namespace Components +{ + enum TextColor + { + TEXT_COLOR_BLACK = 0, + TEXT_COLOR_RED = 1, + TEXT_COLOR_GREEN = 2, + TEXT_COLOR_YELLOW = 3, + TEXT_COLOR_BLUE = 4, + TEXT_COLOR_LIGHT_BLUE = 5, + TEXT_COLOR_PINK = 6, + TEXT_COLOR_DEFAULT = 7, + TEXT_COLOR_AXIS = 8, + TEXT_COLOR_ALLIES = 9, + TEXT_COLOR_RAINBOW = 10, + TEXT_COLOR_SERVER = 11, // using that color in infostrings (e.g. your name) fails, ';' is an illegal character! + + TEXT_COLOR_COUNT + }; + + constexpr unsigned int ColorRgba(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) + { + return (r) | (g << 8) | (b << 16) | (a << 24); + } + + constexpr unsigned int ColorRgb(const uint8_t r, const uint8_t g, const uint8_t b) + { + return ColorRgba(r, g, b, 0xFF); + } + + constexpr char CharForColorIndex(const int colorIndex) + { + return static_cast('0' + colorIndex); + } + + constexpr int ColorIndexForChar(const char colorChar) + { + return colorChar - '0'; + } + + class TextRenderer : public Component + { + static constexpr auto STRING_BUFFER_SIZE_BIG = 1024; + static constexpr auto STRING_BUFFER_SIZE_SMALL = 128; + + static constexpr auto REFERENCE_SEARCH_START_WITH = "FONT_ICON_SEARCH_START_WITH"; + static constexpr auto REFERENCE_HINT_AUTO_COMPLETE = "FONT_ICON_HINT_AUTO_COMPLETE"; + static constexpr auto REFERENCE_HINT_MODIFIER = "FONT_ICON_HINT_MODIFIER"; + static constexpr auto REFERENCE_MODIFIER_LIST_HEADER = "FONT_ICON_MODIFIER_LIST_HEADER"; + static constexpr auto REFERENCE_MODIFIER_LIST_FLIP_HORIZONTAL = "FONT_ICON_MODIFIER_LIST_FLIP_HORIZONTAL"; + static constexpr auto REFERENCE_MODIFIER_LIST_FLIP_VERTICAL = "FONT_ICON_MODIFIER_LIST_FLIP_VERTICAL"; + static constexpr auto REFERENCE_MODIFIER_LIST_BIG = "FONT_ICON_MODIFIER_LIST_BIG"; + + static constexpr unsigned MY_ALTCOLOR_TWO = 0x0DCE6FFE6; + static constexpr unsigned COLOR_MAP_HASH = 0xA0AB1041; + static constexpr auto FONT_ICON_AUTOCOMPLETE_BOX_PADDING = 6.0f; + static constexpr auto FONT_ICON_AUTOCOMPLETE_BOX_BORDER = 2.0f; + static constexpr auto FONT_ICON_AUTOCOMPLETE_COL_SPACING = 12.0f; + static constexpr auto FONT_ICON_AUTOCOMPLETE_ARROW_SIZE = 12.0f; + static constexpr float MY_OFFSETS[4][2] + { + {-1.0f, -1.0f}, + {-1.0f, 1.0f}, + {1.0f, -1.0f}, + {1.0f, 1.0f}, + }; + static constexpr float WHITE_COLOR[4] + { + 1.0f, + 1.0f, + 1.0f, + 1.0f + }; + static constexpr float TEXT_COLOR[4] + { + 1.0f, + 1.0f, + 0.8f, + 1.0f + }; + static constexpr float HINT_COLOR[4] + { + 0.6f, + 0.6f, + 0.6f, + 1.0f + }; + + public: + static constexpr char FONT_ICON_SEPARATOR_CHARACTER = ':'; + static constexpr char FONT_ICON_MODIFIER_SEPARATOR_CHARACTER = '+'; + static constexpr char FONT_ICON_MODIFIER_FLIP_HORIZONTALLY = 'h'; + static constexpr char FONT_ICON_MODIFIER_FLIP_VERTICALLY = 'v'; + static constexpr char FONT_ICON_MODIFIER_BIG = 'b'; + + static constexpr char COLOR_FIRST_CHAR = '0'; + static constexpr char COLOR_LAST_CHAR = CharForColorIndex(TEXT_COLOR_COUNT - 1); + + enum FontIconAutocompleteInstance : unsigned + { + FONT_ICON_ACI_CONSOLE, + FONT_ICON_ACI_CHAT, + + FONT_ICON_ACI_COUNT + }; + + struct FontIconInfo + { + Game::Material* material; + bool flipHorizontal; + bool flipVertical; + bool big; + }; + + private: + struct FontIconTableEntry + { + std::string iconName; + std::string materialName; + Game::Material* material; + }; + + struct HsvColor + { + unsigned char h; + unsigned char s; + unsigned char v; + }; + + class FontIconAutocompleteResult + { + public: + std::string fontIconName; + std::string materialName; + }; + + class BufferedLocalizedString + { + public: + BufferedLocalizedString(const char* reference, size_t bufferSize); + void Cache(); + const char* Format(const char* value); + const char* GetString() const; + int GetWidth(FontIconAutocompleteInstance autocompleteInstance, Game::Font_s* font); + + private: + const char* stringReference; + std::unique_ptr stringBuffer; + size_t stringBufferSize; + int stringWidth[FONT_ICON_ACI_COUNT]; + }; + + class FontIconAutocompleteContext + { + public: + static constexpr auto MAX_RESULTS = 10; + + bool autocompleteActive; + bool inModifiers; + bool userClosed; + unsigned int lastHash; + std::string lastQuery; + FontIconAutocompleteResult results[MAX_RESULTS]; + size_t resultCount; + bool hasMoreResults; + size_t resultOffset; + size_t lastResultOffset; + size_t selectedOffset; + float maxFontIconWidth; + float maxMaterialNameWidth; + BufferedLocalizedString stringSearchStartWith; + + FontIconAutocompleteContext(); + }; + + static unsigned colorTableDefault[TEXT_COLOR_COUNT]; + static unsigned colorTableNew[TEXT_COLOR_COUNT]; + static unsigned(*currentColorTable)[TEXT_COLOR_COUNT]; + static FontIconAutocompleteContext autocompleteContextArray[FONT_ICON_ACI_COUNT]; + static std::map fontIconLookup; + static std::vector fontIconList; + + static BufferedLocalizedString stringHintAutoComplete; + static BufferedLocalizedString stringHintModifier; + static BufferedLocalizedString stringListHeader; + static BufferedLocalizedString stringListFlipHorizontal; + static BufferedLocalizedString stringListFlipVertical; + static BufferedLocalizedString stringListBig; + + static Dvar::Var cg_newColors; + static Dvar::Var cg_fontIconAutocomplete; + static Dvar::Var cg_fontIconAutocompleteHint; + static Game::dvar_t* sv_customTextColor; + static Dvar::Var r_colorBlind; + static Game::dvar_t* g_ColorBlind_MyTeam; + static Game::dvar_t* g_ColorBlind_EnemyTeam; + static Game::dvar_t** con_inputBoxColor; + + public: + static void DrawText2D(const char* text, float x, float y, Game::Font_s* font, float xScale, float yScale, float sinAngle, float cosAngle, Game::GfxColor color, int maxLength, int renderFlags, int cursorPos, char cursorLetter, float padding, Game::GfxColor glowForcedColor, int fxBirthTime, int fxLetterTime, int fxDecayStartTime, int fxDecayDuration, Game::Material* fxMaterial, Game::Material* fxMaterialGlow); + static int R_TextWidth_Hk(const char* text, int maxChars, Game::Font_s* font); + static unsigned int ColorIndex(char index); + static void StripColors(const char* in, char* out, size_t max); + static std::string StripColors(const std::string& in); + static void StripMaterialTextIcons(const char* in, char* out, size_t max); + static std::string StripMaterialTextIcons(const std::string& in); + static void StripAllTextIcons(const char* in, char* out, size_t max); + static std::string StripAllTextIcons(const std::string& in); + + static bool IsFontIcon(const char*& text, FontIconInfo& fontIcon); + static float GetNormalizedFontIconWidth(const FontIconInfo& fontIcon); + static float GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, float xScale); + + static bool HandleFontIconAutocompleteKey(int localClientNum, FontIconAutocompleteInstance autocompleteInstance, int key); + + TextRenderer(); + + private: + static unsigned HsvToRgb(HsvColor hsv); + + static void DrawAutocompleteBox(const FontIconAutocompleteContext& context, float x, float y, float w, float h, const float* color); + static void DrawAutocompleteModifiers(FontIconAutocompleteInstance instance, float x, float y, Game::Font_s* font, float textXScale, float textYScale); + static void DrawAutocompleteResults(FontIconAutocompleteInstance instance, float x, float y, Game::Font_s* font, float textXScale, float textYScale); + static void DrawAutocomplete(FontIconAutocompleteInstance instance, float x, float y, Game::Font_s* font, float textXScale, float textYScale); + static void UpdateAutocompleteContextResults(FontIconAutocompleteContext& context, Game::Font_s* font, float textXScale); + static void UpdateAutocompleteContext(FontIconAutocompleteContext& context, const Game::field_t* edit, Game::Font_s* font, const float textXScale); + static void Field_Draw_Say(int localClientNum, Game::field_t* edit, int x, int y, int horzAlign, int vertAlign); + static void Con_DrawInput_Hk(int localClientNum); + + static void AutocompleteUp(FontIconAutocompleteContext& context); + static void AutocompleteDown(FontIconAutocompleteContext& context); + static void AutocompleteFill(const FontIconAutocompleteContext& context, Game::ScreenPlacement* scrPlace, Game::field_t* edit, bool closeFontIcon); + static bool AutocompleteHandleKeyDown(FontIconAutocompleteContext& context, int key, Game::ScreenPlacement* scrPlace, Game::field_t* edit); + static void Console_Key_Hk(int localClientNum, int key); + static bool ChatHandleKeyDown(int localClientNum, int key); + static void Message_Key_Stub(); + + static int SEH_PrintStrlenWithCursor(const char* string, const Game::field_t* field); + static void Field_AdjustScroll_PrintLen_Stub(); + + static void PatchColorLimit(char limit); + static bool Dvar_GetUnpackedColorByName(const char* name, float* expandedColor); + static void GetUnpackedColorByNameStub(); + + static Game::GfxImage* GetFontIconColorMap(const Game::Material* fontIconMaterial); + static float DrawFontIcon(const FontIconInfo& fontIcon, float x, float y, float sinAngle, float cosAngle, const Game::Font_s* font, float xScale, float yScale, unsigned color); + + static float GetMonospaceWidth(Game::Font_s* font, int rendererFlags); + static void GlowColor(Game::GfxColor* result, Game::GfxColor baseColor, Game::GfxColor forcedGlowColor, int renderFlags); + static unsigned R_FontGetRandomLetter(int seed); + static void DrawTextFxExtraCharacter(Game::Material* material, int charIndex, float x, float y, float w, float h, float sinAngle, float cosAngle, unsigned color); + static float DrawHudIcon(const char*& text, float x, float y, float sinAngle, float cosAngle, const Game::Font_s* font, float xScale, float yScale, unsigned color); + static void RotateXY(float cosAngle, float sinAngle, float pivotX, float pivotY, float x, float y, float* outX, float* outY); + static void UpdateColorTable(); + + static void InitFontIconStrings(); + static void InitFontIcons(); + static void UI_Init_Hk(int localClientNum); + }; +} diff --git a/src/Game/Functions.cpp b/src/Game/Functions.cpp index 6692a23d..462c6d9c 100644 --- a/src/Game/Functions.cpp +++ b/src/Game/Functions.cpp @@ -75,6 +75,7 @@ namespace Game Con_DrawMiniConsole_t Con_DrawMiniConsole = Con_DrawMiniConsole_t(0x464F30); Con_DrawSolidConsole_t Con_DrawSolidConsole = Con_DrawSolidConsole_t(0x5A5040); + Con_CancelAutoComplete_t Con_CancelAutoComplete = Con_CancelAutoComplete_t(0x435580); DB_AllocStreamPos_t DB_AllocStreamPos = DB_AllocStreamPos_t(0x418380); DB_PushStreamPos_t DB_PushStreamPos = DB_PushStreamPos_t(0x458A20); @@ -155,6 +156,7 @@ namespace Game Info_ValueForKey_t Info_ValueForKey = Info_ValueForKey_t(0x47C820); Key_SetCatcher_t Key_SetCatcher = Key_SetCatcher_t(0x43BD00); + Key_RemoveCatcher_t Key_RemoveCatcher = Key_RemoveCatcher_t(0x408260); Key_IsKeyCatcherActive_t Key_IsKeyCatcherActive = Key_IsKeyCatcherActive_t(0x4DA010); LargeLocalInit_t LargeLocalInit = LargeLocalInit_t(0x4A62A0); @@ -340,10 +342,14 @@ namespace Game UI_UpdateArenas_t UI_UpdateArenas = UI_UpdateArenas_t(0x4A95B0); UI_SortArenas_t UI_SortArenas = UI_SortArenas_t(0x630AE0); UI_DrawHandlePic_t UI_DrawHandlePic = UI_DrawHandlePic_t(0x4D0EA0); - UI_GetContext_t UI_GetContext = UI_GetContext_t(0x4F8940); + ScrPlace_GetActivePlacement_t ScrPlace_GetActivePlacement = ScrPlace_GetActivePlacement_t(0x4F8940); UI_TextWidth_t UI_TextWidth = UI_TextWidth_t(0x6315C0); UI_DrawText_t UI_DrawText = UI_DrawText_t(0x49C0D0); + UI_GetFontHandle_t UI_GetFontHandle = UI_GetFontHandle_t(0x4AEA60); + ScrPlace_ApplyRect_t ScrPlace_ApplyRect = ScrPlace_ApplyRect_t(0x454E20); UI_KeyEvent_t UI_KeyEvent = UI_KeyEvent_t(0x4970F0); + UI_SafeTranslateString_t UI_SafeTranslateString = UI_SafeTranslateString_t(0x4F1700); + UI_ReplaceConversions_t UI_ReplaceConversions = UI_ReplaceConversions_t(0x4E9740); Win_GetLanguage_t Win_GetLanguage = Win_GetLanguage_t(0x45CBA0); @@ -353,6 +359,18 @@ namespace Game unzClose_t unzClose = unzClose_t(0x41BF20); + RB_DrawCursor_t RB_DrawCursor = RB_DrawCursor_t(0x534EA0); + + R_NormalizedTextScale_t R_NormalizedTextScale = R_NormalizedTextScale_t(0x5056A0); + + Material_Process2DTextureCoordsForAtlasing_t Material_Process2DTextureCoordsForAtlasing = Material_Process2DTextureCoordsForAtlasing_t(0x506090); + + Byte4PackRgba_t Byte4PackRgba = Byte4PackRgba_t(0x4FE910); + RandWithSeed_t RandWithSeed = RandWithSeed_t(0x495580); + GetDecayingLetterInfo_t GetDecayingLetterInfo = GetDecayingLetterInfo_t(0x5351C0); + + Field_Draw_t Field_Draw = Field_Draw_t(0x4F5B40); + Field_AdjustScroll_t Field_AdjustScroll = Field_AdjustScroll_t(0x488C10); AimAssist_ApplyAutoMelee_t AimAssist_ApplyAutoMelee = AimAssist_ApplyAutoMelee_t(0x56A360); XAssetHeader* DB_XAssetPool = reinterpret_cast(0x7998A8); @@ -448,11 +466,19 @@ namespace Game GfxScene* scene = reinterpret_cast(0x6944914); - clientActive_t* clients = reinterpret_cast(0xB2C698); + ConDrawInputGlob* conDrawInputGlob = reinterpret_cast(0x9FD6F8); + field_t* g_consoleField = reinterpret_cast(0xA1B6B0); clientStatic_t* cls = reinterpret_cast(0xA7FE90); + sharedUiInfo_t* sharedUiInfo = reinterpret_cast(0x62E4B78); + ScreenPlacement* scrPlaceFull = reinterpret_cast(0x10843F0); + ScreenPlacement* scrPlaceView = reinterpret_cast(0x1084378); + + clientActive_t* clients = reinterpret_cast(0xB2C698); + cg_s* cgArray = reinterpret_cast(0x7F0F78); + cgs_t* cgsArray = reinterpret_cast(0x7ED3B8); PlayerKeyState* playerKeys = reinterpret_cast(0xA1B7D0); kbutton_t* playersKb = reinterpret_cast(0xA1A9A8); @@ -584,37 +610,32 @@ namespace Game while (lock && *reinterpret_cast(0x16B8A58)) std::this_thread::sleep_for(1ms); - unsigned int index = 0; - do + const auto pool = Components::Maps::GetAssetEntryPool(); + for(auto hash = 0; hash < 37000; hash++) { - unsigned short hashIndex = db_hashTable[index]; - if (hashIndex) + auto hashIndex = db_hashTable[hash]; + while(hashIndex) { - do + auto* assetEntry = &pool[hashIndex]; + + if(assetEntry->asset.type == type) { - XAssetEntry* asset = &Components::Maps::GetAssetEntryPool()[hashIndex]; - hashIndex = asset->nextHash; - if (asset->asset.type == type) + callback(assetEntry); + if (overrides) { - callback(asset); - if (overrides) + auto overrideIndex = assetEntry->nextOverride; + while (overrideIndex) { - unsigned short overrideIndex = asset->nextOverride; - if (asset->nextOverride) - { - do - { - asset = &Components::Maps::GetAssetEntryPool()[overrideIndex]; - callback(asset); - overrideIndex = asset->nextOverride; - } while (overrideIndex); - } + auto* overrideEntry = &pool[overrideIndex]; + callback(overrideEntry); + overrideIndex = overrideEntry->nextOverride; } } - } while (hashIndex); + } + + hashIndex = assetEntry->nextHash; } - ++index; - } while (index < 74000); + } if(lock) InterlockedDecrement(lockVar); } @@ -643,6 +664,20 @@ namespace Game return hash; } + unsigned int R_HashString(const char* string, size_t maxLen) + { + unsigned int hash = 0; + + while (*string && maxLen > 0) + { + hash = (*string | 0x20) ^ (33 * hash); + ++string; + maxLen--; + } + + return hash; + } + void SV_KickClientError(client_t* client, const std::string& reason) { if (client->state < 5) @@ -1315,22 +1350,123 @@ namespace Game } } - __declspec(naked) Glyph* R_GetCharacterGlyph(Font_s* /*font */, unsigned int /*letter*/) + __declspec(naked) Glyph* R_GetCharacterGlyph(Font_s* /*font*/, unsigned int /*letter*/) { - __asm + __asm { push eax pushad - mov edi, [esp + 0x28 + 4] - push [esp + 0x24 + 4] + + mov edi, [esp + 0x8 + 0x24] // letter + push [esp + 0x4 + 0x24] // font mov eax, 0x5055C0 call eax - add esp,0x4 + add esp, 4 + mov [esp + 0x20], eax + + popad + pop eax + ret + } + } + + __declspec(naked) bool SetupPulseFXVars(const char* /*text*/, int /*maxLength*/, int /*fxBirthTime*/, int /*fxLetterTime*/, int /*fxDecayStartTime*/, int /*fxDecayDuration*/, bool* /*resultDrawRandChar*/, int* /*resultRandSeed*/, int* /*resultMaxLength*/, bool* /*resultDecaying*/, int* /*resultDecayTimeElapsed*/) + { + __asm + { + push eax + pushad + + mov eax, [esp + 0x08 + 0x24] // maxLength + push [esp + 0x2C + 0x24] // resultDecayTimeElapsed + push [esp + 0x2C + 0x24] // resultDecaying + push [esp + 0x2C + 0x24] // resultMaxLength + push [esp + 0x2C + 0x24] // resultRandSeed + push [esp + 0x2C + 0x24] // resultDrawRandChar + push [esp + 0x2C + 0x24] // fxDecayDuration + push [esp + 0x2C + 0x24] // fxDecayStartTime + push [esp + 0x2C + 0x24] // fxLetterTime + push [esp + 0x2C + 0x24] // fxBirthTime + push [esp + 0x28 + 0x24] // text + mov ebx, 0x535050 + call ebx + add esp, 0x28 mov [esp + 0x20],eax popad pop eax - retn + ret + } + } + + __declspec(naked) void RB_DrawChar(Material* /*material*/, float /*x*/, float /*y*/, float /*w*/, float /*h*/, float /*sinAngle*/, float /*cosAngle*/, Glyph* /*glyph*/, unsigned int /*color*/) + { + __asm + { + pushad + + mov eax, [esp + 0x4 + 0x20] // material + mov edx, [esp + 0x20 + 0x20] // glyph + push [esp + 0x24 + 0x20] // color + push [esp + 0x20 + 0x20] // cosAngle + push [esp + 0x20 + 0x20] // sinAngle + push [esp + 0x20 + 0x20] // h + push [esp + 0x20 + 0x20] // w + push [esp + 0x20 + 0x20] // y + push [esp + 0x20 + 0x20] // x + + mov ecx, 0x534E20 + call ecx + add esp, 0x1C + + popad + ret + } + } + + __declspec(naked) void RB_DrawStretchPicRotate(Material* /*material*/, float /*x*/, float /*y*/, float /*w*/, float /*h*/, float /*s0*/, float /*t0*/, float /*s1*/, float /*t1*/, float /*sinAngle*/, float /*cosAngle*/, unsigned int /*color*/) + { + __asm + { + pushad + + mov eax, [esp + 0x4 + 0x20] // material + push [esp + 0x30 + 0x20] // color + push [esp + 0x30 + 0x20] // cosAngle + push [esp + 0x30 + 0x20] // sinAngle + push [esp + 0x30 + 0x20] // t1 + push [esp + 0x30 + 0x20] // s1 + push [esp + 0x30 + 0x20] // t0 + push [esp + 0x30 + 0x20] // s0 + push [esp + 0x30 + 0x20] // h + push [esp + 0x30 + 0x20] // w + push [esp + 0x30 + 0x20] // y + push [esp + 0x30 + 0x20] // x + mov ebx, 0x5310F0 + call ebx + add esp, 0x2C + + popad + ret + } + } + + __declspec(naked) char ModulateByteColors(char /*colorA*/, char /*colorB*/) + { + __asm + { + push eax + pushad + + mov eax, [esp + 0x4 + 0x24] // colorA + mov ecx, [esp + 0x8 + 0x24] // colorB + mov ebx, 0x5353C0 + call ebx + mov [esp + 0x20], eax + + popad + pop eax + ret } } @@ -1356,6 +1492,5 @@ namespace Game } } - #pragma optimize("", on) } diff --git a/src/Game/Functions.hpp b/src/Game/Functions.hpp index 05164910..2569dbcf 100644 --- a/src/Game/Functions.hpp +++ b/src/Game/Functions.hpp @@ -157,6 +157,9 @@ namespace Game typedef void (__cdecl * Con_DrawSolidConsole_t)(); extern Con_DrawSolidConsole_t Con_DrawSolidConsole; + typedef bool(__cdecl * Con_CancelAutoComplete_t)(); + extern Con_CancelAutoComplete_t Con_CancelAutoComplete; + typedef char *(__cdecl *DB_AllocStreamPos_t)(int alignment); extern DB_AllocStreamPos_t DB_AllocStreamPos; @@ -375,6 +378,9 @@ namespace Game typedef void(__cdecl * Key_SetCatcher_t)(int localClientNum, int catcher); extern Key_SetCatcher_t Key_SetCatcher; + typedef void(__cdecl * Key_RemoveCatcher_t)(int localClientNum, int andMask); + extern Key_RemoveCatcher_t Key_RemoveCatcher; + typedef bool(__cdecl * Key_IsKeyCatcherActive_t)(int localClientNum, int catcher); extern Key_IsKeyCatcherActive_t Key_IsKeyCatcherActive; @@ -459,6 +465,12 @@ namespace Game typedef bool(__cdecl * UI_KeyEvent_t)(int clientNum, int key, int down); extern UI_KeyEvent_t UI_KeyEvent; + typedef const char* (__cdecl * UI_SafeTranslateString_t)(const char* reference); + extern UI_SafeTranslateString_t UI_SafeTranslateString; + + typedef void(__cdecl * UI_ReplaceConversions_t)(const char* sourceString, ConversionArguments* arguments, char* outputString, size_t outputStringSize); + extern UI_ReplaceConversions_t UI_ReplaceConversions; + typedef void(__cdecl * MSG_Init_t)(msg_t *buf, char *data, int length); extern MSG_Init_t MSG_Init; @@ -696,7 +708,7 @@ namespace Game typedef char* (__cdecl * SEH_StringEd_GetString_t)(const char* string); extern SEH_StringEd_GetString_t SEH_StringEd_GetString; - typedef int (__cdecl * SEH_ReadCharFromString_t)(const char** text, int* isTrailingPunctuation); + typedef unsigned int(__cdecl* SEH_ReadCharFromString_t)(const char** text, int* isTrailingPunctuation); extern SEH_ReadCharFromString_t SEH_ReadCharFromString; typedef char* (__cdecl * SL_ConvertToString_t)(unsigned short stringValue); @@ -813,14 +825,20 @@ namespace Game typedef void(__cdecl * UI_DrawHandlePic_t)(/*ScreenPlacement*/void *scrPlace, float x, float y, float w, float h, int horzAlign, int vertAlign, const float *color, Material *material); extern UI_DrawHandlePic_t UI_DrawHandlePic; - typedef void* (__cdecl * UI_GetContext_t)(void*); - extern UI_GetContext_t UI_GetContext; + typedef ScreenPlacement* (__cdecl * ScrPlace_GetActivePlacement_t)(int localClientNum); + extern ScrPlace_GetActivePlacement_t ScrPlace_GetActivePlacement; typedef int(__cdecl * UI_TextWidth_t)(const char *text, int maxChars, Font_s *font, float scale); extern UI_TextWidth_t UI_TextWidth; typedef void(__cdecl * UI_DrawText_t)(void* scrPlace, const char *text, int maxChars, Font_s *font, float x, float y, int horzAlign, int vertAlign, float scale, const float *color, int style); extern UI_DrawText_t UI_DrawText; + + typedef Font_s* (__cdecl* UI_GetFontHandle_t)(ScreenPlacement* scrPlace, int fontEnum, float scale); + extern UI_GetFontHandle_t UI_GetFontHandle; + + typedef void(__cdecl* ScrPlace_ApplyRect_t)(ScreenPlacement* a1, float* x, float* y, float* w, float* h, int horzAlign, int vertAlign); + extern ScrPlace_ApplyRect_t ScrPlace_ApplyRect; typedef const char * (__cdecl * Win_GetLanguage_t)(); extern Win_GetLanguage_t Win_GetLanguage; @@ -836,6 +854,30 @@ namespace Game typedef void(__cdecl * unzClose_t)(void* handle); extern unzClose_t unzClose; + + typedef void(__cdecl* RB_DrawCursor_t)(Material* material, char cursor, float x, float y, float sinAngle, float cosAngle, Font_s* font, float xScale, float yScale, unsigned int color); + extern RB_DrawCursor_t RB_DrawCursor; + + typedef float(__cdecl* R_NormalizedTextScale_t)(Font_s* font, float scale); + extern R_NormalizedTextScale_t R_NormalizedTextScale; + + typedef void(__cdecl * Material_Process2DTextureCoordsForAtlasing_t)(const Material* material, float* s0, float* s1, float* t0, float* t1); + extern Material_Process2DTextureCoordsForAtlasing_t Material_Process2DTextureCoordsForAtlasing; + + typedef void(__cdecl* Byte4PackRgba_t)(const float* from, char* to); + extern Byte4PackRgba_t Byte4PackRgba; + + typedef int(__cdecl* RandWithSeed_t)(int* seed); + extern RandWithSeed_t RandWithSeed; + + typedef void(__cdecl* GetDecayingLetterInfo_t)(unsigned int letter, int* randSeed, int decayTimeElapsed, int fxBirthTime, int fxDecayDuration, unsigned __int8 alpha, bool* resultSkipDrawing, char* resultAlpha, unsigned int* resultLetter, bool* resultDrawExtraFxChar); + extern GetDecayingLetterInfo_t GetDecayingLetterInfo; + + typedef void(__cdecl * Field_Draw_t)(int localClientNum, field_t* edit, int x, int y, int horzAlign, int vertAlign); + extern Field_Draw_t Field_Draw; + + typedef void(__cdecl * Field_AdjustScroll_t)(ScreenPlacement* scrPlace, field_t* edit); + extern Field_AdjustScroll_t Field_AdjustScroll; typedef void(__cdecl * AimAssist_ApplyAutoMelee_t)(const AimInput* input, AimOutput* output); extern AimAssist_ApplyAutoMelee_t AimAssist_ApplyAutoMelee; @@ -932,11 +974,19 @@ namespace Game extern GfxScene* scene; - extern clientActive_t* clients; + extern ConDrawInputGlob* conDrawInputGlob; + extern field_t* g_consoleField; extern clientStatic_t* cls; + extern sharedUiInfo_t* sharedUiInfo; + extern ScreenPlacement* scrPlaceFull; + extern ScreenPlacement* scrPlaceView; + + extern clientActive_t* clients; + extern cg_s* cgArray; + extern cgs_t* cgsArray; extern PlayerKeyState* playerKeys; extern kbutton_t* playersKb; @@ -974,6 +1024,7 @@ namespace Game void ShowMessageBox(const std::string& message, const std::string& title); unsigned int R_HashString(const char* string); + unsigned int R_HashString(const char* string, size_t maxLen); void R_LoadSunThroughDvars(const char* mapname, sunflare_t* sun); void R_SetSunFromDvars(sunflare_t* sun); @@ -1012,11 +1063,15 @@ namespace Game void R_AddDebugBounds(float* color, Bounds* b); void R_AddDebugBounds(float* color, Bounds* b, const float(*quat)[4]); + Glyph* R_GetCharacterGlyph(Font_s* font, unsigned int letter); + bool SetupPulseFXVars(const char* text, int maxLength, int fxBirthTime, int fxLetterTime, int fxDecayStartTime, int fxDecayDuration, bool* resultDrawRandChar, int* resultRandSeed, int* resultMaxLength, bool* resultDecaying, int* resultDecayTimeElapsed); + void RB_DrawChar(Material* material, float x, float y, float w, float h, float sinAngle, float cosAngle, Glyph* glyph, unsigned int color); + void RB_DrawStretchPicRotate(Material* material, float x, float y, float w, float h, float s0, float t0, float s1, float t1, float sinAngle, float cosAngle, unsigned int color); + char ModulateByteColors(char colorA, char colorB); + float GraphGetValueFromFraction(int knotCount, const float(*knots)[2], float fraction); float GraphFloat_GetValue(const GraphFloat* graph, const float fraction); - Glyph* R_GetCharacterGlyph(Font_s* font, unsigned int letter); - void AimAssist_UpdateTweakables(int localClientNum); void AimAssist_UpdateAdsLerp(const AimInput* input); } diff --git a/src/Game/Structs.hpp b/src/Game/Structs.hpp index 8bd98dcc..7be1e8d7 100644 --- a/src/Game/Structs.hpp +++ b/src/Game/Structs.hpp @@ -702,8 +702,8 @@ namespace Game const char *name; char gameFlags; char sortKey; - char textureAtlasRowCount; - char textureAtlasColumnCount; + unsigned char textureAtlasRowCount; + unsigned char textureAtlasColumnCount; GfxDrawSurf drawSurf; unsigned int surfaceTypeBits; unsigned __int16 hashIndex; @@ -1566,9 +1566,9 @@ namespace Game { MaterialInfo info; char stateBitsEntry[48]; - char textureCount; - char constantCount; - char stateBitsCount; + unsigned char textureCount; + unsigned char constantCount; + unsigned char stateBitsCount; char stateFlags; char cameraRegion; MaterialTechniqueSet *techniqueSet; @@ -4539,10 +4539,10 @@ namespace Game { XAsset asset; char zoneIndex; - volatile char inuse; + volatile char inuseMask; + bool printedMissingAsset; unsigned __int16 nextHash; unsigned __int16 nextOverride; - unsigned __int16 usageFrame; }; enum XFileLanguage : unsigned char @@ -6042,6 +6042,30 @@ namespace Game int updateSound; int allowAddDObj; }; + + enum TextRenderFlags + { + TEXT_RENDERFLAG_FORCEMONOSPACE = 0x1, + TEXT_RENDERFLAG_CURSOR = 0x2, + TEXT_RENDERFLAG_DROPSHADOW = 0x4, + TEXT_RENDERFLAG_DROPSHADOW_EXTRA = 0x8, + TEXT_RENDERFLAG_GLOW = 0x10, + TEXT_RENDERFLAG_GLOW_FORCE_COLOR = 0x20, + TEXT_RENDERFLAG_FX_DECODE = 0x40, + TEXT_RENDERFLAG_PADDING = 0x80, + TEXT_RENDERFLAG_SUBTITLETEXT = 0x100, + TEXT_RENDERFLAG_CINEMATIC = 0x200, + TEXT_RENDERFLAG_OUTLINE = 0x400, + TEXT_RENDERFLAG_OUTLINE_EXTRA = 0x800, + }; + + enum FontPassType + { + FONTPASS_NORMAL = 0x0, + FONTPASS_GLOW = 0x1, + FONTPASS_OUTLINE = 0x2, + FONTPASS_COUNT = 0x3, + }; struct AimInput { @@ -6183,6 +6207,175 @@ namespace Game entityState_s noDeltaEntities[1024]; }; + struct ConDrawInputGlob + { + char autoCompleteChoice[64]; + int matchIndex; + int matchCount; + const char* inputText; + int inputTextLen; + bool hasExactMatch; + bool mayAutoComplete; + float x; + float y; + float leftX; + float fontHeight; + }; + + struct ScreenPlacement + { + float scaleVirtualToReal[2]; + float scaleVirtualToFull[2]; + float scaleRealToVirtual[2]; + float realViewportPosition[2]; + float realViewportSize[2]; + float virtualViewableMin[2]; + float virtualViewableMax[2]; + float realViewableMin[2]; + float realViewableMax[2]; + float virtualAdjustableMin[2]; + float virtualAdjustableMax[2]; + float realAdjustableMin[2]; + float realAdjustableMax[2]; + float subScreenLeft; + }; + + struct serverStatusInfo_t + { + char address[64]; + const char* lines[128][4]; + char text[1024]; + char pings[54]; + int numLines; + }; + + struct pendingServer_t + { + char adrstr[64]; + char name[64]; + int startTime; + int serverNum; + int valid; + }; + + struct pendingServerStatus_t + { + int num; + pendingServer_t server[16]; + }; + + struct pinglist_t + { + char adrstr[64]; + int start; + }; + + struct serverStatus_s + { + pinglist_t pingList[16]; + int numqueriedservers; + int currentping; + int nextpingtime; + int maxservers; + int refreshtime; + int numServers; + int sortKey; + int sortDir; + int lastCount; + int refreshActive; + int currentServer; + int displayServers[20000]; + int numDisplayServers; + int serverCount; + int numPlayersOnServers; + int nextDisplayRefresh; + int nextSortTime; + int motdLen; + int motdWidth; + int motdPaintX; + int motdPaintX2; + int motdOffset; + int motdTime; + char motd[1024]; + }; + + struct mapInfo + { + char mapName[32]; + char mapLoadName[16]; + char mapDescription[32]; + char mapLoadImage[32]; + char mapCustomKey[32][16]; + char mapCustomValue[32][64]; + int mapCustomCount; + int teamMembers; + int typeBits; + int timeToBeat[32]; + int active; + }; + + struct gameTypeInfo + { + char gameType[12]; + char gameTypeName[32]; + }; + + struct CachedAssets_t + { + Material* scrollBarArrowUp; + Material* scrollBarArrowDown; + Material* scrollBarArrowLeft; + Material* scrollBarArrowRight; + Material* scrollBar; + Material* scrollBarThumb; + Material* sliderBar; + Material* sliderThumb; + Material* whiteMaterial; + Material* cursor; + Material* textDecodeCharacters; + Material* textDecodeCharactersGlow; + Font_s* bigFont; + Font_s* smallFont; + Font_s* consoleFont; + Font_s* boldFont; + Font_s* textFont; + Font_s* extraBigFont; + Font_s* objectiveFont; + Font_s* hudBigFont; + Font_s* hudSmallFont; + snd_alias_list_t* itemFocusSound; + }; + + struct sharedUiInfo_t + { + CachedAssets_t assets; + int playerCount; + char playerNames[18][32]; + char teamNames[18][32]; + int playerClientNums[18]; + volatile int updateGameTypeList; + int numGameTypes; + gameTypeInfo gameTypes[32]; + int numCustomGameTypes; + gameTypeInfo customGameTypes[32]; + char customGameTypeCancelState[2048]; + int numJoinGameTypes; + gameTypeInfo joinGameTypes[32]; + volatile int updateArenas; + int mapCount; + mapInfo mapList[128]; + int mapIndexSorted[128]; + bool mapsAreSorted; + Material* serverHardwareIconList[9]; + unsigned __int64 partyMemberXuid; + Material* talkingIcons[2]; + serverStatus_s serverStatus; + char serverStatusAddress[64]; + serverStatusInfo_t serverStatusInfo; + int nextServerStatusRefresh; + pendingServerStatus_t pendingServerStatus; + }; + struct GraphFloat { char name[64]; @@ -6199,7 +6392,10 @@ namespace Game void* nextSnap; char _pad1[0x673DC]; int frametime; // + 0x6A754 - char _pad2[0x960C]; // + 0x6A758 + int time; + int oldTime; + int physicalsTime; + char _pad2[0x9600]; // + 0x6A758 float compassMapWorldSize[2]; // + 0x73D64 char _pad3[0x74]; // + 0x73D6C float selectedLocation[2]; // + 0x73DE0 @@ -6210,6 +6406,8 @@ namespace Game char _pad4[0x89740]; }; + static_assert(sizeof(cg_s) == 0xFD540); + static constexpr auto MAX_GAMEPADS = 1; static constexpr auto GPAD_VALUE_MASK = 0xFFFFFFFu; @@ -6448,6 +6646,236 @@ namespace Game }; #pragma warning(pop) + enum ShockViewTypes + { + SHELLSHOCK_VIEWTYPE_BLURRED = 0x0, + SHELLSHOCK_VIEWTYPE_FLASHED = 0x1, + SHELLSHOCK_VIEWTYPE_NONE = 0x2, + }; + + struct shellshock_parms_t + { + struct + { + int blurredFadeTime; + int blurredEffectTime; + int flashWhiteFadeTime; + int flashShotFadeTime; + ShockViewTypes type; + } screenBlend; + + struct + { + int fadeTime; + float kickRate; + float kickRadius; + } view; + + struct + { + bool affect; + char loop[64]; + char loopSilent[64]; + char end[64]; + char endAbort[64]; + int fadeInTime; + int fadeOutTime; + float drylevel; + float wetlevel; + char roomtype[16]; + float channelvolume[64]; + int modEndDelay; + int loopFadeTime; + int loopEndDelay; + } sound; + + struct + { + bool affect; + int fadeTime; + float mouseSensitivity; + float maxPitchSpeed; + float maxYawSpeed; + } lookControl; + + struct + { + bool affect; + } movement; + }; + + struct XAnimParent + { + unsigned short flags; + unsigned short children; + }; + + struct XAnimEntry + { + unsigned short numAnims; + unsigned short parent; + + union + { + XAnimParts* parts; + XAnimParent animParent; + }; + }; + + struct XAnim_s + { + unsigned int size; + const char* debugName; + const char** debugAnimNames; + XAnimEntry entries[1]; + }; + + struct animation_s + { + char name[64]; + int initialLerp; + float moveSpeed; + int duration; + int nameHash; + int flags; + int64_t movetype; + int noteType; + }; + + struct lerpFrame_t + { + float yawAngle; + int yawing; + float pitchAngle; + int pitching; + int animationNumber; + animation_s* animation; + int animationTime; + float oldFramePos[3]; + float animSpeedScale; + int oldFrameSnapshotTime; + }; + + struct clientControllers_t + { + float angles[4][3]; + float tag_origin_angles[3]; + float tag_origin_offset[3]; + }; + + struct __declspec(align(4)) XAnimTree_s + { + XAnim_s* anims; + int info_usage; + volatile int calcRefCount; + volatile int modifyRefCount; + unsigned __int16 children; + }; + + enum PlayerDiveState + { + DIVE_NONE = 0x0, + DIVE_FORWARD = 0x1, + DIVE_FORWARDLEFT = 0x2, + DIVE_LEFT = 0x3, + DIVE_BACKLEFT = 0x4, + DIVE_BACK = 0x5, + DIVE_BACKRIGHT = 0x6, + DIVE_RIGHT = 0x7, + DIVE_FORWARDRIGHT = 0x8, + }; + + struct clientInfo_t + { + int infoValid; + int nextValid; + int clientNum; + char name[16]; + team_t team; + team_t oldteam; + int rank; + int prestige; + unsigned int perks[2]; + int score; + int location; + int health; + char model[64]; + char attachModelNames[6][64]; + char attachTagNames[6][64]; + unsigned int partBits[6]; + lerpFrame_t legs; + lerpFrame_t torso; + float lerpMoveDir; + float lerpLean; + float playerAngles[3]; + int legsAnim; + int torsoAnim; + float fTorsoPitch; + float fWaistPitch; + int leftHandGun; + int dobjDirty; + clientControllers_t control; + unsigned int clientConditions[18][2]; + XAnimTree_s* pXAnimTree; + int iDObjWeapon; + char weaponModel; + int stanceTransitionTime; + int turnAnimEndTime; + char turnAnimType; + bool hideWeapon; + bool usingKnife; + int dualWielding; + PlayerDiveState diveState; + int riotShieldNext; + unsigned int playerCardIcon; + unsigned int playerCardTitle; + unsigned int playerCardNameplate; + }; + + struct cgs_t + { + int viewX; + int viewY; + int viewWidth; + int viewHeight; + float viewAspect; + int serverCommandSequence; + int processedSnapshotNum; + int localServer; + char gametype[32]; + char szHostName[256]; + bool hardcore; + int maxclients; + int privateClients; + char mapname[64]; + int gameEndTime; + int voteTime; + int voteYes; + int voteNo; + char voteString[256]; + XModel* gameModels[512]; + FxEffectDef* smokeGrenadeFx; + shellshock_parms_t holdBreathParams; + char teamChatMsgs[8][160]; + int teamChatMsgTimes[8]; + int teamChatPos; + int teamLastChatPos; + float compassWidth; + float compassHeight; + float compassY; + clientInfo_t corpseinfo[8]; + bool entUpdateToggleContextKey; + XAnim_s* helicopterAnims; + }; + + static_assert(sizeof(cgs_t) == 0x3BA4); + + struct ConversionArguments + { + int argCount; + const char* args[9]; + }; + #pragma endregion #ifndef IDA