diff --git a/code/components/gta-core-five/src/FontFormatFixes.cpp b/code/components/gta-core-five/src/FontFormatFixes.cpp new file mode 100644 index 0000000000..ba63df6910 --- /dev/null +++ b/code/components/gta-core-five/src/FontFormatFixes.cpp @@ -0,0 +1,318 @@ +#include "StdInc.h" +#include + +#include + +#include + +#include + +static void(*g_parseHtml)(void* styledText, const wchar_t* str, int64_t length, void* pImgInfoArr, bool multiline, bool condenseWhite, void* styleMgr, void* txtFmt, void* paraFmt); + +static std::wregex emojiRegEx{ L"(?:\xd83d\xdc68\xd83c\xdffc\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c\xdffb|\xd83d\xdc68\xd83c\xdffd\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb\xdffc]|\xd83d\xdc68\xd83c\xdffe\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb-\xdffd]|\xd83d\xdc68\xd83c\xdfff\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb-\xdffe]|\xd83d\xdc69\xd83c\xdffb\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffc-\xdfff]|\xd83d\xdc69\xd83c\xdffc\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb\xdffd-\xdfff]|\xd83d\xdc69\xd83c\xdffc\x200d\xd83e\xdd1d\x200d\xd83d\xdc69\xd83c\xdffb|\xd83d\xdc69\xd83c\xdffd\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb\xdffc\xdffe\xdfff]|\xd83d\xdc69\xd83c\xdffd\x200d\xd83e\xdd1d\x200d\xd83d\xdc69\xd83c[\xdffb\xdffc]|\xd83d\xdc69\xd83c\xdffe\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb-\xdffd\xdfff]|\xd83d\xdc69\xd83c\xdffe\x200d\xd83e\xdd1d\x200d\xd83d\xdc69\xd83c[\xdffb-\xdffd]|\xd83d\xdc69\xd83c\xdfff\x200d\xd83e\xdd1d\x200d\xd83d\xdc68\xd83c[\xdffb-\xdffe]|\xd83d\xdc69\xd83c\xdfff\x200d\xd83e\xdd1d\x200d\xd83d\xdc69\xd83c[\xdffb-\xdffe]|\xd83e\xddd1\xd83c\xdffb\x200d\xd83e\xdd1d\x200d\xd83e\xddd1\xd83c\xdffb|\xd83e\xddd1\xd83c\xdffc\x200d\xd83e\xdd1d\x200d\xd83e\xddd1\xd83c[\xdffb\xdffc]|\xd83e\xddd1\xd83c\xdffd\x200d\xd83e\xdd1d\x200d\xd83e\xddd1\xd83c[\xdffb-\xdffd]|\xd83e\xddd1\xd83c\xdffe\x200d\xd83e\xdd1d\x200d\xd83e\xddd1\xd83c[\xdffb-\xdffe]|\xd83e\xddd1\xd83c\xdfff\x200d\xd83e\xdd1d\x200d\xd83e\xddd1\xd83c[\xdffb-\xdfff]|\xd83e\xddd1\x200d\xd83e\xdd1d\x200d\xd83e\xddd1|\xd83d\xdc6b\xd83c[\xdffb-\xdfff]|\xd83d\xdc6c\xd83c[\xdffb-\xdfff]|\xd83d\xdc6d\xd83c[\xdffb-\xdfff]|\xd83d[\xdc6b-\xdc6d])|(?:\xd83d[\xdc68\xdc69])(?:\xd83c[\xdffb-\xdfff])?\x200d(?:\x2695\xfe0f|\x2696\xfe0f|\x2708\xfe0f|\xd83c[\xdf3e\xdf73\xdf93\xdfa4\xdfa8\xdfeb\xdfed]|\xd83d[\xdcbb\xdcbc\xdd27\xdd2c\xde80\xde92]|\xd83e[\xddaf-\xddb3\xddbc\xddbd])|(?:\xd83c[\xdfcb\xdfcc]|\xd83d[\xdd74\xdd75]|\x26f9)((?:\xd83c[\xdffb-\xdfff]|\xfe0f)\x200d[\x2640\x2642]\xfe0f)|(?:\xd83c[\xdfc3\xdfc4\xdfca]|\xd83d[\xdc6e\xdc71\xdc73\xdc77\xdc81\xdc82\xdc86\xdc87\xde45-\xde47\xde4b\xde4d\xde4e\xdea3\xdeb4-\xdeb6]|\xd83e[\xdd26\xdd35\xdd37-\xdd39\xdd3d\xdd3e\xddb8\xddb9\xddcd-\xddcf\xddd6-\xdddd])(?:\xd83c[\xdffb-\xdfff])?\x200d[\x2640\x2642]\xfe0f|(?:\xd83d\xdc68\x200d\x2764\xfe0f\x200d\xd83d\xdc8b\x200d\xd83d\xdc68|\xd83d\xdc68\x200d\xd83d\xdc68\x200d\xd83d\xdc66\x200d\xd83d\xdc66|\xd83d\xdc68\x200d\xd83d\xdc68\x200d\xd83d\xdc67\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc68\x200d\xd83d\xdc69\x200d\xd83d\xdc66\x200d\xd83d\xdc66|\xd83d\xdc68\x200d\xd83d\xdc69\x200d\xd83d\xdc67\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc69\x200d\x2764\xfe0f\x200d\xd83d\xdc8b\x200d\xd83d[\xdc68\xdc69]|\xd83d\xdc69\x200d\xd83d\xdc69\x200d\xd83d\xdc66\x200d\xd83d\xdc66|\xd83d\xdc69\x200d\xd83d\xdc69\x200d\xd83d\xdc67\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc68\x200d\x2764\xfe0f\x200d\xd83d\xdc68|\xd83d\xdc68\x200d\xd83d\xdc66\x200d\xd83d\xdc66|\xd83d\xdc68\x200d\xd83d\xdc67\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc68\x200d\xd83d\xdc68\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc68\x200d\xd83d\xdc69\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc69\x200d\x2764\xfe0f\x200d\xd83d[\xdc68\xdc69]|\xd83d\xdc69\x200d\xd83d\xdc66\x200d\xd83d\xdc66|\xd83d\xdc69\x200d\xd83d\xdc67\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc69\x200d\xd83d\xdc69\x200d\xd83d[\xdc66\xdc67]|\xd83c\xdff3\xfe0f\x200d\xd83c\xdf08|\xd83c\xdff4\x200d\x2620\xfe0f|\xd83d\xdc15\x200d\xd83e\xddba|\xd83d\xdc41\x200d\xd83d\xdde8|\xd83d\xdc68\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc69\x200d\xd83d[\xdc66\xdc67]|\xd83d\xdc6f\x200d\x2640\xfe0f|\xd83d\xdc6f\x200d\x2642\xfe0f|\xd83e\xdd3c\x200d\x2640\xfe0f|\xd83e\xdd3c\x200d\x2642\xfe0f|\xd83e\xddde\x200d\x2640\xfe0f|\xd83e\xddde\x200d\x2642\xfe0f|\xd83e\xdddf\x200d\x2640\xfe0f|\xd83e\xdddf\x200d\x2642\xfe0f)|[#*0-9]\xfe0f?\x20e3|(?:[©®\x2122\x265f]\xfe0f)|(?:\xd83c[\xdc04\xdd70\xdd71\xdd7e\xdd7f\xde02\xde1a\xde2f\xde37\xdf21\xdf24-\xdf2c\xdf36\xdf7d\xdf96\xdf97\xdf99-\xdf9b\xdf9e\xdf9f\xdfcd\xdfce\xdfd4-\xdfdf\xdff3\xdff5\xdff7]|\xd83d[\xdc3f\xdc41\xdcfd\xdd49\xdd4a\xdd6f\xdd70\xdd73\xdd76-\xdd79\xdd87\xdd8a-\xdd8d\xdda5\xdda8\xddb1\xddb2\xddbc\xddc2-\xddc4\xddd1-\xddd3\xdddc-\xddde\xdde1\xdde3\xdde8\xddef\xddf3\xddfa\xdecb\xdecd-\xdecf\xdee0-\xdee5\xdee9\xdef0\xdef3]|[\x203c\x2049\x2139\x2194-\x2199\x21a9\x21aa\x231a\x231b\x2328\x23cf\x23ed-\x23ef\x23f1\x23f2\x23f8-\x23fa\x24c2\x25aa\x25ab\x25b6\x25c0\x25fb-\x25fe\x2600-\x2604\x260e\x2611\x2614\x2615\x2618\x2620\x2622\x2623\x2626\x262a\x262e\x262f\x2638-\x263a\x2640\x2642\x2648-\x2653\x2660\x2663\x2665\x2666\x2668\x267b\x267f\x2692-\x2697\x2699\x269b\x269c\x26a0\x26a1\x26aa\x26ab\x26b0\x26b1\x26bd\x26be\x26c4\x26c5\x26c8\x26cf\x26d1\x26d3\x26d4\x26e9\x26ea\x26f0-\x26f5\x26f8\x26fa\x26fd\x2702\x2708\x2709\x270f\x2712\x2714\x2716\x271d\x2721\x2733\x2734\x2744\x2747\x2757\x2763\x2764\x27a1\x2934\x2935\x2b05-\x2b07\x2b1b\x2b1c\x2b50\x2b55\x3030\x303d\x3297\x3299])(?:\xfe0f|(?!\xfe0e))|(?:(?:\xd83c[\xdfcb\xdfcc]|\xd83d[\xdd74\xdd75\xdd90]|[\x261d\x26f7\x26f9\x270c\x270d])(?:\xfe0f|(?!\xfe0e))|(?:\xd83c[\xdf85\xdfc2-\xdfc4\xdfc7\xdfca]|\xd83d[\xdc42\xdc43\xdc46-\xdc50\xdc66-\xdc69\xdc6e\xdc70-\xdc78\xdc7c\xdc81-\xdc83\xdc85-\xdc87\xdcaa\xdd7a\xdd95\xdd96\xde45-\xde47\xde4b-\xde4f\xdea3\xdeb4-\xdeb6\xdec0\xdecc]|\xd83e[\xdd0f\xdd18-\xdd1c\xdd1e\xdd1f\xdd26\xdd30-\xdd39\xdd3d\xdd3e\xddb5\xddb6\xddb8\xddb9\xddbb\xddcd-\xddcf\xddd1-\xdddd]|[\x270a\x270b]))(?:\xd83c[\xdffb-\xdfff])?|(?:\xd83c\xdff4\xdb40\xdc67\xdb40\xdc62\xdb40\xdc65\xdb40\xdc6e\xdb40\xdc67\xdb40\xdc7f|\xd83c\xdff4\xdb40\xdc67\xdb40\xdc62\xdb40\xdc73\xdb40\xdc63\xdb40\xdc74\xdb40\xdc7f|\xd83c\xdff4\xdb40\xdc67\xdb40\xdc62\xdb40\xdc77\xdb40\xdc6c\xdb40\xdc73\xdb40\xdc7f|\xd83c\xdde6\xd83c[\xdde8-\xddec\xddee\xddf1\xddf2\xddf4\xddf6-\xddfa\xddfc\xddfd\xddff]|\xd83c\xdde7\xd83c[\xdde6\xdde7\xdde9-\xddef\xddf1-\xddf4\xddf6-\xddf9\xddfb\xddfc\xddfe\xddff]|\xd83c\xdde8\xd83c[\xdde6\xdde8\xdde9\xddeb-\xddee\xddf0-\xddf5\xddf7\xddfa-\xddff]|\xd83c\xdde9\xd83c[\xddea\xddec\xddef\xddf0\xddf2\xddf4\xddff]|\xd83c\xddea\xd83c[\xdde6\xdde8\xddea\xddec\xdded\xddf7-\xddfa]|\xd83c\xddeb\xd83c[\xddee-\xddf0\xddf2\xddf4\xddf7]|\xd83c\xddec\xd83c[\xdde6\xdde7\xdde9-\xddee\xddf1-\xddf3\xddf5-\xddfa\xddfc\xddfe]|\xd83c\xdded\xd83c[\xddf0\xddf2\xddf3\xddf7\xddf9\xddfa]|\xd83c\xddee\xd83c[\xdde8-\xddea\xddf1-\xddf4\xddf6-\xddf9]|\xd83c\xddef\xd83c[\xddea\xddf2\xddf4\xddf5]|\xd83c\xddf0\xd83c[\xddea\xddec-\xddee\xddf2\xddf3\xddf5\xddf7\xddfc\xddfe\xddff]|\xd83c\xddf1\xd83c[\xdde6-\xdde8\xddee\xddf0\xddf7-\xddfb\xddfe]|\xd83c\xddf2\xd83c[\xdde6\xdde8-\xdded\xddf0-\xddff]|\xd83c\xddf3\xd83c[\xdde6\xdde8\xddea-\xddec\xddee\xddf1\xddf4\xddf5\xddf7\xddfa\xddff]|\xd83c\xddf4\xd83c\xddf2|\xd83c\xddf5\xd83c[\xdde6\xddea-\xdded\xddf0-\xddf3\xddf7-\xddf9\xddfc\xddfe]|\xd83c\xddf6\xd83c\xdde6|\xd83c\xddf7\xd83c[\xddea\xddf4\xddf8\xddfa\xddfc]|\xd83c\xddf8\xd83c[\xdde6-\xddea\xddec-\xddf4\xddf7-\xddf9\xddfb\xddfd-\xddff]|\xd83c\xddf9\xd83c[\xdde6\xdde8\xdde9\xddeb-\xdded\xddef-\xddf4\xddf7\xddf9\xddfb\xddfc\xddff]|\xd83c\xddfa\xd83c[\xdde6\xddec\xddf2\xddf3\xddf8\xddfe\xddff]|\xd83c\xddfb\xd83c[\xdde6\xdde8\xddea\xddec\xddee\xddf3\xddfa]|\xd83c\xddfc\xd83c[\xddeb\xddf8]|\xd83c\xddfd\xd83c\xddf0|\xd83c\xddfe\xd83c[\xddea\xddf9]|\xd83c\xddff\xd83c[\xdde6\xddf2\xddfc]|\xd83c[\xdccf\xdd8e\xdd91-\xdd9a\xdde6-\xddff\xde01\xde32-\xde36\xde38-\xde3a\xde50\xde51\xdf00-\xdf20\xdf2d-\xdf35\xdf37-\xdf7c\xdf7e-\xdf84\xdf86-\xdf93\xdfa0-\xdfc1\xdfc5\xdfc6\xdfc8\xdfc9\xdfcf-\xdfd3\xdfe0-\xdff0\xdff4\xdff8-\xdfff]|\xd83d[\xdc00-\xdc3e\xdc40\xdc44\xdc45\xdc51-\xdc65\xdc6a-\xdc6d\xdc6f\xdc79-\xdc7b\xdc7d-\xdc80\xdc84\xdc88-\xdca9\xdcab-\xdcfc\xdcff-\xdd3d\xdd4b-\xdd4e\xdd50-\xdd67\xdda4\xddfb-\xde44\xde48-\xde4a\xde80-\xdea2\xdea4-\xdeb3\xdeb7-\xdebf\xdec1-\xdec5\xded0-\xded2\xded5\xdeeb\xdeec\xdef4-\xdefa\xdfe0-\xdfeb]|\xd83e[\xdd0d\xdd0e\xdd10-\xdd17\xdd1d\xdd20-\xdd25\xdd27-\xdd2f\xdd3a\xdd3c\xdd3f-\xdd45\xdd47-\xdd71\xdd73-\xdd76\xdd7a-\xdda2\xdda5-\xddaa\xddae-\xddb4\xddb7\xddba\xddbc-\xddca\xddd0\xddde-\xddff\xde70-\xde73\xde78-\xde7a\xde80-\xde82\xde90-\xde95]|[\x23e9-\x23ec\x23f0\x23f3\x267e\x26ce\x2705\x2728\x274c\x274e\x2753-\x2755\x2795-\x2797\x27b0\x27bf\xe50a])|\xfe0f" }; +static std::wregex feofRegEx{ L"\xFE0F" }; + +// from https://stackoverflow.com/a/45314422 but adapted to support wide chars +const unsigned char calcFinalSize[] = +{ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 6, 1, 1, 1, 5, 6, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 4, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +template +struct Entities +{ + +}; + +template<> +struct Entities +{ + static constexpr std::string_view amp = "&"; + static constexpr std::string_view apos = "'"; + static constexpr std::string_view quot = """; + static constexpr std::string_view gt = ">"; + static constexpr std::string_view lt = "<"; +}; + +template<> +struct Entities +{ + static constexpr std::wstring_view amp = L"&"; + static constexpr std::wstring_view apos = L"'"; + static constexpr std::wstring_view quot = L"""; + static constexpr std::wstring_view gt = L">"; + static constexpr std::wstring_view lt = L"<"; +}; + +template +void EscapeXml(std::basic_string& in) +{ + const TChar* dataIn = in.data(); + size_t sizeIn = in.size(); + + const TChar* dataInCurrent = dataIn; + const TChar* dataInEnd = dataIn + sizeIn; + size_t outSize = 0; + while (dataInCurrent < dataInEnd) + { + auto ch = static_cast(*dataInCurrent); + + // wide characters need specific branching, try to eliminate this at compile time + if constexpr (sizeof(TChar) > sizeof(uint8_t)) + { + // any >8-bit character is also 1 in size + // this adds an additional branch, sadly. + if (ch > std::numeric_limits::max()) + { + outSize += 1; + dataInCurrent++; + + continue; + } + } + + outSize += calcFinalSize[ch]; + dataInCurrent++; + } + + if (outSize == sizeIn) + { + return; + } + + std::basic_string out; + out.resize(outSize); + + dataInCurrent = dataIn; + TChar* dataOut = &out[0]; + while (dataInCurrent < dataInEnd) + { + using TEntities = Entities; + + switch (*dataInCurrent) { + case '&': + memcpy(dataOut, TEntities::amp.data(), TEntities::amp.size() * sizeof(TChar)); + dataOut += TEntities::amp.size(); + break; + case '\'': + memcpy(dataOut, TEntities::apos.data(), TEntities::apos.size() * sizeof(TChar)); + dataOut += TEntities::apos.size(); + break; + case '\"': + memcpy(dataOut, TEntities::quot.data(), TEntities::quot.size() * sizeof(TChar)); + dataOut += TEntities::quot.size(); + break; + case '>': + memcpy(dataOut, TEntities::gt.data(), TEntities::gt.size() * sizeof(TChar)); + dataOut += TEntities::gt.size(); + break; + case '<': + memcpy(dataOut, TEntities::lt.data(), TEntities::lt.size() * sizeof(TChar)); + dataOut += TEntities::lt.size(); + break; + default: + *dataOut++ = *dataInCurrent; + } + dataInCurrent++; + } + in.swap(out); +} + + +// from https://stackoverflow.com/a/37516316 +template +std::basic_string regex_replace(BidirIt first, BidirIt last, + const std::basic_regex& re, UnaryFunction f) +{ + std::basic_string s; + + typename std::match_results::difference_type + positionOfLastMatch = 0; + auto endOfLastMatch = first; + + auto callback = [&](const std::match_results & match) + { + auto positionOfThisMatch = match.position(0); + auto diff = positionOfThisMatch - positionOfLastMatch; + + auto startOfThisMatch = endOfLastMatch; + std::advance(startOfThisMatch, diff); + + s.append(endOfLastMatch, startOfThisMatch); + s.append(f(match)); + + auto lengthOfMatch = match.length(0); + + positionOfLastMatch = positionOfThisMatch + lengthOfMatch; + + endOfLastMatch = startOfThisMatch; + std::advance(endOfLastMatch, lengthOfMatch); + }; + + std::regex_iterator begin(first, last, re), end; + std::for_each(begin, end, callback); + + s.append(endOfLastMatch, last); + + return s; +} + +template +std::string regex_replace(const std::string& s, + const std::basic_regex& re, UnaryFunction f) +{ + return regex_replace(s.cbegin(), s.cend(), re, f); +} + +static void ParseHtmlStub(void* styledText, const wchar_t* str, int64_t length, void* pImgInfoArr, bool multiline, bool condenseWhite, void* styleMgr, void* txtFmt, void* paraFmt) +{ + if (!txtFmt) + { + txtFmt = *(void**)((char*)styledText + 56); // default text format + } + + int sz = ceil(*(uint16_t*)((char*)txtFmt + 62) / 20.0f * 1.2f); + + auto emojifiedText = regex_replace(str, str + length, emojiRegEx, [sz](const auto& m) + { + auto s = m.str(); + + if (s.find(L"\x200D") == std::string::npos) + { + s = std::regex_replace(s, feofRegEx, L""); + } + + std::wstringstream codePointString; + + int i = 0; + int p = 0; + + while (i < s.length()) + { + auto c = s[i++]; + if (p) { + codePointString << std::hex << (0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)) << L"_"; + p = 0; + } + else if (0xD800 <= c && c <= 0xDBFF) { + p = c; + } + else { + codePointString << std::hex << c << L"-"; + } + } + + auto cps = codePointString.str(); + + return fmt::sprintf(L"", cps.substr(0, cps.length() - 1), sz, sz); + }); + + g_parseHtml(styledText, emojifiedText.c_str(), emojifiedText.size(), pImgInfoArr, multiline, condenseWhite, styleMgr, txtFmt, paraFmt); +} + +static void ParseHtmlUtf8(void* styledText, const char* str, uint64_t length, void* pImgInfoArr, bool multiline, bool condenseWhite, void* styleMgr, void* txtFmt, void* paraFmt) +{ + auto u16 = ToWide(std::string{ str, length }); + + ParseHtmlStub(styledText, u16.c_str(), u16.size(), pImgInfoArr, multiline, condenseWhite, styleMgr, txtFmt, paraFmt); +} + +static void GfxLog(void* sfLog, int messageType, const char* pfmt, va_list argList) +{ + static char buf[32768]; + vsnprintf(buf, sizeof(buf) - 1, pfmt, argList); + + trace("GFx log: %s\n", buf); +} + +static void(*g_origGFxEditTextCharacterDef__SetTextValue)(void* self, const char* newText, bool html, bool notifyVariable); + +static void GFxEditTextCharacterDef__SetTextValue(void* self, const char* newText, bool html, bool notifyVariable) +{ + std::string textRef; + + if (!html) + { + textRef = newText; + EscapeXml(textRef); + + newText = textRef.c_str(); + html = true; + } + + g_origGFxEditTextCharacterDef__SetTextValue(self, newText, html, notifyVariable); +} + +struct GSizeF +{ + float x; + float y; +}; + +static GSizeF (*getHtmlTextExtent)(void* self, const char* putf8Str, float width, const void* ptxtParams); + +static GSizeF GetHtmlTextExtentWrap(void* self, const char* putf8Str, float width, const void* ptxtParams) +{ + // escape (since this is actually non-HTML text extent) + std::string textRef = putf8Str; + EscapeXml(textRef); + + return getHtmlTextExtent(self, textRef.c_str(), width, ptxtParams); +} + +static void(*g_origFormatGtaText)(const char* in, char* out, bool a3, void* a4, float size, bool* html, int maxLength, bool a8); + +static void FormatGtaTextWrap(const char* in, char* out, bool a3, void* a4, float size, bool* html, int maxLength, bool a8) +{ + g_origFormatGtaText(in, out, a3, a4, size, html, maxLength, a8); + + if (!*html) + { + std::string outRef = out; + EscapeXml(outRef); + + strcpy_s(out, maxLength, outRef.c_str()); + out[maxLength - 1] = '\0'; + + *html = true; + } +} + +static HookFunction hookFunction([]() +{ + MH_Initialize(); + MH_CreateHook(hook::get_call(hook::get_pattern("40 88 6C 24 28 44 88 44 24 20 4C 8B C3 48 8B D6", 16)), ParseHtmlStub, (void**)&g_parseHtml); + MH_CreateHook(hook::get_pattern("48 8B F1 44 8D 6F 01 48", -48), GFxEditTextCharacterDef__SetTextValue, (void**)&g_origGFxEditTextCharacterDef__SetTextValue); + + // GTA text formatting function + MH_CreateHook(hook::get_pattern("4C 8B FA 4C 8B D9 F3 0F 59 0D", -0x3A), FormatGtaTextWrap, (void**)& g_origFormatGtaText); + + MH_EnableHook(MH_ALL_HOOKS); + + // parse UTF-8 HTML + hook::jump(hook::get_pattern("49 8B D8 45 33 C0 49 8B E9 FF 50", -0x31), ParseHtmlUtf8); + + // GFxDrawTextImpl(?) + + // GetTextExtent + hook::set_call(&getHtmlTextExtent, hook::get_pattern("48 8B 55 60 45 33 E4 4C 89", -0x5B)); + hook::jump(hook::get_pattern("0F 29 70 D8 4D 8B F0 48 8B F2 0F 28 F3", -0x1F), GetHtmlTextExtentWrap); + + // 1604 + //*(void**)0x1419F4858 = GfxLog; +}); diff --git a/code/components/gta-streaming-five/src/ScaleformHacks.cpp b/code/components/gta-streaming-five/src/ScaleformHacks.cpp index 36e2d66c2e..2b7a0e68a2 100644 --- a/code/components/gta-streaming-five/src/ScaleformHacks.cpp +++ b/code/components/gta-streaming-five/src/ScaleformHacks.cpp @@ -177,7 +177,7 @@ class GFxMemoryHeap virtual void Free(void* memory) = 0; }; -static GFxMemoryHeap** g_gfxMemoryHeap;// = (GFxMemoryHeap**)0x142CBB3E8; +GFxMemoryHeap** g_gfxMemoryHeap;// = (GFxMemoryHeap**)0x142CBB3E8; class GFxRefCountBase { @@ -293,6 +293,8 @@ static uint32_t* g_gfxId; static void SetupTerritories() { g_origSetupTerritories(); + + overlayRootClip = {}; g_foregroundOverlay3D->CreateEmptyMovieClip(&overlayRootClip, "asTestClip3D"); diff --git a/code/components/gta-streaming-five/src/sfFontStuff.cpp b/code/components/gta-streaming-five/src/sfFontStuff.cpp index c02d4ce8c8..80aaafe15b 100644 --- a/code/components/gta-streaming-five/src/sfFontStuff.cpp +++ b/code/components/gta-streaming-five/src/sfFontStuff.cpp @@ -1,6 +1,10 @@ #include "StdInc.h" #include +#include + +#include + #include #include @@ -117,6 +121,383 @@ static void UpdateFontLoading() } } +static bool(*g_origFindExportedResource)(void* movieRoot, void* localDef, void* bindData, void* symbol); + +struct GFxMovieDef +{ + +}; + +struct GRectF +{ + float left, top, right, bottom; +}; + +struct GFxMovieRoot +{ + char pad[200]; + GRectF VisibleFrameRect; +}; + +static GFxMovieDef* g_md; +static GFxMovieRoot* g_movie; + +template +static void HandleEarlyLoading(TFn&& cb) +{ + auto idx = streaming::GetStreamingIndexForName("font_lib_cfx.gfx"); + + if (idx == 0) + { + uint32_t fileId = 0; + streaming::RegisterRawStreamingFile(&fileId, "citizen:/font_lib_cfx.gfx", true, "font_lib_cfx.gfx", false); + + idx = fileId; + + auto gfxStore = streaming::Manager::GetInstance()->moduleMgr.GetStreamingModule("gfx"); + auto pool = (atPoolBase*)((char*)gfxStore + 56); + auto refBase = pool->GetAt(idx - gfxStore->baseIdx); + refBase[50] = true; // 'create movie once loaded' flag + + // '7' flag so the game won't try unloading it + streaming::Manager::GetInstance()->RequestObject(idx, 7); + + // we should load *now* + streaming::LoadObjectsNow(false); + } + + if (idx != 0) + { + auto gfxStore = streaming::Manager::GetInstance()->moduleMgr.GetStreamingModule("gfx"); + + auto pool = (atPoolBase*)((char*)gfxStore + 56); + auto refBase = pool->GetAt(idx - gfxStore->baseIdx); + + auto ref = *refBase; + + if (ref) + { + auto movieDefRef = *(void***)(ref + 16); + auto movieRef = *(void**)(*(char**)(ref + 24) + 16); + + if (movieDefRef) + { + g_md = (GFxMovieDef*)* movieDefRef; + g_movie = (GFxMovieRoot*)movieRef; + + cb(); + } + } + } +} + +static bool FindExportedResource(void* movieRoot, void* localDef, void* bindData, void* symbol) +{ + bool rv = g_origFindExportedResource(movieRoot, localDef, bindData, symbol); + + if (!rv) + { + HandleEarlyLoading([&rv, movieRoot, bindData, symbol]() + { + rv = g_origFindExportedResource(movieRoot, g_md, bindData, symbol); + }); + } + + return rv; +} + +static bool(*g_origGetExportedResource)(void* movieDef, void* bindData, void* symbol, void* ignoreDef); + +static bool GetExportedResource(void* movieDef, void* bindData, void* symbol, void* ignoreDef) +{ + bool rv = g_origGetExportedResource(movieDef, bindData, symbol, ignoreDef); + + if (!rv) + { + HandleEarlyLoading([&rv, bindData, symbol]() + { + rv = g_origGetExportedResource(g_md, bindData, symbol, NULL); + }); + } + + return rv; +} + +static void AddRef(void* ptr) +{ + InterlockedIncrement((unsigned long*)((char*)ptr + 8)); +} + +static void ReleaseRef(void* ptr) +{ + struct VBase + { + public: + virtual ~VBase() = 0; + }; + + if (!InterlockedDecrement((unsigned long*)((char*)ptr + 8))) + { + delete (VBase*)ptr; + } +} + +static hook::cdecl_stub _gfxMovieRoot_getLevelMovie([]() +{ + return hook::get_pattern("48 8B 49 40 44 8B C0 4D 03 C0 42 39 14 C1 74 0D", -0xB); +}); + +static void* Alloc_Align(void* self, size_t size, size_t align) +{ + return _aligned_malloc(size, align); +} + +static void* Alloc(void* self, size_t size) +{ + return _aligned_malloc(size, 16); +} + +static void* Realloc(void* self, void* ptr, size_t size) +{ + return _aligned_realloc(ptr, size, 16); +} + +static void Free(void* self, void* ptr) +{ + _aligned_free(ptr); +} + +static void* AllocAuto_Align(void* self, void* h, size_t size, size_t align) +{ + return _aligned_malloc(size, align); +} + +static void* AllocAuto(void* self, void* h, size_t size) +{ + return _aligned_malloc(size, 16); +} + +class GFxMemoryHeap +{ +public: + virtual void m_00() = 0; + virtual void m_08() = 0; + virtual void m_10() = 0; + virtual void m_18() = 0; + virtual void m_20() = 0; + virtual void m_28() = 0; + virtual void m_30() = 0; + virtual void m_38() = 0; + virtual void m_40() = 0; + virtual void m_48() = 0; + virtual void* Alloc(uint32_t size) = 0; + virtual void m_58() = 0; + virtual void Free(void* memory) = 0; +}; + +extern GFxMemoryHeap** g_gfxMemoryHeap; + +static void* GetHeap(void* self) +{ + return self; +} + +static LONG SafetyFilter(PEXCEPTION_POINTERS pointers) +{ + static bool excepted = false; + + if (!excepted) + { + trace("Exception in unsafe stubbed GFx hooks: %08x at %016llx.\n", pointers->ExceptionRecord->ExceptionCode, (uint64_t)pointers->ExceptionRecord->ExceptionAddress); + + excepted = true; + } + + return EXCEPTION_EXECUTE_HANDLER; +} + +struct Matrix2D +{ + float matrix[2][3]; + + inline Matrix2D() + { + memset(matrix, 0, sizeof(matrix)); + matrix[0][0] = 1.0f; + matrix[1][1] = 1.0f; + } + + inline void AppendScaling(float sx, float sy) + { + matrix[0][0] *= sx; + matrix[0][1] *= sx; + matrix[0][2] *= sx; + matrix[1][0] *= sy; + matrix[1][1] *= sy; + matrix[1][2] *= sy; + } + + inline void AppendTranslation(float dx, float dy) + { + matrix[0][2] += dx; + matrix[1][2] += dy; + } + + inline void Prepend(const Matrix2D& m) + { + Matrix2D t = *this; + matrix[0][0] = t.matrix[0][0] * m.matrix[0][0] + t.matrix[0][1] * m.matrix[1][0]; + matrix[1][0] = t.matrix[1][0] * m.matrix[0][0] + t.matrix[1][1] * m.matrix[1][0]; + matrix[0][1] = t.matrix[0][0] * m.matrix[0][1] + t.matrix[0][1] * m.matrix[1][1]; + matrix[1][1] = t.matrix[1][0] * m.matrix[0][1] + t.matrix[1][1] * m.matrix[1][1]; + matrix[0][2] = t.matrix[0][0] * m.matrix[0][2] + t.matrix[0][1] * m.matrix[1][2] + t.matrix[0][2]; + matrix[1][2] = t.matrix[1][0] * m.matrix[0][2] + t.matrix[1][1] * m.matrix[1][2] + t.matrix[1][2]; + } +}; + +struct CXform +{ + float matrix[4][2]; + + CXform() + { + matrix[0][0] = 1.0f; + matrix[1][0] = 1.0f; + matrix[2][0] = 1.0f; + matrix[3][0] = 1.0f; + matrix[0][1] = 0.0f; + matrix[1][1] = 0.0f; + matrix[2][1] = 0.0f; + matrix[3][1] = 0.0f; + } +}; + +struct GFxTextImageDesc +{ + void* vtbl; + int refcount; + void* imageShape; + void* spriteShape; + int baseLineX; + int baseLineY; + uint32_t screenWidth; + uint32_t screenHeight; + Matrix2D matrix; +}; + +struct HTMLImageTagInfo +{ + GFxTextImageDesc* textImageDesc; + char pad[32]; + uint32_t Width, Height; +}; + +struct GFxResource +{ + virtual void m_0() = 0; + virtual void m_1() = 0; + virtual int GetType() = 0; + + inline bool IsSprite() + { + return ((GetType() >> 8) & 0xff) == 0x84; + } +}; + +struct GFxSprite +{ + virtual void m0() = 0; + virtual void m1() = 0; + virtual void m2() = 0; + virtual void m3() = 0; + virtual void m4() = 0; + virtual void m5() = 0; + virtual void m6() = 0; + virtual void m7() = 0; + virtual void m8() = 0; + virtual void m9() = 0; + virtual void m10() = 0; + virtual void m11() = 0; + virtual void m12() = 0; + virtual void m13() = 0; + virtual void m14() = 0; + virtual void m15() = 0; + virtual GRectF GetRectBounds(const Matrix2D& matrix) = 0; + virtual void m17() = 0; + virtual void m18() = 0; + virtual void m19() = 0; + virtual void m20() = 0; + virtual void m21() = 0; + virtual void m22() = 0; + virtual void m23() = 0; + virtual void m24() = 0; + virtual void m25() = 0; + virtual void m26() = 0; + virtual void m27() = 0; + virtual void m28() = 0; + virtual void Display(void* context) = 0; + virtual void Restart() = 0; +}; + +struct GFxSpriteDef : public GFxResource +{ + virtual void m_s0() = 0; + virtual void m_s1() = 0; + virtual void m_s2() = 0; + virtual void m_s3() = 0; + virtual void m_s4() = 0; + virtual void m_s5() = 0; + virtual void* CreateCharacterInstance(void* parent, uint32_t* id, + void* pbindingImpl) = 0; +}; + +struct DisplayContext +{ + CXform* parentCxform; + Matrix2D* parentMatrix; +}; + +static void HandleSprite(GFxResource* resource, HTMLImageTagInfo* imgTagInfo, uint64_t document, void* md, void* character) +{ + auto sprite = (GFxSpriteDef*)resource; + + static uint32_t id = 0x1234; + auto ci = (GFxSprite*)sprite->CreateCharacterInstance(character ? character : _gfxMovieRoot_getLevelMovie(g_movie, 0), &id, g_md ? g_md : md); + + AddRef(ci); + + if (imgTagInfo->textImageDesc->spriteShape) + { + ReleaseRef(imgTagInfo->textImageDesc->spriteShape); + } + + imgTagInfo->textImageDesc->spriteShape = ci; + + float screenWidth = imgTagInfo->Width; + float screenHeight = imgTagInfo->Height; + + ci->Restart(); + + auto rect = ci->GetRectBounds(Matrix2D{}); + float origWidth = abs(rect.right - rect.left); + float origHeight = abs(rect.bottom - rect.top); + + imgTagInfo->textImageDesc->screenWidth = screenWidth; + + float origAspect = (origWidth / origHeight); + screenWidth = screenHeight * origAspect; + + imgTagInfo->textImageDesc->matrix.AppendTranslation(0.0f, -origHeight); + imgTagInfo->textImageDesc->matrix.AppendScaling(screenWidth / origWidth, screenHeight / origHeight); + + imgTagInfo->textImageDesc->screenHeight = screenHeight; + + // SetCompleteReformatReq + *(uint8_t*)(document + 456i64) |= 2u; + + ReleaseRef(ci); +} + static HookFunction hookFunction([]() { // get font by id, hook @@ -137,4 +518,203 @@ static HookFunction hookFunction([]() { UpdateFontLoading(); }); + + // find resource parent movie defs + MH_Initialize(); + MH_CreateHook(hook::get_pattern("B0 01 EB 47 48 8B 5F 70 48 83 C7 68", -0x39), FindExportedResource, (void**)&g_origFindExportedResource); + MH_CreateHook(hook::get_pattern("48 8B F9 33 DB 49 8B 4A 40 48 8B F2 83", -0x2F), GetExportedResource, (void**)&g_origGetExportedResource); + MH_EnableHook(MH_ALL_HOOKS); + + static struct : jitasm::Frontend + { + virtual void InternalMain() override + { + mov(r9, r14); // moviedef + mov(r8, r12); // self + mov(rdx, qword_ptr[rbp - 0x30]); // resource + mov(rcx, rsi); // img tag info, base + add(rcx, r15); // img tag info, offset + + sub(rsp, 0x28); + + mov(rax, (uint64_t)&HandleNewImageTag); + call(rax); + + add(rsp, 0x28); + + ret(); + } + + static void HandleNewImageTag(HTMLImageTagInfo* imgTagInfo, GFxResource* resource, void* self, void* md) + { + if (resource->IsSprite()) + { + __try + { + HandleSprite(resource, imgTagInfo, *(uint64_t*)((char*)self + 272), md, self); + } + __except (SafetyFilter(GetExceptionInformation())) + { + + } + } + } + } textFieldStub; + + { + auto location = hook::get_pattern("3D 00 01 00 00 74 0F 48 8B 4D D0 48 8B 01", 7); + hook::nop(location, 10); + hook::call(location, textFieldStub.GetCode()); + } + + static void* returnAddress; + + static struct : jitasm::Frontend + { + virtual void InternalMain() override + { + mov(r8, qword_ptr[rbp + 0x67]); // document + mov(rdx, rcx); // resource + mov(rcx, qword_ptr[rbp + 0xF]); // image tag info, base + add(rcx, r15); // image tag info, itself + + sub(rsp, 0x28); + + mov(rax, (uint64_t)&HandleNewImageTag); + call(rax); + + add(rsp, 0x28); + + test(rax, rax); + jz("jumpOut"); + + and(eax, 0xFF00); // original code + + ret(); + + L("jumpOut"); // jump to end of block + mov(rax, (uint64_t)returnAddress); + mov(qword_ptr[rsp], rax); + ret(); + } + + static int HandleNewImageTag(HTMLImageTagInfo* imgTagInfo, GFxResource* resource, uint64_t document) + { + __try + { + if (resource->IsSprite() && g_md) + { + HandleSprite(resource, imgTagInfo, document, g_md, nullptr); + + return 0; + } + } + __except (SafetyFilter(GetExceptionInformation())) + { + + } + + return resource->GetType(); + } + } drawTextStub; + + { + auto location = hook::get_pattern("48 8B 4D C7 48 8B 01 FF 50 10 25 00 FF 00", 7); + + returnAddress = location + 76; + + hook::nop(location, 8); + hook::call(location, drawTextStub.GetCode()); + } + + static struct : jitasm::Frontend + { + virtual void InternalMain() override + { + cmp(qword_ptr[r14 + 0x18], 0); // if (image->spriteShape != 0) + jz("onReturn"); // { + + lea(r8, qword_ptr[rbp - 0x48]); // matrix + mov(rdx, rsi); // context + mov(rcx, r14); // pImage + + sub(rsp, 0x28); + mov(rax, (uint64_t)&DrawSprite);// DrawSprite(pImage, context, matrix) + call(rax); + add(rsp, 0x28); + + L("onReturn"); // } + + cmp(qword_ptr[r14 + 0x10], 0); // original code + ret(); + } + + static void DrawSprite(GFxTextImageDesc* image, DisplayContext* drawContext, const Matrix2D* matrix) + { + // add a SEH frame here so we won't try unwinding over the jitasm stub + __try + { + DrawSpriteInternal(image, drawContext, matrix); + } + __except (SafetyFilter(GetExceptionInformation())) + { + + } + } + + static void DrawSpriteInternal(GFxTextImageDesc* image, DisplayContext* drawContext, const Matrix2D* matrix) + { + // expand the visible rectangle of the placeholder movie + // otherwise, only ~600x400 on-screen gets sprites drawn due to culling tests + if (g_movie) + { + g_movie->VisibleFrameRect.left = 0.0f; + g_movie->VisibleFrameRect.top = 0.0f; + g_movie->VisibleFrameRect.bottom = 10000.0f * 20.0f; + g_movie->VisibleFrameRect.right = 10000.0f * 20.0f; + } + + // +88 -> local matrix + GFxSprite* sprite = (GFxSprite*)image->spriteShape; + + // copy the input (character) matrix and prepend the local matrix + auto m2 = *matrix; + m2.Prepend(image->matrix); + + // set the matrix/color transform in the context + CXform identityCxform; + + auto oldParentCxform = drawContext->parentCxform; + auto oldParentMatrix = drawContext->parentMatrix; + + drawContext->parentCxform = &identityCxform; + drawContext->parentMatrix = &m2; + + // draw the sprite + sprite->Display(drawContext); + + // restore the old color transform and matrix + drawContext->parentCxform = oldParentCxform; + drawContext->parentMatrix = oldParentMatrix; + + } + } lineBufferStub; + + hook::call(hook::get_pattern("F3 0F 58 55 34 F3 0F 11 4D C0 F3 0F 11 55 CC", 24), lineBufferStub.GetCode()); + + // formerly debuggability for GFx heap, but as it seems now this is required to not get memory corruption + // (memory locking, maybe? GFx allows disabling thread safety of its heap) + // TODO: figure this out + auto memoryHeapPt = hook::get_address(hook::get_call(hook::get_pattern("41 F6 06 04 75 03 83 CD 20", 12)) + 0x19); + memoryHeapPt[9] = Alloc_Align; + memoryHeapPt[10] = Alloc; + memoryHeapPt[11] = Realloc; + memoryHeapPt[12] = Free; + memoryHeapPt[13] = AllocAuto_Align; + memoryHeapPt[14] = AllocAuto; + memoryHeapPt[15] = GetHeap; + + // always enable locking for GFx heap + // doesn't fix it, oddly - probably singlethreaded non-atomic reference counts? + //hook::put(hook::get_pattern("24 01 41 88 87 C0 00 00 00 41 8B"), 0xB0); }); diff --git a/data/client/citizen/font_lib_cfx.gfx b/data/client/citizen/font_lib_cfx.gfx new file mode 100644 index 0000000000..bfe0f5511a Binary files /dev/null and b/data/client/citizen/font_lib_cfx.gfx differ