From b36df5130ba4fda08e5e3a6da45798261b893b63 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 19 Sep 2021 15:49:12 +0200 Subject: [PATCH] Change logic that splits chat messages with new lines to support fonticons properly --- src/Components/Modules/Chat.cpp | 121 +++++++++++++ src/Components/Modules/Chat.hpp | 9 + src/Components/Modules/TextRenderer.cpp | 15 ++ src/Components/Modules/TextRenderer.hpp | 27 +-- src/Game/Functions.cpp | 1 + src/Game/Functions.hpp | 1 + src/Game/Structs.hpp | 231 +++++++++++++++++++++++- 7 files changed, 392 insertions(+), 13 deletions(-) diff --git a/src/Components/Modules/Chat.cpp b/src/Components/Modules/Chat.cpp index e4c5ce44..204981df 100644 --- a/src/Components/Modules/Chat.cpp +++ b/src/Components/Modules/Chat.cpp @@ -2,6 +2,10 @@ 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) @@ -74,11 +78,128 @@ namespace Components } } + 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::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; + if (Game::cgsArray[0].teamChatPos++ + 1 - 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 index 9d76a710..9cf35e9c 100644 --- a/src/Components/Modules/Chat.hpp +++ b/src/Components/Modules/Chat.hpp @@ -4,15 +4,24 @@ 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/TextRenderer.cpp b/src/Components/Modules/TextRenderer.cpp index 321d1aba..6a69283d 100644 --- a/src/Components/Modules/TextRenderer.cpp +++ b/src/Components/Modules/TextRenderer.cpp @@ -636,6 +636,21 @@ namespace Components 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); diff --git a/src/Components/Modules/TextRenderer.hpp b/src/Components/Modules/TextRenderer.hpp index 9a285fda..97de8f54 100644 --- a/src/Components/Modules/TextRenderer.hpp +++ b/src/Components/Modules/TextRenderer.hpp @@ -49,14 +49,6 @@ namespace Components Game::Material* material; }; - struct FontIconInfo - { - Game::Material* material; - bool flipHorizontal; - bool flipVertical; - bool big; - }; - struct HsvColor { unsigned char h; @@ -99,8 +91,6 @@ namespace Components float maxMaterialNameWidth; }; - static constexpr char COLOR_FIRST_CHAR = '0'; - static constexpr char COLOR_LAST_CHAR = CharForColorIndex(TEXT_COLOR_COUNT - 1); static constexpr unsigned MY_ALTCOLOR_TWO = 0x0DCE6FFE6; static constexpr unsigned COLOR_MAP_HASH = 0xA0AB1041; static constexpr auto FONT_ICON_AUTOCOMPLETE_BOX_PADDING = 6.0f; @@ -153,6 +143,17 @@ namespace Components static Game::dvar_t** con_inputBoxColor; public: + static constexpr char COLOR_FIRST_CHAR = '0'; + static constexpr char COLOR_LAST_CHAR = CharForColorIndex(TEXT_COLOR_COUNT - 1); + + struct FontIconInfo + { + Game::Material* material; + bool flipHorizontal; + bool flipVertical; + bool big; + }; + 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); @@ -163,6 +164,10 @@ namespace Components 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); + TextRenderer(); private: @@ -193,8 +198,6 @@ namespace Components static void GetUnpackedColorByNameStub(); static Game::GfxImage* GetFontIconColorMap(const Game::Material* fontIconMaterial); - static bool IsFontIcon(const char*& text, FontIconInfo& fontIcon); - static float GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, float xScale); 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); diff --git a/src/Game/Functions.cpp b/src/Game/Functions.cpp index 22c62aa1..12d2bd8a 100644 --- a/src/Game/Functions.cpp +++ b/src/Game/Functions.cpp @@ -474,6 +474,7 @@ namespace Game 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); diff --git a/src/Game/Functions.hpp b/src/Game/Functions.hpp index b07c3825..332ce61b 100644 --- a/src/Game/Functions.hpp +++ b/src/Game/Functions.hpp @@ -974,6 +974,7 @@ namespace Game extern clientActive_t* clients; extern cg_s* cgArray; + extern cgs_t* cgsArray; extern PlayerKeyState* playerKeys; extern kbutton_t* playersKb; diff --git a/src/Game/Structs.hpp b/src/Game/Structs.hpp index b2cf941a..16b1cf4b 100644 --- a/src/Game/Structs.hpp +++ b/src/Game/Structs.hpp @@ -6380,7 +6380,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 @@ -6391,6 +6394,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; @@ -6629,6 +6634,230 @@ 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); + #pragma endregion #ifndef IDA