#include "UI/Text/NavigationLink.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Client/Application/ApplicationWindow.h" #include "Client/Input/InputCharset.h" #include "Client/Mode/ModeTooltip.h" #include "Client/Session.h" #include "Contents/CashEmotionMgr.h" #include "Contents/Configuration/ServiceConfiguration.h" #include "Contents/Items/ItemDescription.h" #include "Contents/Items/ItemInfo.h" #include "Contents/Items/ItemLink.h" #include "Contents/Items/ItemNaming.h" #include "Contents/MessageTable.h" #include "Contents/PlayerNameDictionary.h" #include "Contents/TipBoxTitles.h" #include "Network/TextEncoding.h" #include "Render/Text/FontCache.h" #include "Render/Text/TextContext.h" #include "Resource/Bitmap/BitmapResource.h" #include "Resource/ResourceMgr.h" #include "Resource/Sprites/ActionResource.h" #include "Resource/Sprites/SpriteResource.h" #include "Resource/TextureNames.h" #include "UI/Buttons/UIButton.h" #include "UI/Buttons/UITextButton.h" #include "UI/Dialogs/UITipboxWnd.h" #include "UI/Text/ColoredText.h" #include "UI/Text/CreateLineInfo.h" #include "UI/Text/TextMarkup.h" #include "UI/Window/UINewSelectCharWnd.h" #include "UI/Window/UIWindow.h" #include "UI/Window/UIWindowMgr.h" #include "Utility/Text/StringUtil.h" #include "Utility/Text/TextEscapes.h" #include "Utility/Text/TextTokens.h" namespace LineCutter { class UIText : public UIWindow { public: // INLINED explicit UIText(UIWindowContext& windows) : UIWindow(windows, 0), m_color(0), m_fontHeight(12), m_bold(0), m_italic(0), m_outlined(0), m_outlineColor(0xFFFFFF), m_shadowed(0), m_shadowColor(0xFFFFFF) { } void Paint() override; void Slot25(int x, int y) override; void Slot26(int x, int y) override; void Slot27(int x, int y) override; void ResetCursor(int x, int y) override; void Slot31(int x, int y) override; void Slot32(int x, int y) override; void Slot33(int x, int y) override; void Slot35(int value) override; /* 007C */ std::uint32_t m_color; /* 0080 */ int m_fontHeight; /* 0084 */ unsigned char m_bold; /* 0085 */ unsigned char m_italic; /* 0088 */ unsigned char m_outlined; /* 008C */ std::uint32_t m_outlineColor; /* 0090 */ unsigned char m_shadowed; /* 0094 */ std::uint32_t m_shadowColor; /* 0098 */ std::string m_text; }; } class UIEmotion : public UIWindow { public: // INLINED UIEmotion(UIWindowContext& windows, const char* sprite, const char* action, unsigned animation, unsigned maxLoops) : UIWindow(windows, 0), m_animation(animation), m_frame(0), m_maxLoops(maxLoops), m_sprite(static_cast(ResourceMgr::Instance().Load(sprite))), m_action(static_cast(ResourceMgr::Instance().Load(action))), m_startTime(timeGetTime()), m_loops(1) { } void Slot19() override; void Paint() override; protected: /* 007C */ unsigned m_animation; /* 0080 */ unsigned m_frame; /* 0084 */ unsigned m_maxLoops; /* 0088 */ CSprRes* m_sprite; /* 008C */ CActRes* m_action; /* 0090 */ std::uint32_t m_startTime; /* 0094 */ unsigned m_loops; }; class UIItemImage : public UIWindow { public: // INLINED UIItemImage(UIWindowContext& windows, const std::string& itemId) : UIWindow(windows, 0), m_itemId(itemId) { this->m_clip = 1; } void Paint() override; void Slot28(int x, int y) override; void Slot32(int x, int y) override; protected: /* 007C */ std::string m_itemId; }; #if defined(_WIN32) && !defined(_WIN64) static_assert(sizeof(LineCutter::UIText) == 0xB0); #endif // GLOBAL: gl_24668 NavigationLinkParser* g_navigationLinkParser; // OFFSET: 0x00831FE0 // FUNCTION: fn_10404 NavigationLinkParser::NavigationLinkParser() : m_ready(true) { } // OFFSET: 0x00831FF0 // FUNCTION: fn_10405 NavigationLinkParser::~NavigationLinkParser() { } // OFFSET: 0x00833960 // FUNCTION: fn_10412 bool CreateNavigationLinkParser() { if (g_navigationLinkParser) { return false; } g_navigationLinkParser = new (std::nothrow) NavigationLinkParser; if (!g_navigationLinkParser) { return false; } if (!g_navigationLinkParser->m_ready) { delete g_navigationLinkParser; g_navigationLinkParser = nullptr; return false; } return true; } // OFFSET: 0x00833A00 // FUNCTION: fn_10413 void DestroyNavigationLinkParser() { if (g_navigationLinkParser) { delete g_navigationLinkParser; g_navigationLinkParser = nullptr; } } // GLOBAL: gl_24818 static const std::string g_urlTag = ""; // GLOBAL: gl_24819 static const std::string g_urlCloseTag = ""; // GLOBAL: gl_24820 static const std::string g_naviTag = ""; // GLOBAL: gl_24821 static const std::string g_naviCloseTag = ""; // GLOBAL: gl_24822 static const std::string g_infoTag = ""; // GLOBAL: gl_24823 static const std::string g_infoCloseTag = ""; // GLOBAL: gl_24824 static const std::string g_itemTag = ""; // GLOBAL: gl_24825 static const std::string g_itemCloseTag = ""; // GLOBAL: gl_24826 static const std::string g_itemLinkTag = ""; // GLOBAL: gl_24827 static const std::string g_itemLinkCloseTag = ""; // GLOBAL: gl_24828 static const std::string g_messageTag = ""; // GLOBAL: gl_24829 static const std::string g_messageCloseTag = ""; // GLOBAL: gl_24830 static const std::string g_naviLinkTag = ""; // GLOBAL: gl_24831 static const std::string g_naviLinkCloseTag = ""; // GLOBAL: gl_24832 static const std::string g_tipBoxTag = ""; // GLOBAL: gl_24833 static const std::string g_tipBoxCloseTag = ""; // GLOBAL: gl_24834 static const std::string g_questTag = ""; // GLOBAL: gl_24835 static const std::string g_questCloseTag = ""; // GLOBAL: gl_24836 std::vector CCreateLineInfo::s_fontStackCache; // OFFSET: 0x00832500 // FUNCTION: fn_10407 bool NavigationLinkParser::HasLink(std::string text) { return text.find(g_naviLinkTag) != std::string::npos || text.find(g_naviLinkCloseTag) != std::string::npos || text.find(g_itemLinkTag) != std::string::npos || text.find(g_itemLinkCloseTag) != std::string::npos; } // OFFSET: 0x00832640 // FUNCTION: fn_10408 std::string NavigationLinkParser::ExpandMessages(std::string text) { std::string expanded; for (;;) { std::string work; if (expanded.empty()) { work = text; } else { work = expanded; } const std::size_t begin = work.find(g_messageTag); if (begin == std::string::npos) { return text; } work = work.substr(begin + std::strlen(g_messageTag.c_str())); const std::size_t end = work.find(g_messageCloseTag); if (end == std::string::npos) { return text; } work = work.substr(0, end); int messageId; if (work.find("MSI_") != std::string::npos) { messageId = FindMessageId(work.c_str()); } else { messageId = std::atoi(work.c_str()); } const std::string message = FormatMessageArguments(work, messageId); work.clear(); work = expanded.empty() ? text : expanded; const std::string head = work.substr(0, begin); const std::string tail = work.substr(work.find(g_messageCloseTag) + std::strlen(g_messageCloseTag.c_str())); expanded.clear(); expanded += head; expanded += message; expanded += tail; if (expanded.find(g_messageTag) == std::string::npos) { return expanded; } } } // OFFSET: 0x00833160 // FUNCTION: fn_10409 int DecodeNavigationNumber(std::string value) { constexpr char digits[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const std::size_t length = value.size(); std::uint32_t result = 0; for (std::size_t index = 0; index < length; ++index) { const char* digit = std::strchr(digits, value[index]); if (!digit || *digit == '\0') { continue; } std::uint32_t place = 1; for (std::size_t exponent = index + 1; exponent < length; ++exponent) { place *= 62; } result += static_cast(digit - digits) * place; } return static_cast(result); } // OFFSET: 0x00833270 // FUNCTION: fn_10410 std::string EncodeNavigationNumber(int value) { const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; std::vector characters; do { characters.push_back(digits[value % 62]); value /= 62; } while (value >= 62); characters.push_back(digits[value]); std::string result; const std::size_t count = characters.size(); for (std::size_t index = 0; index < count; ++index) { result += characters[count - 1 - index]; } return result; } // OFFSET: 0x00835890 // FUNCTION: fn_10420 bool ParseNavigationLink(std::string value, std::string& name, int& x, int& y) { const auto end = value.find(""); if (end == std::string::npos) { return true; } const auto coordinates = end - 4; const std::string tail = value.substr(coordinates); x = DecodeNavigationNumber(tail.substr(0, 2)); y = DecodeNavigationNumber(tail.substr(2, 2)); const std::string head = value.substr(0, coordinates); const auto begin = head.find(""); if (begin != std::string::npos) { name = head.substr(begin + 7); } return !name.empty(); } // OFFSET: 0x00835600 // FUNCTION: fn_10419 std::string FormatNavigationLink(std::string value, bool brackets) { std::string name; int x = 0; int y = 0; ParseNavigationLink(std::move(value), name, x, y); char text[128] = {}; if (brackets) { if (x > 999 && y > 999) { std::sprintf(text, "<%s>", name.c_str()); } else { std::sprintf(text, "<%s %d,%d>", name.c_str(), x, y); } } else { if (x > 999 && y > 999) { std::sprintf(text, "%s", name.c_str()); } else { std::sprintf(text, "%s %d,%d", name.c_str(), x, y); } } return text; } // OFFSET: 0x00835B00 // FUNCTION: fn_10422 std::pair RemoveLineBreakTag(std::string value) { std::string text = value; const std::size_t position = text.find(""); if (position == std::string::npos) { return std::pair(false, value); } std::string head = text.substr(0, position); const std::string tail = text.substr(position + std::strlen("")); head.append(tail.c_str(), tail.size()); return std::pair(true, head); } // OFFSET: 0x00833550 // FUNCTION: fn_10411 void DisplayNavigationLink(std::string& output, std::string value) { std::string text = value; output = text; const auto begin = text.find(g_naviLinkTag); if (begin != std::string::npos) { output.clear(); const std::string head = text.substr(0, begin); text = text.substr(begin); const auto end = text.find(g_naviLinkCloseTag); if (end != std::string::npos) { const std::string rest = text.substr(end + std::strlen(g_naviLinkCloseTag.c_str())); text = text.substr(0, end + std::strlen(g_naviLinkCloseTag.c_str())); const std::string link = FormatNavigationLink(text, true); output += head; output += link; output += rest; } } } // OFFSET: 0x00833A20 // FUNCTION: fn_10414 bool ExtractInnerItemLink(std::string& text, std::string& link, int& offset, const char* source) { std::size_t closePos = text.find(g_itemLinkCloseTag); if (closePos == std::string::npos) { return false; } do { text = text.substr(0, closePos); std::size_t openPos = text.find(g_itemLinkTag); if (closePos != std::string::npos && openPos == std::string::npos) { offset += g_itemLinkCloseTag.size(); text = source; text = text.substr(offset); return true; } closePos = text.find(g_itemLinkCloseTag); if (openPos != std::string::npos) { for (;;) { offset += openPos; text = text.substr(openPos + g_itemLinkTag.size()); openPos = text.find(g_itemLinkTag); closePos = text.find(g_itemLinkCloseTag); if (openPos == std::string::npos) { break; } offset += g_itemLinkTag.size(); } link = text; text = source; text = text.substr(link.size() + offset + g_itemLinkTag.size() + g_itemLinkCloseTag.size()); } } while (closePos != std::string::npos); return true; } // OFFSET: 0x00834980 // FUNCTION: fn_10417 void ExtractNavigationLink(std::string& output, std::string value) { output = value; const auto begin = value.find(""); if (begin != std::string::npos) { output = value.substr(begin); const auto end = output.find(""); if (end != std::string::npos) { output.resize(end + 8); } } } // OFFSET: 0x00834160 // FUNCTION: fn_10415 static void SplitMessageArguments(const char* text, std::vector& arguments) { std::string remaining = text; std::size_t separator = remaining.find(','); while (separator != std::string::npos) { std::string argument = remaining.substr(0, separator); arguments.emplace_back(argument.data(), argument.size()); remaining = remaining.substr(separator + 1); separator = remaining.find(','); } if (!remaining.empty()) { arguments.emplace_back(remaining.data(), remaining.size()); } } // OFFSET: 0x00834C30 // FUNCTION: fn_10418 std::string FormatMessageArguments(std::string value, int messageId) { const std::string text(value.data(), value.size()); std::string result = LookupMessage(messageId); const std::size_t separator = text.find(','); if (separator != std::string::npos) { const std::string tail = text.substr(separator + 1); std::vector arguments; arguments.reserve(8); SplitMessageArguments(tail.c_str(), arguments); for (std::size_t index = 0; index < arguments.size(); ++index) { const std::size_t percent = result.find('%'); if (percent != std::string::npos) { unsigned length = 1; char conversion[8] = {}; std::size_t position = percent + 1; for (;;) { strcpy_s(conversion, sizeof(conversion), result.substr(position, 1).c_str()); if (conversion[0] >= 'A') { break; } ++length; ++position; } const std::size_t end = percent + length + 1; const std::string format = result.substr(0, end); const std::string remainder = result.substr(end); char buffer[164] = {}; std::string& argument = arguments[index]; if (_mbsstr(reinterpret_cast(conversion), reinterpret_cast("s"))) { if (argument.find("MsgStr(") != std::string::npos) { argument = argument.substr(7); argument = argument.substr(0, argument.size() - 1); const std::string nested = LookupMessage(std::atoi(argument.c_str())); std::sprintf(buffer, format.c_str(), nested.c_str()); } else { std::sprintf(buffer, format.c_str(), argument.c_str()); } } else if (_mbsstr(reinterpret_cast(conversion), reinterpret_cast("d"))) { std::sprintf(buffer, format.c_str(), std::atoi(argument.c_str())); } else if (_mbsstr(reinterpret_cast(conversion), reinterpret_cast("f"))) { std::sprintf(buffer, format.c_str(), std::atof(argument.c_str())); } result = buffer + remainder; } } } return result; } // OFFSET: 0x0083B250 // FUNCTION: fn_10473 void LineCutter::UIText::Slot25(int x, int y) { if (this->parent) { this->parent->Slot25(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B220 // FUNCTION: fn_10472 void LineCutter::UIText::Slot26(int x, int y) { if (this->parent) { this->parent->Slot26(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B400 // FUNCTION: fn_10476 void LineCutter::UIText::Slot27(int x, int y) { if (this->parent) { this->parent->Slot27(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B430 // FUNCTION: fn_10477 void LineCutter::UIText::ResetCursor(int x, int y) { if (this->parent) { this->parent->ResetCursor(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B280 // FUNCTION: fn_10474 void LineCutter::UIText::Slot31(int x, int y) { if (this->parent) { this->parent->Slot31(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B630 // FUNCTION: fn_10480 void LineCutter::UIText::Slot32(int x, int y) { if (this->parent) { this->parent->Slot32(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B660 // FUNCTION: fn_10481 void LineCutter::UIText::Slot33(int x, int y) { if (this->parent) { this->parent->Slot33(x + this->geometry.x, y + this->geometry.y); } } // OFFSET: 0x0083B690 // FUNCTION: fn_10482 void LineCutter::UIText::Slot35(int value) { if (this->parent) { this->parent->Slot35(value); } } // OFFSET: 0x0083B150 // FUNCTION: fn_10471 void LineCutter::UIText::Paint() { this->ClearSurface(0xFFFF00FF); const TextLocale locale = {}; if (this->m_shadowed) { this->DrawTextAt( g_fontCache, locale, 1, 1, this->m_text.c_str(), this->m_text.size(), 0, this->m_fontHeight, this->m_shadowColor, this->m_bold, this->m_italic ); } if (this->m_outlined == 1) { DrawOutlinedText( *this, 1, 1, this->m_text.c_str(), this->m_text.size(), this->m_color, this->m_outlineColor, 0, this->m_fontHeight, this->m_bold, this->m_italic ); } else { this->DrawTextAt( g_fontCache, locale, 0, 0, this->m_text.c_str(), this->m_text.size(), 0, this->m_fontHeight, this->m_color, this->m_bold, this->m_italic ); } } // OFFSET: 0x0083B460 // FUNCTION: fn_10478 void UIEmotion::Slot19() { if (!this->m_action || this->m_action->Speed(this->m_animation) == 0.0f) { return; } const float elapsed = static_cast(timeGetTime() - this->m_startTime); const unsigned frames = static_cast(elapsed / (this->m_action->Speed(this->m_animation) * 24.0f)); if (this->m_maxLoops != 0) { if (this->m_loops > this->m_maxLoops) { return; } if (frames >= this->m_action->FrameCount(this->m_animation) * this->m_loops) { this->m_loops = this->m_loops + 1; } } this->m_frame = frames % this->m_action->FrameCount(this->m_animation); this->Invalidate(); } // OFFSET: 0x0083B0A0 // FUNCTION: fn_10470 void UIItemImage::Paint() { if (!this->geometry.surface || this->m_itemId.empty()) { return; } this->ClearSurface(0xFFFF00FF); char path[128] = {}; ItemDescriptions descriptions{g_itemDescriptions}; descriptions.IconPath(path, this->m_itemId.c_str(), 1); CBitmapRes* bitmap = static_cast(ResourceMgr::Instance().Load(Resolve(path))); if (bitmap) { this->DrawBitmap(0, 0, &bitmap->image, true); } } // OFFSET: 0x00836970 // FUNCTION: fn_10433 CCreateLineInfo::CCreateLineInfo( UIWindow* parent, const char* text, int width, int lineSpacing, std::uint32_t color, int fontHeight, LineFontFlags flags, bool unknown0028, int unknown002C, bool unknown0030, int unknown0034, unsigned char align ) : m_align(align), m_newSegment(true), m_height(0) { if (parent && (this->m_parent = parent, text)) { this->m_text.assign(text, std::strlen(text)); this->m_width = width; this->m_lineSpacing = lineSpacing; this->m_defaultColor = color; this->m_color = color; this->m_defaultFontHeight = fontHeight; this->m_fontHeight = fontHeight; this->m_bold = flags.bold; this->m_italic = flags.italic; this->unknown_0030 = unknown0030; this->unknown_0034 = unknown0034; this->unknown_0028 = unknown0028; this->unknown_002C = unknown002C; if (!s_fontStackCache.empty()) { this->m_fontStack.swap(s_fontStackCache); } this->Rebuild(0); } } // OFFSET: 0x00837390 // FUNCTION: fn_10441 CCreateLineInfo::~CCreateLineInfo() { if (!this->m_fontStack.empty()) { this->m_fontStack.swap(s_fontStackCache); } } // OFFSET: 0x008379D0 // FUNCTION: fn_10448 bool CCreateLineInfo::CompareTag(std::string text, std::string keyword, bool exactMatch, bool caseSensitive) { if (text.empty() || keyword.empty()) { return false; } if (!caseSensitive) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); std::transform(keyword.begin(), keyword.end(), keyword.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); } if (exactMatch == true) { return text.compare(keyword) == 0; } return text.compare(0, keyword.size(), keyword) == 0; } // OFFSET: 0x00837B40 // FUNCTION: fn_10449 void CCreateLineInfo::AppendText(const std::string& text) { if (this->m_segments.empty()) { this->AddSegment(LINE_SEGMENT_TEXT); } this->m_segments.back().text += text; } // OFFSET: 0x00837B80 // FUNCTION: fn_10450 void CCreateLineInfo::AddWindow(UIWindow* window) { if (this->m_lines.empty()) { this->NewLine(); } this->m_lines.back().push_back(window); } // OFFSET: 0x00837BC0 // FUNCTION: fn_10451 int CCreateLineInfo::LayoutEmotion(int x, const std::string& text) { int group = 0; int id = -1; std::cmatch match; if (!std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^e\\[(\\d{1,3})\\](\\[\\d\\])*"))) { if (!std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^e\\[(\\d{1,3}\\s*,\\s*\\d{1,3})\\](\\[\\d\\])*"))) { return x; } const std::vector parts = Split(match[1].str(), ","); if (parts.size() == 2) { group = std::stoi(parts[0]); id = std::stoi(parts[1]); } } else { id = std::stoi(match[1].str()); } if (id >= 0) { int loops = 0; if (match.size() > 2 && match[2].matched) { const std::string tail = match[2].str(); std::cmatch loopMatch; if (std::regex_search(tail.c_str(), tail.c_str() + tail.size(), loopMatch, std::regex("^\\[(\\d)\\]"))) { loops = std::atoi(loopMatch[1].str().c_str()); } } CashEmotionData* data = g_cashEmotionMgr->GetEmotionData(group, id); if (data) { const char* sprite = g_cashEmotionMgr->GetSprFileName(data->actId); if (sprite) { const char* action = g_cashEmotionMgr->GetPackName(data->sprId); if (action) { UIEmotion* emotion = new UIEmotion(*reinterpret_cast(nullptr), sprite, action, data->unknown_0020, loops); emotion->SetSize(50, 50); emotion->Invalidate(); if (this->m_width < emotion->geometry.width + x) { this->NewLine(); this->AddWindow(emotion); x = emotion->geometry.width; } else { this->AddWindow(emotion); x += emotion->geometry.width; } } } } } return x; } // OFFSET: 0x00838130 // FUNCTION: fn_10452 int CCreateLineInfo::LayoutLink(int x, std::string text, int type, LineTextStyle style) { const std::tuple info = SplitLinkInfo(text, type); if (std::get<0>(info)) { std::string display = std::get<1>(info); if (this->m_parent) { display = RemoveTextColors(display.c_str(), 0); } const std::vector parts = SplitString(std::get<2>(info), ","); const int width = MeasureText(this->m_parent, display, style).cx; if (width > this->m_width) { const std::vector lines = SplitLines(this->m_parent, display, style, this->m_width); for (auto line = lines.begin(); line != lines.end(); ++line) { this->NewLine(); x = 0; UITextButton* button = this->CreateLinkWindow(*line, parts, type, style); if (button) { this->AddWindow(button); x = button->geometry.width; } } } else { if (x + width > this->m_width) { this->NewLine(); x = 0; } if (!display.empty() && !parts.empty()) { UITextButton* button = this->CreateLinkWindow(display, parts, type, style); this->AddWindow(button); x += button->geometry.width; } } } return x; } // OFFSET: 0x00838780 // FUNCTION: fn_10453 int CCreateLineInfo::LayoutItemImage(int x, const std::string& text) { std::cmatch match; if (std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^i\\[(\\d{1,7})\\]*"))) { const std::string itemId = match[1].str(); UIItemImage* image = new (std::nothrow) UIItemImage(*reinterpret_cast(nullptr), itemId); if (image) { image->SetSize(25, 25); image->Invalidate(); if (this->m_width < image->geometry.width + x) { this->NewLine(); this->AddWindow(image); x = image->geometry.width; } else { this->AddWindow(image); x += image->geometry.width; } } } return x; } // OFFSET: 0x008389A0 // FUNCTION: fn_10454 int CCreateLineInfo::LayoutText(int x, std::string text, LineTextStyle style) { const int width = MeasureText(this->m_parent, text, style).cx; if (this->m_width < width) { const std::string first = FitText(this->m_parent, text, style, this->m_width - x); UIWindow* last = this->CreateTextWindow(first, style); if (last) { this->AddWindow(last); } const std::string rest = text.substr(first.size(), text.size() - first.size()); const std::vector lines = SplitLines(this->m_parent, rest, style, this->m_width); if (!lines.empty()) { x = 0; } for (auto line = lines.begin(); line != lines.end(); ++line) { this->NewLine(); last = this->CreateTextWindow(*line, style); if (last) { this->AddWindow(last); } } if (last) { x += last->geometry.width; } } else { if (this->m_width < x + width) { this->NewLine(); x = 0; } UIWindow* window = this->CreateTextWindow(text, style); if (window) { this->AddWindow(window); x += window->geometry.width; } } return x; } // OFFSET: 0x00838D40 // FUNCTION: fn_10455 void CCreateLineInfo::BuildWindows() { int x = 0; for (auto segment = this->m_segments.begin(); segment != this->m_segments.end(); ++segment) { const std::string text = segment->text; switch (segment->type) { case LINE_SEGMENT_URL: case LINE_SEGMENT_NAVI: case LINE_SEGMENT_ITEM: case LINE_SEGMENT_TIPBOX: case LINE_SEGMENT_QUEST: x = this->LayoutLink(x, text, segment->type, segment->style); break; case LINE_SEGMENT_MESSAGE: x = this->LayoutText(x, g_navigationLinkParser->ExpandMessages(text), segment->style); break; case LINE_SEGMENT_EMOTION: x = this->LayoutEmotion(x, text); break; case LINE_SEGMENT_ITEM_IMAGE: x = this->LayoutItemImage(x, text); break; case LINE_SEGMENT_TEXT: if (text == "\n") { this->NewLine(); x = 0; } else { x = this->LayoutText(x, text, segment->style); } break; } } } // OFFSET: 0x008392C0 // OFFSET: 0x008392C0 // OFFSET: 0x00839310 // FUNCTION: fn_10457 std::size_t CCreateLineInfo::FindTag(std::string text, std::string keyword, bool caseSensitive) { if (text.empty() || keyword.empty()) { return std::string::npos; } if (!caseSensitive) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); std::transform(keyword.begin(), keyword.end(), keyword.begin(), [](unsigned char ch) { return static_cast(std::toupper(ch)); }); } return text.find(keyword, 0); } // OFFSET: 0x00839420 // FUNCTION: fn_10458 std::vector CCreateLineInfo::SplitLines(UIWindow* window, std::string text, const LineTextStyle& style, int width) { std::vector lines; const int length = static_cast(text.size()); int position = 0; while (position < length) { const std::string rest = text.substr(position, length - position); if (MeasureText(window, rest, style).cx + 1 <= width) { lines.push_back(text.substr(position, length - position)); break; } const std::string line = FitText(window, rest, style, width); lines.push_back(line); position += static_cast(line.size()); } return lines; } // OFFSET: 0x00839860 // FUNCTION: fn_10459 std::string CCreateLineInfo::FitText(UIWindow* window, std::string text, const LineTextStyle& style, int width) { const int length = static_cast(text.size()); int position = 0; int total = 0; while (position < length) { const std::string rest = text.substr(position, length - position); const std::string character = NextCharacter(rest); if (!character.empty()) { total += MeasureText(window, character, style).cx; if (total > width) { return text.substr(0, position); } position += static_cast(character.size()); } } return text; } // OFFSET: 0x00839D60 // FUNCTION: fn_10460 std::pair CCreateLineInfo::GetLinkTags(int type) { switch (type) { case LINE_SEGMENT_URL: return std::make_pair(g_urlTag, g_urlCloseTag); case LINE_SEGMENT_NAVI: return std::make_pair(g_naviTag, g_naviCloseTag); case LINE_SEGMENT_ITEM: return std::make_pair(g_itemTag, g_itemCloseTag); case LINE_SEGMENT_ITEM_LINK: return std::make_pair(g_itemLinkTag, g_itemLinkCloseTag); case LINE_SEGMENT_TIPBOX: return std::make_pair(g_tipBoxTag, g_tipBoxCloseTag); case LINE_SEGMENT_QUEST: return std::make_pair(g_questTag, g_questCloseTag); case LINE_SEGMENT_NAVI_LINK: return std::make_pair(g_naviLinkTag, g_naviLinkCloseTag); case LINE_SEGMENT_MESSAGE: return std::make_pair(g_messageTag, g_messageCloseTag); default: return std::pair("", ""); } } // OFFSET: 0x00839FA0 // FUNCTION: fn_10461 tagSIZE CCreateLineInfo::MeasureText(UIWindow* window, std::string text, const LineTextStyle& style) { if (!window) { return tagSIZE{0, 0}; } const TextLocale locale = {}; return window->TextSize(g_fontCache, locale, text.c_str(), text.size(), 0, style.fontHeight, style.bold, style.italic); } // OFFSET: 0x0083A180 // FUNCTION: fn_10462 void CCreateLineInfo::ApplyFont(std::string tag) { bool changed = false; std::size_t position = FindTag(tag, "SIZE", false); if (position != std::string::npos) { const std::string value = tag.substr(position, tag.size() - position); int size = 0; std::sscanf(value.c_str(), "%*[^=]=%d", &size); this->m_fontHeight = size != 0 ? size : this->m_fontHeight; changed = true; } position = FindTag(tag, "COLOR", false); if (position != std::string::npos) { const std::string value = tag.substr(position, tag.size() - position); char digits[7] = {}; std::sscanf(value.c_str(), "%*[^=]=%6s", digits); char* end = nullptr; const unsigned long color = std::strtol(digits, &end, 16); if (end > digits && end - digits == 6) { changed = true; this->m_color = RGB((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF); } } if (changed == true) { this->m_fontStack.push_back(LineFontState{this->m_fontHeight, this->m_color}); this->m_newSegment = true; } } // OFFSET: 0x0083A820 // FUNCTION: fn_10464 int CCreateLineInfo::AddLink(int index, int type) { const std::pair tags = GetLinkTags(type); const std::size_t end = this->m_text.find(tags.second, index); if (end != std::string::npos) { const std::string link = this->m_text.substr(index, end + tags.second.size() - index); this->AddSegment(type); this->AppendText(link); this->m_newSegment = true; index = static_cast(end + tags.second.size()); } return index; } // OFFSET: 0x0083A5F0 // OFFSET: 0x0083ACF0 // FUNCTION: fn_10467 UIWindow* CCreateLineInfo::CreateTextWindow(std::string text, const LineTextStyle& style) { if (text.empty()) { return nullptr; } const tagSIZE size = MeasureText(this->m_parent, text, style); LineCutter::UIText* window = new LineCutter::UIText(*reinterpret_cast(nullptr)); window->SetSize(size.cx, size.cy); window->m_text = text; window->m_outlineColor = this->unknown_002C; window->m_shadowed = this->unknown_0030; window->m_color = style.color; window->m_fontHeight = style.fontHeight; window->m_bold = style.bold; window->m_italic = style.italic; window->m_outlined = this->unknown_0028; window->m_shadowColor = this->unknown_0034; if (window->m_outlined == 1) { const TextLocale locale = {}; const int textWidth = window->TextWidth(g_fontCache, locale, window->m_text.c_str(), 0, 0, style.fontHeight, style.bold, style.italic); window->Resize(textWidth + 2, style.fontHeight + 2); } window->Invalidate(); return window; } // OFFSET: 0x0083AA40 // FUNCTION: fn_10465 void CCreateLineInfo::NewLine() { std::vector line; this->m_lines.push_back(line); } // OFFSET: 0x0083AB90 // FUNCTION: fn_10466 void CCreateLineInfo::AddSegment(int type) { LineTextSegment segment(std::string(), LineTextStyle{this->m_color, this->m_fontHeight, this->m_bold > 0, this->m_italic > 0}); segment.type = type; this->m_segments.push_back(segment); this->m_newSegment = false; } // OFFSET: 0x0083AF20 // FUNCTION: fn_10468 void CCreateLineInfo::ParseText() { const int length = static_cast(this->m_text.size()); int index = 0; while (index < length) { int next = this->ParseTag(index); if (index == next) { next = this->ParseChar(index); } index = next; } } // OFFSET: 0x0083B6B0 // FUNCTION: fn_10483 void ShowTextButtonItem(UITextButton* button) { if (!button) { return; } if (button->m_lines.size() == 0) { return; } const std::string itemId = button->GetLine(0); const int index = std::atoi(button->GetLine(1).c_str()); ITEM_INFO item; item.unknown_0000[0] = static_cast(index); item.itemId = itemId; item.identified = 1; UIWindow* window = g_windowMgr.EnsureWindow(0xC); if (window) { window->SendMsg(nullptr, 0x18, reinterpret_cast(&item), 0, 0, 0); } } // OFFSET: 0x0083BB50 // FUNCTION: fn_10486 void ShowTextButtonTip(UITextButton* button) { if (!button) { return; } if (button->m_lines.size() == 0) { return; } const int tip = std::atoi(button->GetLine(0).c_str()); UITipboxWnd* window = static_cast(g_windowMgr.EnsureWindow(0x13B)); if (window) { window->ShowTip(tip); } } // OFFSET: 0x0083BBC0 // FUNCTION: fn_10487 void OpenTextButtonLink(UITextButton* button) { if (!button) { return; } if (button->m_lines.size() == 0) { return; } const std::string line = button->GetLine(0); if (!line.empty()) { ShellExecuteA(hWnd, "open", line.c_str(), nullptr, nullptr, SW_SHOWNORMAL); } } // OFFSET: 0x0083BD80 // FUNCTION: fn_10488 int CCreateLineInfo::ParseEmotion(int index) { const std::string text = this->m_text.substr(index, this->m_text.size() - index); std::cmatch match; bool missing = !std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^e\\[\\d{1,3}\\](\\[\\d\\])*")); if (missing) { missing = !std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^e\\[\\d{1,3}\\s*,\\s*\\d{1,3}\\](\\[\\d\\])*")); if (missing) { return index; } } if (match.size() >= 2) { this->AddSegment(LINE_SEGMENT_EMOTION); this->AppendText(match[0].str()); this->m_newSegment = true; index += static_cast(match[0].length()); } return index; } // INLINED inline void CCreateLineInfo::CloseFont(std::string tag) { if (!this->m_fontStack.empty()) { this->m_fontStack.pop_back(); if (this->m_fontStack.empty()) { this->m_fontHeight = this->m_defaultFontHeight; this->m_color = this->m_defaultColor; } else { this->m_fontHeight = this->m_fontStack.back().fontHeight; this->m_color = this->m_fontStack.back().color; } this->m_newSegment = true; } } // INLINED inline void CCreateLineInfo::OpenBold(std::string tag) { if (this->m_bold == 0) { this->m_newSegment = true; } ++this->m_bold; } // INLINED inline void CCreateLineInfo::CloseBold(std::string tag) { if (--this->m_bold == 0) { this->m_newSegment = true; } } // INLINED inline void CCreateLineInfo::OpenItalic(std::string tag) { if (this->m_italic == 0) { this->m_newSegment = true; } ++this->m_italic; } // INLINED inline void CCreateLineInfo::CloseItalic(std::string tag) { if (--this->m_italic == 0) { this->m_newSegment = true; } } // OFFSET: 0x0083BFF0 // FUNCTION: fn_10489 int CCreateLineInfo::ParseMarkup(int index) { if (this->m_text.at(index) != '<') { return index; } const std::size_t end = this->m_text.find('>', index); if (end == std::string::npos) { return index; } const std::string tag = this->m_text.substr(index + 1, end - index - 1); if (CompareTag(tag, "FONT", false, false) == true) { this->ApplyFont(tag); } else if (CompareTag(tag, "/FONT", true, false) == true) { this->CloseFont(tag); } else if (CompareTag(tag, "B", true, false) == true) { this->OpenBold(tag); } else if (CompareTag(tag, "/B", true, false) == true) { this->CloseBold(tag); } else if (CompareTag(tag, "I", true, false) == true) { this->OpenItalic(tag); } else if (CompareTag(tag, "/I", true, false) == true) { this->CloseItalic(tag); } else { return index; } return static_cast(end + 1); } // OFFSET: 0x0083C3D0 // FUNCTION: fn_10490 int CCreateLineInfo::ParseLink(int index) { const std::string text = this->m_text.substr(index, this->m_text.size() - index); int type; if (CompareTag(text, g_urlTag, false, true) == true) { type = LINE_SEGMENT_URL; } else if (CompareTag(text, g_itemTag, false, true) == true) { type = LINE_SEGMENT_ITEM; } else if (CompareTag(text, g_itemLinkTag, false, true) == true) { type = LINE_SEGMENT_ITEM_LINK; } else if (CompareTag(text, g_naviTag, false, true) == true) { type = LINE_SEGMENT_NAVI; } else if (CompareTag(text, g_naviLinkTag, false, true) == true) { type = LINE_SEGMENT_NAVI_LINK; } else if (CompareTag(text, g_messageTag, false, true) == true) { type = LINE_SEGMENT_MESSAGE; } else if (CompareTag(text, g_questTag, false, true) == true) { type = LINE_SEGMENT_QUEST; } else if (CompareTag(text, g_tipBoxTag, false, true) == true) { type = LINE_SEGMENT_TIPBOX; } else { return index; } return this->AddLink(index, type); } // OFFSET: 0x0083C6A0 // FUNCTION: fn_10491 int CCreateLineInfo::ParseItemImage(int index) { const std::string text = this->m_text.substr(index, this->m_text.size() - index); std::cmatch match; const bool missing = !std::regex_search(text.c_str(), text.c_str() + text.size(), match, std::regex("^\\^i(\\[\\d{1,7}\\])*")); if (!missing && match.size() >= 2) { this->AddSegment(LINE_SEGMENT_ITEM_IMAGE); this->AppendText(match[0].str()); this->m_newSegment = true; index += static_cast(match[0].length()); } return index; } // OFFSET: 0x0083C890 // FUNCTION: fn_10492 int CCreateLineInfo::ParseItemName(int index) { const std::string keyword = "^nItemID^"; if (index == static_cast(this->m_text.find(keyword, index))) { std::string digits; for (long long position = index + keyword.size(); position < this->m_text.size(); ++position) { const char ch = this->m_text.at(static_cast(position)); if (!g_colorHexDigit[static_cast(ch)]) { break; } digits.push_back(ch); } if (!digits.empty()) { ITEM_INFO item; item.SetItemId(std::atoi(digits.c_str())); char name[260]; std::memset(name, 0, sizeof(name)); int capacity = sizeof(name); ItemDescriptions descriptions{g_itemDescriptions}; descriptions.Name(item, name, capacity, 0); this->AddSegment(LINE_SEGMENT_TEXT); this->AppendText(std::string(name)); index += static_cast(digits.size()) + 9; this->m_newSegment = true; } } return index; } // OFFSET: 0x0083CDA0 // FUNCTION: fn_10493 int CCreateLineInfo::ParseChar(int index) { const std::string text = this->m_text.substr(index, this->m_text.size() - index); const std::string character = NextCharacter(text); if (character.empty()) { return index + 1; } if (character == "\n") { this->AddSegment(LINE_SEGMENT_TEXT); this->AppendText(character); this->m_newSegment = true; } else if (character != "\r") { if (character == " ") { if (index > 0) { const std::string previousText = this->m_text.substr(index - 1, this->m_text.size() - (index - 1)); const std::string previous = NextCharacter(previousText); if (previous == "\n") { this->AddSegment(LINE_SEGMENT_TEXT); } this->AppendText(character); this->m_newSegment = true; } else { this->AppendText(character); this->m_newSegment = true; } } else { if (this->m_newSegment == true) { this->AddSegment(LINE_SEGMENT_TEXT); } this->AppendText(character); } } return index + static_cast(character.size()); } // OFFSET: 0x0083CFE0 // FUNCTION: fn_10494 std::string CCreateLineInfo::NextCharacter(std::string text) { const char* begin = text.c_str(); const char* end = CharNextExA(static_cast(g_textCodePage), begin, 0); if (!end) { return ""; } return std::string(begin, end); } // OFFSET: 0x0083D090 // FUNCTION: fn_10495 int CCreateLineInfo::ParseTag(int index) { if (this->m_text.at(index) == '^') { int next = index; const std::string code = this->m_text.substr(index + 1, 6); const char* digits = code.c_str(); if (digits) { char* end = nullptr; const unsigned long value = std::strtol(digits, &end, 16); if (end > digits && end - digits == 6) { next = index + 7; const std::uint32_t color = RGB((value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); this->m_newSegment = true; this->m_defaultColor = color; this->m_color = color; } } if (index != next) { return next; } } int next = this->ParseItemName(index); if (index != next) { return next; } next = this->ParseEmotion(index); if (index != next) { return next; } next = this->ParseItemImage(index); if (index != next) { return next; } next = this->ParseLink(index); if (index != next) { return next; } next = this->ParseMarkup(index); return index != next ? next : index; } // OFFSET: 0x0083D1F0 // FUNCTION: fn_10496 void CCreateLineInfo::ArrangeLines() { int y = 0; for (auto lineIt = this->m_lines.begin(); lineIt != this->m_lines.end(); ++lineIt) { std::vector& line = *lineIt; unsigned lineHeight = 0; for (UIWindow* window : line) { lineHeight = std::max(lineHeight, static_cast(window->geometry.height)); } switch (this->m_align) { case 0: { int x = 0; for (auto it = line.begin(); it != line.end(); ++it) { UIWindow* window = *it; if (window) { window->Move(x, lineHeight - window->geometry.height + y); x += window->geometry.width; } } break; } case 1: { int x = this->m_width; for (auto it = line.rbegin(); it != line.rend(); ++it) { UIWindow* window = *it; if (window) { x -= window->geometry.width; window->Move(x, lineHeight - window->geometry.height + y); } } break; } case 2: { int width = 0; for (UIWindow* window : line) { width += window->geometry.width; } int x = static_cast(this->m_width - width) / 2; for (auto it = line.begin(); it != line.end(); ++it) { UIWindow* window = *it; if (window) { window->Move(x, lineHeight - window->geometry.height + y); x += window->geometry.width; } } break; } } y += this->m_lineSpacing + lineHeight; } this->m_height = y - this->m_lineSpacing; } // OFFSET: 0x0083D360 // FUNCTION: fn_10497 std::tuple CCreateLineInfo::SplitLinkInfo(std::string text, int type) { const std::pair tags = GetLinkTags(type); const std::string open = tags.first; const std::string close = tags.second; if (text.empty() || open.empty() || close.empty()) { return std::tuple(false, "", ""); } const std::size_t begin = text.find(open, 0); if (begin == std::string::npos) { return std::tuple(false, "", ""); } if (text.find(close, begin) != std::string::npos) { const std::size_t infoBegin = text.find(g_infoTag, begin); if (infoBegin != std::string::npos) { const std::size_t infoEnd = text.find(g_infoCloseTag, infoBegin); if (infoEnd != std::string::npos) { const std::string name = text.substr(begin + open.size(), infoBegin - open.size() - begin); const std::string info = text.substr(infoBegin + g_infoTag.size(), infoEnd - g_infoTag.size() - infoBegin); return std::tuple(true, name, info); } } } return std::tuple(false, "", ""); } // OFFSET: 0x0083D660 // FUNCTION: fn_10498 std::vector CCreateLineInfo::SplitString(std::string text, const std::string& delimiter) { std::vector parts; if (text.empty()) { return parts; } std::size_t position = text.find(delimiter, 0); while (position != std::string::npos) { parts.push_back(text.substr(0, position)); text = text.substr(position + delimiter.size()); position = text.find(delimiter, 0); } parts.push_back(text); return parts; } // OFFSET: 0x0083AF50 // FUNCTION: fn_10469 void UIEmotion::Paint() { if (!this->geometry.surface) { return; } this->ClearSurface(0xFFFF00FF); if (!this->m_sprite || !this->m_action) { return; } const ActionFrame* frame = &this->m_action->Frame(this->m_animation, this->m_frame); if (!frame) { return; } const std::uint32_t* palette = this->m_sprite->palette; if (!palette) { return; } for (unsigned index = 0; index < frame->cells.size(); ++index) { const SpriteFrameCell& cell = frame->Cell(index); if (cell.image == -1 || cell.imageType >= 2) { continue; } const std::vector& images = this->m_sprite->images[cell.imageType]; if (static_cast(cell.image) < images.size()) { SpriteRleImage* image = images[cell.image]; if (image) { this->AddRotatedSpriteCellOverlay( static_cast(this->geometry.width * 0.5), static_cast(this->geometry.height * 0.5), reinterpret_cast(this->m_action), reinterpret_cast(this->m_sprite), image, &cell, palette ); } } } } // OFFSET: 0x0083B2B0 // FUNCTION: fn_10475 void UIItemImage::Slot28(int, int) { ITEM_INFO item; item.itemId = this->m_itemId; item.identified = 1; int x = 0; int y = -17; this->ToScreen(x, y); if (g_charViewMode) { ModeTooltip* tooltip = reinterpret_cast(g_charViewMode); const std::uint32_t color = g_session.GetItemNameColor(item); ItemDescriptions descriptions{g_itemDescriptions}; bool languagePrefix = false; WORD language = g_serverLanguage->language; ItemNaming naming{descriptions, g_itemCardNames, *g_playerNameDictionary, languagePrefix, g_serviceType, language}; tooltip->Show(naming.DisplayName(item, 0).c_str(), x, y, color, 0, 1); } } // OFFSET: 0x0083B550 // FUNCTION: fn_10479 void UIItemImage::Slot32(int, int) { ITEM_INFO item; item.itemId = this->m_itemId; item.identified = 1; UIWindow* window = g_windowMgr.EnsureWindow(0xC); if (window) { window->SendMsg(nullptr, 0x18, reinterpret_cast(&item), 0, 0, 0); } } // OFFSET: 0x00832000 // FUNCTION: fn_10406 bool NavigationLinkParser::HasUnownedItemLink(std::string text) { if (!StrStrIA(text.c_str(), g_itemLinkTag.c_str())) { return false; } std::string work = text; int offset = 0; for (;;) { std::string link; if (!ExtractInnerItemLink(work, link, offset, text.c_str())) { return false; } const int itemId = ItemLinkMessageId(link); if (link.substr(0, 6) == "00000T" && itemId <= g_itemTagOwner->unknown_0118) { offset += static_cast(link.size() + g_itemLinkTag.size() + g_itemLinkCloseTag.size()); continue; } const bool unowned = !(LookupClientItem(g_session.m_inventoryItems, itemId, 0).unknown_0000[1] > 0 || g_session.HasItemId(itemId) || g_session.HasItem1740Number(itemId)); if (!unowned) { offset += static_cast(link.size() + g_itemLinkTag.size() + g_itemLinkCloseTag.size()); continue; } g_windowMgr.SendNotice(1, reinterpret_cast(LookupMessage(0xE8D)), 0xFF, 0, 0, 0); return true; } }