From c92c904970c1947bab941fcaaab094ba8d88e3c5 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Mon, 26 Oct 2026 16:41:11 -0400 Subject: [PATCH] Row 182: a v83 chat-log line carrying an item (BROADCAST_MSG 6, 8, 11) links it: the item megaphone's [Name] is drawn violet (#6C4CE3), and a press on any of the line's rows shows the item's tooltip 20 px below, until the cursor leaves the 40x40 square around the click --- HeavenClient/Graphics/Color.h | 4 +- HeavenClient/IO/Components/Tooltip.h | 4 +- HeavenClient/IO/UIStateGame.cpp | 8 ++ HeavenClient/IO/UITypes/UIChatBar.cpp | 92 ++++++++++++++++++- HeavenClient/IO/UITypes/UIChatBar.h | 24 ++++- .../Net/Handlers/MessagingHandlers.cpp | 43 ++++++--- HeavenClient/Util/ChatLinks.h | 35 +++++++ HeavenClient/Util/Megaphones.h | 16 ++-- HeavenClient/tests/megaphones.cpp | 10 ++ agent_docs/inventory.md | 17 +++- 10 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 HeavenClient/Util/ChatLinks.h diff --git a/HeavenClient/Graphics/Color.h b/HeavenClient/Graphics/Color.h index c332776e5..9c21e2aac 100644 --- a/HeavenClient/Graphics/Color.h +++ b/HeavenClient/Graphics/Color.h @@ -153,6 +153,7 @@ namespace ms FAMILYBLUE, MEDALTEAL, SPOUSEPINK, + ITEMLINK, NUM_COLORS }; @@ -255,7 +256,8 @@ namespace ms {1.00f, 0.78f, 0.00f}, // Script gold (#FFC600, the floating script lines) {0.64f, 0.94f, 1.00f}, // Family blue (#A4F0FF, v83 font 0x10: an empty junior's box in the Family Tree) {0.20f, 0.43f, 0.50f}, // Medal teal (#346E80, a title's name in v83's title window) - {1.00f, 0.44f, 0.75f} // Spouse pink (#FF71BF, v83 chat font 6: spouse chat) + {1.00f, 0.44f, 0.75f}, // Spouse pink (#FF71BF, v83 chat font 6: spouse chat) + {0.42f, 0.30f, 0.89f} // Item link (#6C4CE3, v83 chat font ctx+0xc20: an item megaphone's [item]) }; // Create a color by an array of real numbers [0.0f, 1.0f] diff --git a/HeavenClient/IO/Components/Tooltip.h b/HeavenClient/IO/Components/Tooltip.h index f521221f9..fac242550 100644 --- a/HeavenClient/IO/Components/Tooltip.h +++ b/HeavenClient/IO/Components/Tooltip.h @@ -74,7 +74,9 @@ namespace ms WEDDINGREGISTRY, USERLIST, MEDALS, - TIMEDQUESTS + TIMEDQUESTS, + // An item a chat log line links, shown below the click. + CHATBAR }; virtual ~Tooltip() diff --git a/HeavenClient/IO/UIStateGame.cpp b/HeavenClient/IO/UIStateGame.cpp index 82d685595..e5dad28b4 100644 --- a/HeavenClient/IO/UIStateGame.cpp +++ b/HeavenClient/IO/UIStateGame.cpp @@ -24,6 +24,8 @@ #include "FamilyActions.h" #include "Components/TooltipBox.h" +#include "../Util/ChatLinks.h" + #include "UITypes/UIBuffList.h" #include "UITypes/UIPartyHP.h" #include "UITypes/UIPartySearch.h" @@ -136,6 +138,8 @@ namespace ms return Tooltip::Parent::ITEMSEARCHRESULT; case UIElement::Type::USERLIST: return Tooltip::Parent::USERLIST; + case UIElement::Type::CHATBAR: + return Tooltip::Parent::CHATBAR; default: return Tooltip::Parent::NONE; } @@ -157,6 +161,8 @@ namespace ms // The Owl of Minerva's results put it 20 right of and below the cursor (FUN_008a45b9), as // does the User List (0x91d82f). const Point OWL_TOOLTIP_POS = Point(20, 20) - tooltip::OFFSET; + // A chat log line's item goes 20 below the cursor (FUN_008f5b20). + const Point CHAT_TOOLTIP_POS = Point(0, chat_link::BELOW) - tooltip::OFFSET; Point tooltip_pos(Tooltip::Parent parent) { @@ -169,6 +175,8 @@ namespace ms case Tooltip::Parent::ITEMSEARCHRESULT: case Tooltip::Parent::USERLIST: return OWL_TOOLTIP_POS; + case Tooltip::Parent::CHATBAR: + return CHAT_TOOLTIP_POS; default: return TOOLTIP_POS; } diff --git a/HeavenClient/IO/UITypes/UIChatBar.cpp b/HeavenClient/IO/UITypes/UIChatBar.cpp index 32dfa7f75..f07eb2e9a 100644 --- a/HeavenClient/IO/UITypes/UIChatBar.cpp +++ b/HeavenClient/IO/UITypes/UIChatBar.cpp @@ -42,6 +42,7 @@ #include "../../Graphics/GraphicsGL.h" #include "../../Net/Packets/MessagingPackets.h" #include "../../Util/CharacterName.h" +#include "../../Util/ChatLinks.h" #include "../../Util/ChatWrap.h" #include "../../Util/NxFiles.h" @@ -563,6 +564,10 @@ namespace ms } shown.text.draw(position + Point(3 + indent, top - TEXT_RAISE)); + + // v83 draws the linked item's name over itself in violet. + if (shown.link) + shown.link->draw(position + Point(3 + indent + shown.link_x, top - TEXT_RAISE)); } Point bar = position + get_scrollbar_pos(); @@ -681,6 +686,14 @@ namespace ms bool released = !clicking && scrollbar_pressed; scrollbar_pressed = clicking; + if (link_click) + { + Point moved = cursorpos - *link_click; + + if (!chat_link::keeps(moved.x(), moved.y())) + hide_link(); + } + if (thumb_dragging) { if (clicking) @@ -784,7 +797,22 @@ namespace ms ScrollPart part = scroll_part_at(cursorpos); if (part == ScrollPart::NONE) + { + // The button going down on a row that links an item shows it + // (FUN_008d5f5c). + if (pressed) + { + std::optional row = row_at(cursorpos); + + if (row && rowtexts[*row].item) + { + UI::get().show_item(Tooltip::Parent::CHATBAR, *rowtexts[*row].item); + link_click = cursorpos; + } + } + return UIElement::send_cursor(clicking, cursorpos); + } if (get_scrollrange() < 2) return Cursor::State::IDLE; @@ -830,6 +858,7 @@ namespace ms held_part = ScrollPart::NONE; edge_dragging = false; combo_hovered = false; + hide_link(); } UIElement::Type UIChatBar::get_type() const @@ -858,7 +887,14 @@ namespace ms add_line(line, type, channel, ear); } - void UIChatBar::add_line(const std::string &line, LineType type, int8_t channel, bool ear) + void UIChatBar::send_linked(const std::string &line, LineType type, int8_t channel, bool ear, + const storage_contents::Stored &item, size_t from, size_t length) + { + add_line(line, type, channel, ear, std::make_shared(item), from, length); + } + + void UIChatBar::add_line(const std::string &line, LineType type, int8_t channel, bool ear, + std::shared_ptr item, size_t from, size_t length) { Color::Name color; Color strip; @@ -964,11 +1000,27 @@ namespace ms type == LineType::SUPER_MEGAPHONE || type == LineType::ITEM_MEGAPHONE || type == LineType::MEGAPHONE_PREVIEW || type == LineType::GACHAPON; + bool first = true; + for (const auto& row : ChatWrap::wrap(line, indent, fit)) { - rowtexts.push_back({ Text(Text::Font::A12M, Text::Alignment::LEFT, color, row), strip, channel, ear, row, - claimable }); + Text text(Text::Font::A12M, Text::Alignment::LEFT, color, row); + std::optional link; + int16_t link_x = 0; + + if (first) + { + if (size_t shown = chat_link::shown(row.size(), from, length)) + { + link = Text(Text::Font::A12M, Text::Alignment::LEFT, Color::Name::ITEMLINK, + row.substr(from, shown)); + link_x = static_cast(text.advance(from)); + } + } + + rowtexts.push_back({ text, strip, channel, ear, row, claimable, item, link, link_x }); channel = -1; + first = false; } while (rowtexts.size() > MAX_ROWS) @@ -1204,6 +1256,40 @@ namespace ms return Rectangle(left, left + COMBO_LIST_WIDTH, COMBO.top() - height, COMBO.top()); } + std::optional UIChatBar::row_at(Point cursorpos) const + { + Point cursor = cursorpos - position; + + if (cursor.x() < 0 || cursor.x() >= chat_link::HOT_WIDTH) + return {}; + + int16_t logbottom = get_logtop() + get_logheight(); + + for (int16_t i = 0; i < get_logrows(); i++) + { + int16_t row = rowpos - i; + + if (row < 0 || row > rowmax) + break; + + int16_t top = logbottom - CHATROWHEIGHT * (i + 1); + + if (cursor.y() >= top && cursor.y() < top + CHATROWHEIGHT) + return static_cast(row); + } + + return {}; + } + + void UIChatBar::hide_link() + { + if (link_click) + { + UI::get().clear_tooltip(Tooltip::Parent::CHATBAR); + link_click.reset(); + } + } + std::optional UIChatBar::combo_row_at(Point cursorpos) const { Rectangle list = combo_list_bounds(); diff --git a/HeavenClient/IO/UITypes/UIChatBar.h b/HeavenClient/IO/UITypes/UIChatBar.h index 0954993a8..a8307d514 100644 --- a/HeavenClient/IO/UITypes/UIChatBar.h +++ b/HeavenClient/IO/UITypes/UIChatBar.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -34,6 +35,7 @@ #include "../Components/ChatTarget.h" #include "../Components/Textfield.h" +#include "../../Net/Handlers/StorageContents.h" #include "../../Template/EnumMap.h" namespace ms @@ -118,6 +120,12 @@ namespace ms // and whether they can be whispered back. void send_megaphone(const std::string &line, LineType type, int8_t channel, bool ear); + // A line that links an item (BROADCAST_MSG types 6, 8 and 11): a + // click on any of its rows shows the item. The part of `length` from + // `from` is the item's name, drawn violet on the first row. + void send_linked(const std::string &line, LineType type, int8_t channel, bool ear, + const storage_contents::Stored &item, size_t from = 0, size_t length = 0); + // The log's rows a report can quote, oldest first: v83 takes the // rows of its chat kinds 0 to 4, 0xd to 0xf and 0x12 to 0x16 // (0x8d468d), which leaves out alliance, spouse and GM chat and the @@ -329,6 +337,11 @@ namespace ms // The row as written, and whether a report can quote it std::string line; bool claimable; + // The item the line links, if any, and the item's name drawn + // over the first row from link_x + std::shared_ptr item; + std::optional link; + int16_t link_x; }; // Oldest first; a wrapped row keeps its strip when old rows leave. std::deque rowtexts; @@ -336,6 +349,15 @@ namespace ms Texture channel_tags[2]; Texture channel_digits[10]; - void add_line(const std::string &line, LineType type, int8_t channel, bool ear); + // Where the item shown by a click on a row was clicked, while it + // shows. + std::optional> link_click; + + void add_line(const std::string &line, LineType type, int8_t channel, bool ear, + std::shared_ptr item = nullptr, size_t from = 0, + size_t length = 0); + // The row under the cursor, if the log shows one there. + std::optional row_at(Point cursorpos) const; + void hide_link(); }; } diff --git a/HeavenClient/Net/Handlers/MessagingHandlers.cpp b/HeavenClient/Net/Handlers/MessagingHandlers.cpp index a0959339d..95f1c229b 100644 --- a/HeavenClient/Net/Handlers/MessagingHandlers.cpp +++ b/HeavenClient/Net/Handlers/MessagingHandlers.cpp @@ -560,12 +560,20 @@ namespace ms chatbar->send_chatline(message, UIChatBar::LineType::SKILL); break; case 6: - // An item id, linking the item to the line when not 0 - recv.read_int(); + { + // An item id, linking the item to the line when not 0; + // nothing in the line's look shows it. + int32_t itemid = recv.read_int(); if (chatbar) - chatbar->send_chatline(message, UIChatBar::LineType::BLUE); + { + if (itemid) + chatbar->send_linked(message, UIChatBar::LineType::BLUE, -1, false, { itemid, 1 }); + else + chatbar->send_chatline(message, UIChatBar::LineType::BLUE); + } break; + } case 7: { // The NPC says it in a dialog with OK, which answers @@ -587,15 +595,23 @@ namespace ms int8_t channel = recv.read_byte(); bool ear = type == 9 || recv.read_bool(); - // The item comes last, after its kind; only its name is shown. + // The item comes last; its name goes before the text, and a + // click on the line shows it. if (type == 8 && recv.read_bool()) { - recv.read_byte(); - message = Megaphones::with_item(message, ItemData::get(recv.read_int()).get_name()); - } - - if (chatbar) + storage_contents::Stored item = storage_contents::read_item(recv); + std::string name = ItemData::get(item.itemid).get_name(); + size_t at = Megaphones::item_at(message); + message = Megaphones::with_item(message, name); + + // "[Name]", brackets and all + if (chatbar) + chatbar->send_linked(message, UIChatBar::LineType::ITEM_MEGAPHONE, channel, ear, item, at, + name.size() + 2); + } else if (chatbar) + { chatbar->send_megaphone(message, UIChatBar::LineType::ITEM_MEGAPHONE, channel, ear); + } warn(message); break; @@ -604,15 +620,14 @@ namespace ms { // The winner's channel (-1 would be the Cash Shop Surprise, // which RustMS doesn't send), the machine's town, then the - // prize after its kind; only its name is shown. + // prize, which a click on the line shows. recv.read_int(); std::string town = recv.read_string(); - recv.read_byte(); - std::string item = ItemData::get(recv.read_int()).get_name(); - std::string line = Megaphones::gachapon(message, item, town); + storage_contents::Stored item = storage_contents::read_item(recv); + std::string line = Megaphones::gachapon(message, ItemData::get(item.itemid).get_name(), town); if (chatbar) - chatbar->send_chatline(line, UIChatBar::LineType::GACHAPON); + chatbar->send_linked(line, UIChatBar::LineType::GACHAPON, -1, false, item); warn(line); break; diff --git a/HeavenClient/Util/ChatLinks.h b/HeavenClient/Util/ChatLinks.h new file mode 100644 index 000000000..e7c0e4502 --- /dev/null +++ b/HeavenClient/Util/ChatLinks.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace ms +{ + // How v83 links an item to a chat log line (BROADCAST_MSG types 6, 8 and + // 11): every row of the line carries the item, and a click on one shows + // it (FUN_008d5f5c). + namespace chat_link + { + // A row is hot left of this, the log's whole width. + constexpr int16_t HOT_WIDTH = 0x244; + // The tooltip goes 20 below the click (FUN_008f5b20)... + constexpr int16_t BELOW = 0x14; + // ...and stays while the cursor is in the 40x40 square around the + // click, the right and bottom edges left out (PtInRect, 0x8d804e). + // `dx` and `dy` are how far the cursor is from the click. + constexpr int16_t KEEP = 0x14; + + inline bool keeps(int16_t dx, int16_t dy) + { + return dx >= -KEEP && dx < KEEP && dy >= -KEEP && dy < KEEP; + } + + // How much of the part from `from`, `length` long, a row of + // `row_size` shows; only the first row draws it violet. + inline size_t shown(size_t row_size, size_t from, size_t length) + { + return from >= row_size ? 0 : std::min(length, row_size - from); + } + } +} diff --git a/HeavenClient/Util/Megaphones.h b/HeavenClient/Util/Megaphones.h index 073ae0bbc..44c3f8e48 100644 --- a/HeavenClient/Util/Megaphones.h +++ b/HeavenClient/Util/Megaphones.h @@ -23,16 +23,20 @@ namespace ms return (mapid / 1'000'000) % 100 != 9; } - // The line with the shown item's name before the text, as - // "Name : [Item]text". - inline std::string with_item(const std::string& line, const std::string& item) + // Where with_item puts the item's name: after the first separator, or + // first without one. + inline size_t item_at(const std::string& line) { size_t split = line.find(SEPARATOR); - if (split == std::string::npos) - return "[" + item + "]" + line; + return split == std::string::npos ? 0 : split + SEPARATOR.size(); + } - split += SEPARATOR.size(); + // The line with the shown item's name before the text, as + // "Name : [Item]text". + inline std::string with_item(const std::string& line, const std::string& item) + { + size_t split = item_at(line); return line.substr(0, split) + "[" + item + "]" + line.substr(split); } diff --git a/HeavenClient/tests/megaphones.cpp b/HeavenClient/tests/megaphones.cpp index c03cddd58..f9c31a637 100644 --- a/HeavenClient/tests/megaphones.cpp +++ b/HeavenClient/tests/megaphones.cpp @@ -1,3 +1,4 @@ +#include "../Util/ChatLinks.h" #include "../Util/Megaphones.h" #include #include @@ -18,6 +19,15 @@ int main() require(with_item("Hero : selling", "Ilbi") == "Hero : [Ilbi]selling", "the item goes before the text"); require(with_item("selling", "Ilbi") == "[Ilbi]selling", "even without a name"); + require(item_at("Hero : selling") == 7 && item_at("selling") == 0, "the item's name goes where with_item puts it"); + + require(ms::chat_link::keeps(0, 0) && ms::chat_link::keeps(-20, -20) && ms::chat_link::keeps(19, 19), + "a linked item stays in the 40x40 square around the click"); + require(!ms::chat_link::keeps(20, 0) && !ms::chat_link::keeps(0, 20) && !ms::chat_link::keeps(-21, 0), + "but goes once the cursor leaves it, its right and bottom edges left out"); + require(ms::chat_link::shown(20, 7, 6) == 6, "a first row shows all of the item's name"); + require(ms::chat_link::shown(10, 7, 6) == 3, "or the part before it wraps"); + require(ms::chat_link::shown(7, 7, 6) == 0 && ms::chat_link::shown(5, 0, 0) == 0, "or none"); require(gachapon("Hero : got a(n)", "Chaos Scroll 60%", "Henesys") == "Hero got a(n) : Chaos Scroll 60% from the Henesys Gachapon! Congrats!", diff --git a/agent_docs/inventory.md b/agent_docs/inventory.md index 4672c1832..38f99bb64 100644 --- a/agent_docs/inventory.md +++ b/agent_docs/inventory.md @@ -342,6 +342,15 @@ megaphone's later lines take the first line's name. Lines are dropped on event maps (`Util/Megaphones.h`); v83 shows them there to GMs, but HeavenClient doesn't know who is a GM. +A line that carries an item (BROADCAST_MSG types 6, 8 and 11) links it +(`UIChatBar::send_linked`, `Util/ChatLinks.h`). The item megaphone's +`[item name]` is drawn again in violet (#6C4CE3, v83's chat font +ctx+0xc20). When the mouse button goes down on any of the line's rows, the +item's tooltip opens 20 px below the cursor (FUN_008d5f5c, FUN_008f5b20). It +closes once the cursor leaves the 40x40 square around the click (0x8d804e). +HeavenClient also closes it when the cursor leaves the chat bar, since its +chat bar is a window of its own. + Two other server message types aren't megaphones. Type 6 (a line with an item ID) is shown as a blue chat line; v83 also links the item, which HeavenClient leaves out. Type 7 (an NPC ID and the text) opens that NPC's @@ -946,7 +955,8 @@ them (0x9382b0). `Stage::effect_node` finds the path's node, as user effect 23 does too: the NX file named first (`UI`, `Effect` or `Map`), then the node, retried with the image's `.img` put back (`field_effect::with_image`), since Item.nx writes reward paths without it. A world message's item link -(BROADCAST_MSG type 6) isn't drawn; the line shows as plain blue text. +(BROADCAST_MSG type 6) changes nothing in the line's look; it shows as plain +blue text, but a click on it shows the item, as above. ### Gachapon @@ -977,10 +987,9 @@ default, `v83ref/lanes/QUESTIONS.md`) as BROADCAST_MSG type 11 and the item as in an inventory. The v83 client shows it as a chat line, " got a(n) : from the Gachapon! Congrats!" (StringPool 0x137d), black text on a green bar (chat type 0x12), with the item's tooltip -on hover; a channel of -1 would name the Cash Shop Surprise. HeavenClient +on a click; a channel of -1 would name the Cash Shop Surprise. HeavenClient reads it in `MessagingHandlers.cpp` (case 11), writes the line with -`Megaphones::gachapon` and shows it as `UIChatBar::GACHAPON`, without the -tooltip. +`Megaphones::gachapon` and shows it as a linked `UIChatBar::GACHAPON` line. The Remote Gachapon Ticket (5451000, report b196) opens a window of ten town buttons in the v83 client; a button sends USE_REMOTE (`0x74`, 0xa14074):