/** * @file chatlog.cpp * * Implementation of the in-game chat log. */ #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "chatlog.h" #include "control/control.hpp" #include "diablo_msg.hpp" #include "doom.h" #include "engine/render/text_render.hpp" #include "gamemenu.h" #include "help.h" #include "inv.h" #include "minitext.h" #include "stores.h" #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { namespace { struct ColoredText { std::string text; UiFlags color; }; struct MultiColoredText { std::string text; std::vector colors; }; bool UnreadFlag = false; size_t SkipLines; unsigned int MessageCounter = 0; std::vector ChatLogLines; constexpr int PaddingTop = 32; constexpr int PaddingLeft = 32; constexpr int PanelHeight = 297; constexpr int ContentTextWidth = 577; int LineHeight() { return IsSmallFontTall() ? 18 : 14; } int BlankLineHeight() { return 12; } int DividerLineMarginY() { return BlankLineHeight() / 2; } int HeaderHeight() { return PaddingTop + LineHeight() + 2 * BlankLineHeight() + DividerLineMarginY(); } int ContentPaddingY() { return BlankLineHeight(); } int ContentsTextHeight() { return PanelHeight - HeaderHeight() - DividerLineMarginY() - 2 * ContentPaddingY() - BlankLineHeight(); } int NumVisibleLines() { return (ContentsTextHeight() - 1) / LineHeight() + 1; // Ceil } } // namespace bool ChatLogFlag = false; void ToggleChatLog() { if (ChatLogFlag) { ChatLogFlag = false; } else { ActiveStore = TalkID::None; CloseInventory(); CloseCharPanel(); SpellbookFlag = false; SpellSelectFlag = false; if (qtextflag && leveltype == DTYPE_TOWN) { qtextflag = false; stream_stop(); } QuestLogIsOpen = false; HelpFlag = false; CancelCurrentDiabloMsg(); gamemenu_off(); SkipLines = 0; ChatLogFlag = true; doom_close(); } } void AddMessageToChatLog(std::string_view message, Player *player, UiFlags flags) { MessageCounter++; const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); std::string timestamp = localtimeResult != nullptr ? StrCat("[#", MessageCounter, "] ", LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')) : StrCat("[#", MessageCounter, "] "); const size_t oldSize = ChatLogLines.size(); if (player == nullptr) { ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { timestamp, UiFlags::ColorRed }, { std::string(message), flags } } }); } else { std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->getCharacterLevel()); UiFlags nameColor = player == MyPlayer ? UiFlags::ColorWhitegold : UiFlags::ColorBlue; const std::string prefix = timestamp + " - " + playerInfo; const std::string text = WordWrapString(prefix + std::string(message), ContentTextWidth); std::vector lines; std::stringstream ss(text); for (std::string s; getline(ss, s, '\n');) { lines.push_back(s); } for (int i = static_cast(lines.size()) - 1; i >= 1; i--) { ChatLogLines.emplace_back(MultiColoredText { lines[i], {} }); } lines[0].erase(0, prefix.length()); ChatLogLines.emplace_back(MultiColoredText { "{0} - {1}{2}", { { timestamp, UiFlags::ColorRed }, { playerInfo, nameColor }, { lines[0], UiFlags::ColorWhite } } }); } const size_t diff = ChatLogLines.size() - oldSize; // only autoscroll when on top of the log if (SkipLines != 0) { SkipLines += diff; UnreadFlag = true; } } void DrawChatLog(const Surface &out) { DrawSTextHelp(); DrawQTextBack(out); if (SkipLines == 0) { UnreadFlag = false; } const Point uiPosition = GetUIRectangle().position; const int lineHeight = LineHeight(); const int blankLineHeight = BlankLineHeight(); const int sx = uiPosition.x + PaddingLeft; const int sy = uiPosition.y; DrawString(out, fmt::format(fmt::runtime(_("Chat History (Messages: {:d})")), MessageCounter), { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = (UnreadFlag ? UiFlags::ColorRed : UiFlags::ColorWhitegold) | UiFlags::AlignCenter }); const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); if (localtimeResult != nullptr) { const std::string timestamp = StrCat( LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')); DrawString(out, timestamp, { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold }); } const int titleBottom = sy + HeaderHeight(); DrawSLine(out, titleBottom); const int numLines = NumVisibleLines(); const int contentY = titleBottom + DividerLineMarginY() + ContentPaddingY(); for (int i = 0; i < numLines; i++) { if (i + SkipLines >= ChatLogLines.size()) break; const MultiColoredText &text = ChatLogLines[ChatLogLines.size() - (i + SkipLines + 1)]; const std::string_view line = text.text; std::vector args; for (auto &x : text.colors) { args.emplace_back(DrawStringFormatArg { x.text, x.color }); } DrawStringWithColors(out, line, args, { { sx, contentY + i * lineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhite, .lineHeight = lineHeight }); } DrawString(out, _("Press ESC to end or the arrow keys to scroll."), { { sx, contentY + ContentsTextHeight() + ContentPaddingY() + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter }); } void ChatLogScrollUp() { if (SkipLines > 0) SkipLines--; } void ChatLogScrollDown() { if (SkipLines + NumVisibleLines() < ChatLogLines.size()) SkipLines++; } void ChatLogScrollTop() { SkipLines = 0; } void ChatLogScrollBottom() { SkipLines = ChatLogLines.size() - NumVisibleLines(); } } // namespace devilution