From f37092cea310bde30432e04c95a697662bd001a0 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 01:45:36 -0400 Subject: [PATCH 1/3] HC: SCRIPT_PROGRESS_MESSAGE (0x7A) floating gold lines and QUEST_CLEAR (0x31) Quest Complete card; server notices kept for quests outside the catalog; #R# shows a quest record in quest text --- HeavenClient/CMakeLists.txt | 4 + HeavenClient/Graphics/Color.h | 4 +- HeavenClient/IO/Components/FloatingLines.h | 74 +++++++++++++++++++ HeavenClient/IO/UIElement.h | 1 + HeavenClient/IO/UIStateGame.cpp | 3 + HeavenClient/IO/UITypes/UIQuestComplete.cpp | 33 ++++++--- HeavenClient/IO/UITypes/UIQuestComplete.h | 7 +- HeavenClient/IO/UITypes/UIQuestLog.cpp | 15 +++- HeavenClient/IO/UITypes/UIScriptMessage.cpp | 70 ++++++++++++++++++ HeavenClient/IO/UITypes/UIScriptMessage.h | 38 ++++++++++ .../Net/Handlers/MessagingHandlers.cpp | 22 ++++++ HeavenClient/Net/Handlers/MessagingHandlers.h | 14 ++++ HeavenClient/Net/PacketSwitch.cpp | 4 + HeavenClient/tests/floating_lines.cpp | 51 +++++++++++++ 14 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 HeavenClient/IO/Components/FloatingLines.h create mode 100644 HeavenClient/IO/UITypes/UIScriptMessage.cpp create mode 100644 HeavenClient/IO/UITypes/UIScriptMessage.h create mode 100644 HeavenClient/tests/floating_lines.cpp diff --git a/HeavenClient/CMakeLists.txt b/HeavenClient/CMakeLists.txt index c478ef8cb..11cd58770 100644 --- a/HeavenClient/CMakeLists.txt +++ b/HeavenClient/CMakeLists.txt @@ -44,6 +44,8 @@ if(HEAVENCLIENT_BUILD_TESTS) add_test(NAME slide_menu COMMAND slide_menu_tests) add_executable(quiz_tests tests/quiz.cpp) add_test(NAME quiz COMMAND quiz_tests) + add_executable(floating_lines_tests tests/floating_lines.cpp) + add_test(NAME floating_lines COMMAND floating_lines_tests) add_executable(world_map_lookup_tests tests/world_map_lookup.cpp) add_test(NAME world_map_lookup COMMAND world_map_lookup_tests) add_executable(swindle_words_tests tests/swindle_words.cpp) @@ -818,6 +820,8 @@ add_executable(HeavenClient IO/UITypes/UIQuiz.h IO/UITypes/UIRpsGame.cpp IO/UITypes/UIRpsGame.h + IO/UITypes/UIScriptMessage.cpp + IO/UITypes/UIScriptMessage.h IO/UITypes/UIFieldClock.cpp IO/UITypes/UIFieldClock.h IO/UITypes/UIAvatarMegaphone.cpp diff --git a/HeavenClient/Graphics/Color.h b/HeavenClient/Graphics/Color.h index 90fe46e5c..64748b421 100644 --- a/HeavenClient/Graphics/Color.h +++ b/HeavenClient/Graphics/Color.h @@ -149,6 +149,7 @@ namespace ms KITEMSG, JOBHEAD, JOBDISABLED, + SCRIPTGOLD, NUM_COLORS }; @@ -247,7 +248,8 @@ namespace ms {0.14f, 0.28f, 0.41f}, // Parcel select (#244768, the parcel Duey selects) {0.80f, 0.80f, 0.60f}, // Kite msg (#CCCC99, the item's words after a kite's flier) {0.47f, 0.50f, 0.53f}, // Job head (#777F88, a class's box in the Party Search window) - {0.55f, 0.54f, 0.55f} // Job disabled (#8C8A8C, a greyed box's label there) + {0.55f, 0.54f, 0.55f}, // Job disabled (#8C8A8C, a greyed box's label there) + {1.00f, 0.78f, 0.00f} // Script gold (#FFC600, the floating script lines) }; // Create a color by an array of real numbers [0.0f, 1.0f] diff --git a/HeavenClient/IO/Components/FloatingLines.h b/HeavenClient/IO/Components/FloatingLines.h new file mode 100644 index 000000000..318b9f341 --- /dev/null +++ b/HeavenClient/IO/Components/FloatingLines.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ms +{ + // v83's five floating lines near the top of the screen (CWvsContext+0x3680, FUN_006ce251), which + // SCRIPT_PROGRESS_MESSAGE writes to. A line takes the first slot that is free: never used, or shown for + // 3 s already, though it may still be fading. With all five busy the line is dropped. A line stays for + // 3 s and then fades out over 3 more. + class FloatingLines + { + public: + static constexpr size_t SLOTS = 5; + static constexpr int32_t SHOWN = 3000; + static constexpr int32_t FADE = 3000; + + // The slot `text` went to, if any was free. + std::optional add(const std::string &text) + { + for (size_t i = 0; i < SLOTS; i++) + { + Slot &slot = slots[i]; + + if (slot.age < 0 || slot.age >= SHOWN) + { + slot = { text, 0 }; + return i; + } + } + + return std::nullopt; + } + + void update(int32_t elapsed) + { + for (Slot &slot : slots) + if (slot.age >= 0 && slot.age < SHOWN + FADE) + slot.age += elapsed; + } + + float opacity(size_t i) const + { + int32_t age = slots[i].age; + + if (age < 0 || age >= SHOWN + FADE) + return 0.0f; + + if (age <= SHOWN) + return 1.0f; + + return 1.0f - static_cast(age - SHOWN) / FADE; + } + + const std::string &text(size_t i) const + { + return slots[i].text; + } + + private: + struct Slot + { + std::string text; + // Since it was shown, in ms; -1 when never used. + int32_t age = -1; + }; + + std::array slots; + }; +} diff --git a/HeavenClient/IO/UIElement.h b/HeavenClient/IO/UIElement.h index d5c782466..a954b039d 100644 --- a/HeavenClient/IO/UIElement.h +++ b/HeavenClient/IO/UIElement.h @@ -123,6 +123,7 @@ namespace ms PARTYSEARCH, PARTYSEARCHICON, RPSGAME, + SCRIPTMESSAGE, NUM_TYPES }; diff --git a/HeavenClient/IO/UIStateGame.cpp b/HeavenClient/IO/UIStateGame.cpp index e801b866e..cfe446f45 100644 --- a/HeavenClient/IO/UIStateGame.cpp +++ b/HeavenClient/IO/UIStateGame.cpp @@ -37,6 +37,7 @@ #include "UITypes/UIMessenger.h" #include "UITypes/UIMiniMap.h" #include "UITypes/UIQuestComplete.h" +#include "UITypes/UIScriptMessage.h" #include "UITypes/UIQuestLog.h" #include "UITypes/UIQuit.h" #include "UITypes/UIShop.h" @@ -160,6 +161,7 @@ namespace ms emplace(); emplace(Stage::get().get_player().get_quests(), inventory); emplace(Stage::get().get_player().get_quests(), inventory); + emplace(); emplace(Stage::get().get_player().get_party(), stats); emplace(look, inventory); emplace(inventory); @@ -765,6 +767,7 @@ namespace ms UIElement::Type::BUFFLIST, UIElement::Type::QUESTHELPER, UIElement::Type::QUESTCOMPLETE, + UIElement::Type::SCRIPTMESSAGE, UIElement::Type::PARTYHP, UIElement::Type::NPCTALK, UIElement::Type::AVATARSELECT, diff --git a/HeavenClient/IO/UITypes/UIQuestComplete.cpp b/HeavenClient/IO/UITypes/UIQuestComplete.cpp index ad3fde93a..5044cb9b3 100644 --- a/HeavenClient/IO/UITypes/UIQuestComplete.cpp +++ b/HeavenClient/IO/UITypes/UIQuestComplete.cpp @@ -78,12 +78,13 @@ namespace ms bool changed = false; // Quests handed in or forfeited take their notice with them; the rest - // fade, and those below a notice that leaves move down. + // fade, and those below a notice that leaves move down. The server's + // notices may be for quests the catalog lacks, as the explorer titles. for (auto it = notices.begin(); it != notices.end();) { it->life.update(Constants::TIMESTEP); - if (it->life.gone() || !started.count(it->id)) + if (it->life.gone() || !quests.is_started(it->id)) { it = notices.erase(it); changed = true; @@ -96,15 +97,7 @@ namespace ms for (int16_t id : completion.update(started)) { - const QuestInfo* quest = QuestCatalog::get().find(id); - Text name(Text::A12M, Text::LEFT, Color::Name::WHITE, quest ? quest->name : std::to_string(id)); - string_format::format_with_ellipsis(name, NAME_WIDTH); - - notices.erase(std::remove_if(notices.begin(), notices.end(), [&](const Notice& n) { return n.id == id; }), - notices.end()); - notices.push_back({ id, std::move(name), FadeNotice(LIFETIME) }); - Sound(Sound::Name::INVITE).play(); - + push_notice(id); changed = true; } @@ -115,6 +108,24 @@ namespace ms UIElement::update(); } + void UIQuestComplete::add_notice(int16_t id) + { + push_notice(id); + rebuild(); + } + + void UIQuestComplete::push_notice(int16_t id) + { + std::string title = nl::nx::quest["QuestInfo.img"][std::to_string(id)]["name"]; + Text name(Text::A12M, Text::LEFT, Color::Name::WHITE, title.empty() ? std::to_string(id) : title); + string_format::format_with_ellipsis(name, NAME_WIDTH); + + notices.erase(std::remove_if(notices.begin(), notices.end(), [&](const Notice& n) { return n.id == id; }), + notices.end()); + notices.push_back({ id, std::move(name), FadeNotice(LIFETIME) }); + Sound(Sound::Name::INVITE).play(); + } + void UIQuestComplete::rebuild() { buttons.clear(); diff --git a/HeavenClient/IO/UITypes/UIQuestComplete.h b/HeavenClient/IO/UITypes/UIQuestComplete.h index c8e691eae..55ba091d9 100644 --- a/HeavenClient/IO/UITypes/UIQuestComplete.h +++ b/HeavenClient/IO/UITypes/UIQuestComplete.h @@ -15,7 +15,8 @@ namespace ms { // v83's red "Quest Complete!" notice (UIWindow.img/FadeYesNo/backgrnd4) - // above the status bar, shown when a quest's last objective is met. O + // above the status bar, shown when a quest's last objective is met, or + // when the server says so (QUEST_CLEAR, as for an explorer title). O // opens the quest in the quest log and X dismisses it. Each notice stays // until then, or until the quest is no longer in progress; newer notices // stack above older ones, invitations included. @@ -35,6 +36,9 @@ namespace ms UIElement::Type get_type() const override; + // A notice for quest `id`, replacing one it already has. + void add_notice(int16_t id); + protected: Button::State button_pressed(uint16_t buttonid) override; @@ -49,6 +53,7 @@ namespace ms FadeNotice life; }; + void push_notice(int16_t id); void rebuild(); // From the bottom of the stack to `notice`. Point offset(const Notice& notice) const; diff --git a/HeavenClient/IO/UITypes/UIQuestLog.cpp b/HeavenClient/IO/UITypes/UIQuestLog.cpp index bc0b8f919..58352a3e0 100644 --- a/HeavenClient/IO/UITypes/UIQuestLog.cpp +++ b/HeavenClient/IO/UITypes/UIQuestLog.cpp @@ -184,9 +184,10 @@ namespace ms } // Fill in the quest-only placeholders, which NpcTextMarkup leaves: - // #c how many of an item the player holds, and #a a kill count - // (#a# for this quest's mobs, or #a#). Line - // breaks become the renderer's escapes. + // #c how many of an item the player holds, #a a kill count + // (#a# for this quest's mobs, or #a#), and #R + // a quest's record as it stands (#R27010# an explorer title's count of + // places). Line breaks become the renderer's escapes. std::string quest_counts(const std::string& text, int16_t quest_id) { auto& player = Stage::get().get_player(); @@ -208,7 +209,7 @@ namespace ms size_t close = c == '#' && i + 2 < text.size() ? text.find('#', i + 2) : std::string::npos; char tag = i + 1 < text.size() ? text[i + 1] : 0; - if (close == std::string::npos || (tag != 'c' && tag != 'a') + if (close == std::string::npos || (tag != 'c' && tag != 'a' && tag != 'R') || !NpcTextMarkup::is_digits(text.substr(i + 2, close - i - 2)) || close - i - 2 > 9) { out += c; @@ -221,6 +222,12 @@ namespace ms { out += std::to_string(player.get_inventory().get_total_item_count(id)); } + else if (tag == 'R') + { + const auto& started = player.get_quests().started_quests(); + auto record = started.find(static_cast(id)); + out += record == started.end() || record->second.empty() ? "0" : record->second; + } else { int16_t quest = id < 10 ? quest_id : static_cast(id / 10); diff --git a/HeavenClient/IO/UITypes/UIScriptMessage.cpp b/HeavenClient/IO/UITypes/UIScriptMessage.cpp new file mode 100644 index 000000000..2507e495f --- /dev/null +++ b/HeavenClient/IO/UITypes/UIScriptMessage.cpp @@ -0,0 +1,70 @@ +#include "UIScriptMessage.h" + +#include "../../Constants.h" + +namespace ms +{ + namespace + { + // The first line's middle, from the top; later lines sit higher. v83's line is a 20 px layer around it. + constexpr int16_t FIRST_Y = 150; + constexpr int16_t LINE_HEIGHT = 20; + constexpr int16_t HALF_LINE = 10; + // Where the black copies go under the gold. + const std::array, 4> EDGE = { + Point(-1, 0), Point(1, 0), Point(0, -1), Point(0, 1) + }; + } + + UIScriptMessage::UIScriptMessage() : UIElement(Point(), Point()) + { + update_screen(Constants::Constants::get().get_viewwidth(), Constants::Constants::get().get_viewheight()); + } + + void UIScriptMessage::draw(float) const + { + for (size_t i = 0; i < FloatingLines::SLOTS; i++) + { + float opacity = lines.opacity(i); + + if (opacity <= 0.0f) + continue; + + Point at = position + Point(0, FIRST_Y - HALF_LINE - LINE_HEIGHT * static_cast(i)); + + for (const Point &edge : EDGE) + edges[i].draw(DrawArgument(at + edge, opacity)); + + texts[i].draw(DrawArgument(at, opacity)); + } + } + + void UIScriptMessage::update() + { + lines.update(Constants::TIMESTEP); + } + + void UIScriptMessage::update_screen(int16_t new_width, int16_t) + { + position = Point(new_width / 2, 0); + } + + bool UIScriptMessage::is_in_range(Point) const + { + return false; + } + + UIElement::Type UIScriptMessage::get_type() const + { + return TYPE; + } + + void UIScriptMessage::show(const std::string &text) + { + if (auto slot = lines.add(text)) + { + texts[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::SCRIPTGOLD, text); + edges[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::BLACK, text); + } + } +} diff --git a/HeavenClient/IO/UITypes/UIScriptMessage.h b/HeavenClient/IO/UITypes/UIScriptMessage.h new file mode 100644 index 000000000..47a1a2d01 --- /dev/null +++ b/HeavenClient/IO/UITypes/UIScriptMessage.h @@ -0,0 +1,38 @@ +#pragma once + +#include "../UIElement.h" +#include "../Components/FloatingLines.h" + +#include "../../Graphics/Text.h" + +#include +#include + +namespace ms +{ + // SCRIPT_PROGRESS_MESSAGE's lines (FUN_00a13f20): gold Arial bold 15 with a black edge, centred across the + // screen, the first slot 150 px from the top and each later one 20 px higher (ctor FUN_006cd6ba). See + // FloatingLines for how long they stay. Clicks go through them. + class UIScriptMessage : public UIElement + { + public: + static constexpr Type TYPE = UIElement::Type::SCRIPTMESSAGE; + static constexpr bool FOCUSED = false; + static constexpr bool TOGGLED = false; + + UIScriptMessage(); + + void draw(float inter) const override; + void update() override; + void update_screen(int16_t new_width, int16_t new_height) override; + bool is_in_range(Point cursorpos) const override; + + UIElement::Type get_type() const override; + + void show(const std::string &text); + + private: + FloatingLines lines; + std::array texts, edges; + }; +} diff --git a/HeavenClient/Net/Handlers/MessagingHandlers.cpp b/HeavenClient/Net/Handlers/MessagingHandlers.cpp index 03ec29a25..e6a37573e 100644 --- a/HeavenClient/Net/Handlers/MessagingHandlers.cpp +++ b/HeavenClient/Net/Handlers/MessagingHandlers.cpp @@ -34,6 +34,8 @@ #include "StatusMessages.h" #include "../../IO/UITypes/UIAvatarMegaphone.h" +#include "../../IO/UITypes/UIQuestComplete.h" +#include "../../IO/UITypes/UIScriptMessage.h" #include "../../IO/UITypes/UIChatBar.h" #include "../../IO/UITypes/UIInviteCard.h" #include "../../IO/UITypes/UIMessenger.h" @@ -401,6 +403,26 @@ namespace ms banner->hide(); } + void QuestClearHandler::handle(InPacket& recv) const + { + int16_t quest_id = recv.read_short(); + + if (auto notices = UI::get().get_element()) + notices->add_notice(quest_id); + } + + void ScriptProgressMessageHandler::handle(InPacket& recv) const + { + std::string text = recv.read_string(); + + // v83 ignores an empty one. + if (text.empty()) + return; + + if (auto lines = UI::get().get_element()) + lines->show(text); + } + void ServerMessageHandler::handle(InPacket& recv) const { // v83 reads a flag first only for the scrolling notice, which has no diff --git a/HeavenClient/Net/Handlers/MessagingHandlers.h b/HeavenClient/Net/Handlers/MessagingHandlers.h index eb08d29f5..d50149b34 100644 --- a/HeavenClient/Net/Handlers/MessagingHandlers.h +++ b/HeavenClient/Net/Handlers/MessagingHandlers.h @@ -79,6 +79,20 @@ namespace ms void handle(InPacket& recv) const override; }; + // A quest's title is earned: its "Quest Complete!" notice (FUN_00a26d44) + // Opcode: QUEST_CLEAR(49) + class QuestClearHandler : public PacketHandler + { + void handle(InPacket& recv) const override; + }; + + // A line among the floating script lines (FUN_00a13f20) + // Opcode: SCRIPT_PROGRESS_MESSAGE(122) + class ScriptProgressMessageHandler : public PacketHandler + { + void handle(InPacket& recv) const override; + }; + // Show another type of server message // Opcode: WEEK_EVENT_MESSAGE(77) class WeekEventMessageHandler : public PacketHandler diff --git a/HeavenClient/Net/PacketSwitch.cpp b/HeavenClient/Net/PacketSwitch.cpp index af16cf144..c0b8f305f 100644 --- a/HeavenClient/Net/PacketSwitch.cpp +++ b/HeavenClient/Net/PacketSwitch.cpp @@ -69,6 +69,7 @@ namespace ms CANCEL_BUFF = 33, RECALCULATE_STATS = 35, SET_TAMING_MOB_INFO = 48, + QUEST_CLEAR = 49, UPDATE_SKILL = 36, SKILL_USE_RESULT = 37, FAME_RESPONSE = 38, @@ -101,6 +102,7 @@ namespace ms MONSTER_BOOK_SET_COVER = 84, MINIMAP_ON_OFF = 86, + SCRIPT_PROGRESS_MESSAGE = 122, SKILL_MACROS = 124, SET_FIELD = 125, SET_BACK_EFFECT = 128, @@ -383,6 +385,8 @@ namespace ms emplace(); emplace(); emplace(); + emplace(); + emplace(); emplace(); // Inventory Handlers diff --git a/HeavenClient/tests/floating_lines.cpp b/HeavenClient/tests/floating_lines.cpp new file mode 100644 index 000000000..d9b97116f --- /dev/null +++ b/HeavenClient/tests/floating_lines.cpp @@ -0,0 +1,51 @@ +#include "../IO/Components/FloatingLines.h" +#include +#include + +void require(bool condition, const char* message) +{ + if (!condition) throw std::runtime_error(message); +} + +int main() +{ + try + { + ms::FloatingLines lines; + + require(lines.opacity(0) == 0.0f, "an unused slot shows nothing"); + + for (size_t i = 0; i < ms::FloatingLines::SLOTS; i++) + require(lines.add("line " + std::to_string(i)) == i, "lines fill the slots in order"); + + require(!lines.add("sixth"), "with every slot busy a line is dropped"); + require(lines.text(0) == "line 0", "the dropped line overwrote nothing"); + + lines.update(1000); + require(lines.opacity(0) == 1.0f, "a line is solid at first"); + require(!lines.add("still busy"), "nothing frees within 3 s"); + + lines.update(2000); + require(lines.opacity(0) == 1.0f, "still solid at 3 s"); + lines.update(1500); + require(lines.opacity(0) == 0.5f, "half faded 1.5 s later"); + + require(lines.add("next") == size_t(0), "a fading slot takes a new line"); + require(lines.text(0) == "next" && lines.opacity(0) == 1.0f, "the new line starts solid"); + require(lines.opacity(1) == 0.5f, "the others carry on fading"); + + lines.update(1500); + require(lines.opacity(1) == 0.0f, "gone after 6 s"); + lines.update(100000); + require(lines.opacity(1) == 0.0f, "and stays gone"); + require(lines.opacity(0) == 0.0f, "the newer line has gone too"); + + std::cout << "floating lines tests passed\n"; + return 0; + } + catch (const std::exception& e) + { + std::cerr << e.what() << '\n'; + return 1; + } +} From 089bcde2b5778c721878379b301a09fe49ea0f4e Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 01:46:05 -0400 Subject: [PATCH 2/3] Docs: HeavenClient shows script lines (0x7A), QUEST_CLEAR cards and #R records --- agent_docs/client.md | 9 +++++++-- agent_docs/quests.md | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/agent_docs/client.md b/agent_docs/client.md index 040aab541..a8c16120f 100644 --- a/agent_docs/client.md +++ b/agent_docs/client.md @@ -134,8 +134,13 @@ from `UI.nx`. Moved so far: [npc-dialogue.md](npc-dialogue.md). - **Quest Helper** (`UIQuestHelper`, `UIWindow.img/QuestAlarm`). See [quests.md](quests.md). -- **Quest Complete! notice** (`UIQuestComplete`, `UIWindow.img/FadeYesNo`). - See [quests.md](quests.md). +- **Quest Complete! notice** (`UIQuestComplete`, `UIWindow.img/FadeYesNo`), + for a quest whose objectives the client sees met or one the server names + in QUEST_CLEAR (0x31). See [quests.md](quests.md). +- **Script lines** (`UIScriptMessage`, SCRIPT_PROGRESS_MESSAGE, 0x7A): up to + five gold lines at y = 150, 130, ... 70, centred, each shown for 3 s and + faded over 3 more; with all five busy a line is dropped (FUN_00a13f20, + FUN_006ce251). - **Quest alert** (`QuestAlert`, `Effect/BasicEff.img/QuestAlert`): the bulb over the player's face for autoStart and autoComplete quests. See [quests.md](quests.md). diff --git a/agent_docs/quests.md b/agent_docs/quests.md index 2bb790f89..dafe921d2 100644 --- a/agent_docs/quests.md +++ b/agent_docs/quests.md @@ -247,7 +247,9 @@ place starts the quest. Each new place sends the info record with the count and an info text; the place that reaches the count sends QUEST_CLEAR (0x31), the title line and the "see Dalair" text instead. The texts are the `medal.explorer.*` keys (placeholders until the content pack has them). -HeavenClient doesn't handle 0x7A or 0x31 yet. +HeavenClient shows 0x7A as a gold line near the top of the screen +(`UIScriptMessage`) and 0x31 as a Quest Complete! card (`UIQuestComplete`), +and its quest log fills `#R27010#` in the quest text with the record's count. Places seen are `QuestLog::explored`, keyed by the info record, and are stored in `explored_maps (character_id, record_id, map_id)`. They outlast a forfeit: From c9baf17d2bf525b4cb9577d1ce46907eaaea2852 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 01:51:31 -0400 Subject: [PATCH 3/3] Review a054: script lines are plain text, free after more than 3 s, stay on top; edges fade faster; #R ids up to 5 digits; FloatingLines.h listed --- HeavenClient/CMakeLists.txt | 1 + HeavenClient/IO/Components/FloatingLines.h | 4 ++-- HeavenClient/IO/UIStateGame.cpp | 1 + HeavenClient/IO/UITypes/UIQuestLog.cpp | 2 +- HeavenClient/IO/UITypes/UIScriptMessage.cpp | 9 ++++++--- HeavenClient/tests/floating_lines.cpp | 1 + 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/HeavenClient/CMakeLists.txt b/HeavenClient/CMakeLists.txt index 11cd58770..f6fe8ca70 100644 --- a/HeavenClient/CMakeLists.txt +++ b/HeavenClient/CMakeLists.txt @@ -693,6 +693,7 @@ add_executable(HeavenClient IO/Components/EquipTooltip.h IO/Components/ContextMenuLayout.h IO/Components/FadeNotice.h + IO/Components/FloatingLines.h IO/Components/Gauge.cpp IO/Components/Gauge.h IO/Components/Icon.cpp diff --git a/HeavenClient/IO/Components/FloatingLines.h b/HeavenClient/IO/Components/FloatingLines.h index 318b9f341..324499956 100644 --- a/HeavenClient/IO/Components/FloatingLines.h +++ b/HeavenClient/IO/Components/FloatingLines.h @@ -10,7 +10,7 @@ namespace ms { // v83's five floating lines near the top of the screen (CWvsContext+0x3680, FUN_006ce251), which // SCRIPT_PROGRESS_MESSAGE writes to. A line takes the first slot that is free: never used, or shown for - // 3 s already, though it may still be fading. With all five busy the line is dropped. A line stays for + // more than 3 s, though it may still be fading. With all five busy the line is dropped. A line stays for // 3 s and then fades out over 3 more. class FloatingLines { @@ -26,7 +26,7 @@ namespace ms { Slot &slot = slots[i]; - if (slot.age < 0 || slot.age >= SHOWN) + if (slot.age < 0 || slot.age > SHOWN) { slot = { text, 0 }; return i; diff --git a/HeavenClient/IO/UIStateGame.cpp b/HeavenClient/IO/UIStateGame.cpp index cfe446f45..f169cbfa0 100644 --- a/HeavenClient/IO/UIStateGame.cpp +++ b/HeavenClient/IO/UIStateGame.cpp @@ -869,6 +869,7 @@ namespace ms case UIElement::Type::INVITECARD: case UIElement::Type::MEMONOTICE: case UIElement::Type::QUESTCOMPLETE: + case UIElement::Type::SCRIPTMESSAGE: case UIElement::Type::NOTICE: case UIElement::Type::ENTERNUMBER: case UIElement::Type::FRIENDGROUP: diff --git a/HeavenClient/IO/UITypes/UIQuestLog.cpp b/HeavenClient/IO/UITypes/UIQuestLog.cpp index 58352a3e0..14b916b0c 100644 --- a/HeavenClient/IO/UITypes/UIQuestLog.cpp +++ b/HeavenClient/IO/UITypes/UIQuestLog.cpp @@ -210,7 +210,7 @@ namespace ms char tag = i + 1 < text.size() ? text[i + 1] : 0; if (close == std::string::npos || (tag != 'c' && tag != 'a' && tag != 'R') - || !NpcTextMarkup::is_digits(text.substr(i + 2, close - i - 2)) || close - i - 2 > 9) + || !NpcTextMarkup::is_digits(text.substr(i + 2, close - i - 2)) || close - i - 2 > (tag == 'R' ? 5 : 9)) { out += c; continue; diff --git a/HeavenClient/IO/UITypes/UIScriptMessage.cpp b/HeavenClient/IO/UITypes/UIScriptMessage.cpp index 2507e495f..bf44cb094 100644 --- a/HeavenClient/IO/UITypes/UIScriptMessage.cpp +++ b/HeavenClient/IO/UITypes/UIScriptMessage.cpp @@ -32,8 +32,10 @@ namespace ms Point at = position + Point(0, FIRST_Y - HALF_LINE - LINE_HEIGHT * static_cast(i)); + // v83 fades the outlined line as one layer. Drawn apart, edges at full fade would darken the gold + // they lie under, so they fade faster. for (const Point &edge : EDGE) - edges[i].draw(DrawArgument(at + edge, opacity)); + edges[i].draw(DrawArgument(at + edge, opacity * opacity)); texts[i].draw(DrawArgument(at, opacity)); } @@ -63,8 +65,9 @@ namespace ms { if (auto slot = lines.add(text)) { - texts[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::SCRIPTGOLD, text); - edges[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::BLACK, text); + // One plain line: no colour codes or breaks. + texts[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::SCRIPTGOLD, text, 0, false); + edges[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::BLACK, text, 0, false); } } } diff --git a/HeavenClient/tests/floating_lines.cpp b/HeavenClient/tests/floating_lines.cpp index d9b97116f..24ddc0981 100644 --- a/HeavenClient/tests/floating_lines.cpp +++ b/HeavenClient/tests/floating_lines.cpp @@ -27,6 +27,7 @@ int main() lines.update(2000); require(lines.opacity(0) == 1.0f, "still solid at 3 s"); + require(!lines.add("at 3 s"), "a slot frees only after 3 s"); lines.update(1500); require(lines.opacity(0) == 0.5f, "half faded 1.5 s later");