#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::FormattedStringBuffer TextRenderer::stringSearchStartWith; TextRenderer::FormattedStringBuffer TextRenderer::stringHintAutoComplete; TextRenderer::FormattedStringBuffer TextRenderer::stringHintModifier; TextRenderer::FormattedStringBuffer TextRenderer::stringListHeader; TextRenderer::FormattedStringBuffer TextRenderer::stringListFlipHorizontal; TextRenderer::FormattedStringBuffer TextRenderer::stringListFlipVertical; TextRenderer::FormattedStringBuffer TextRenderer::stringListBig; 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); 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(TextRenderer::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 context.lastQuery = std::string(&edit->buffer[fontIconStart], edit->cursor - fontIconStart); stringSearchStartWith.Format(context.lastQuery.c_str()); UpdateAutocompleteContextResults(context, font, textXScale); } void TextRenderer::DrawAutocompleteModifiers(const FontIconAutocompleteContext& context, const float x, const float y, Game::Font_s* font, float textXScale, float textYScale) { const auto textWidth = std::max(std::max(std::max(stringListHeader.GetWidth(font), stringListFlipHorizontal.GetWidth(font)), stringListFlipVertical.GetWidth(font)), stringListBig.GetWidth(font)); const auto boxWidth = static_cast(textWidth) * 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; Game::R_AddCmdDrawText(stringListHeader.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); currentY += lineHeight; 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 FontIconAutocompleteContext& context, const float x, const float y, Game::Font_s* font, const float textXScale, const float textYScale) { const auto hintEnabled = cg_fontIconAutocompleteHint.get(); auto longestStringLength = stringSearchStartWith.GetWidth(font); if(hintEnabled) longestStringLength = std::max(std::max(longestStringLength, stringHintAutoComplete.GetWidth(font)), stringHintModifier.GetWidth(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; 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); auto currentY = y + lineHeight; Game::R_AddCmdDrawText(stringSearchStartWith.GetString(), std::numeric_limits::max(), font, x, currentY, textXScale, textYScale, 0.0, TEXT_COLOR, 0); currentY += lineHeight; 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; } 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 FontIconAutocompleteContext& context, const float x, const float y, Game::Font_s* font, const float textXScale, const float textYScale) { if (context.inModifiers) DrawAutocompleteModifiers(context, x, y, font, textXScale, textYScale); else DrawAutocompleteResults(context, 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(autocompleteContext, 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(autocompleteContext, 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) { 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()); 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_TAB: AutocompleteFill(context, scrPlace, edit); return true; case Game::K_ESCAPE: if (!context.userClosed) { context.autocompleteActive = false; context.userClosed = true; return true; } return false; default: return false; } } void TextRenderer::Console_Key_Hk(const int localClientNum, const int key) { auto& autocompleteContext = autocompleteContextArray[FONT_ICON_ACI_CONSOLE]; if (autocompleteContext.autocompleteActive && AutocompleteHandleKeyDown(autocompleteContext, key, Game::scrPlaceFull, Game::g_consoleField)) return; Utils::Hook::Call(0x4311E0)(localClientNum, key); } bool TextRenderer::ChatHandleKeyDown(const int localClientNum, const int key) { auto& autocompleteContext = autocompleteContextArray[FONT_ICON_ACI_CHAT]; return autocompleteContext.autocompleteActive && AutocompleteHandleKeyDown(autocompleteContext, key, &Game::scrPlaceView[localClientNum], &Game::playerKeys[localClientNum].chatField); } 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() { stringSearchStartWith.Load("FONT_ICON_SEARCH_START_WITH"); stringHintAutoComplete.Load("FONT_ICON_HINT_AUTO_COMPLETE"); stringHintAutoComplete.Format("TAB"); stringHintModifier.Load("FONT_ICON_HINT_MODIFIER"); stringHintModifier.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_SEPARATOR_CHARACTER)); stringListHeader.Load("FONT_ICON_MODIFIER_LIST_HEADER"); stringListFlipHorizontal.Load("FONT_ICON_MODIFIER_LIST_FLIP_HORIZONTAL"); stringListFlipHorizontal.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_FLIP_HORIZONTALLY)); stringListFlipVertical.Load("FONT_ICON_MODIFIER_LIST_FLIP_VERTICAL"); stringListFlipVertical.Format(Utils::String::VA("%c", FONT_ICON_MODIFIER_FLIP_VERTICALLY)); stringListBig.Load("FONT_ICON_MODIFIER_LIST_BIG"); 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); } }