From 42d30b823aed6b1356f820a264ca0d8335478345 Mon Sep 17 00:00:00 2001 From: stribog Date: Sun, 17 May 2026 11:35:35 +0200 Subject: [PATCH 1/4] =?UTF-8?q?revert:=20=D0=BE=D1=82=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20TopPlayer=20=D0=BA=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8E=20=D0=B4=D0=BE=20#3142=20(?= =?UTF-8?q?#3280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3142 ограничил chart_ до kPlayerChartSize ради перфа на per-kill Refresh. Из-за этого «Перед вами / Ваш текущий рейтинг / После вас» в `do_best <класс>` показывался только для топ-10 — у остальных секции пустые, а позиция игрока не определяется. Возвращаю pre-#3142 состояние `top.cpp` и `top.h`. Дисплей снова работает для всех игроков. Внимание — этот ревёрт также снимает мои последующие правки (top_remort_ как отдельный список для максремортов, dup-check в Refresh, секция «Достигшие максимум» в PrintPlayersChart). Если эти куски нужны — занесу отдельным коммитом поверх. Перфовая идея «пересчитывать chart раз в RL-час вместо per-kill» осталась нереализованной — отдельная задача. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gameplay/statistics/top.cpp | 92 +++++++++------------------------ src/gameplay/statistics/top.h | 2 +- 2 files changed, 26 insertions(+), 68 deletions(-) diff --git a/src/gameplay/statistics/top.cpp b/src/gameplay/statistics/top.cpp index 714daff32..309f372ff 100644 --- a/src/gameplay/statistics/top.cpp +++ b/src/gameplay/statistics/top.cpp @@ -10,7 +10,6 @@ PlayerChart TopPlayer::chart_(kNumPlayerClasses); -PlayerChart TopPlayer::top_remort_(kNumPlayerClasses); // отдельное удаление из списка (для ренеймов, делетов и т.п.) // данная функция работает в том числе и с неполностью загруженным персонажем @@ -29,71 +28,34 @@ void TopPlayer::Remove(CharData *short_ch) { // данная функция работает в том числе и с неполностью загруженным персонажем // подробности в комментарии к load_char_ascii void TopPlayer::Refresh(CharData *short_ch, bool reboot) { - const int ch_remort = GetRealRemort(short_ch); - const long ch_exp = short_ch->get_exp(); - const long ch_uid = short_ch->get_uid(); - if (short_ch->IsNpc() || short_ch->IsFlagged(EPlrFlag::kFrozen) || short_ch->IsFlagged(EPlrFlag::kDeleted) || short_ch->IsImmortal()) { return; } - if (short_ch->get_name().empty()) { - return; - } - - if (GetRealRemort(short_ch) == kMaxRemort) { - auto &top_remort = TopPlayer::top_remort_[short_ch->GetClass()]; - auto it = std::find_if(top_remort.begin(), top_remort.end(), [ch_uid](const TopPlayer &p) { return p.unique_ == ch_uid; }); - - if (it == top_remort.end()) { - TopPlayer temp_player(ch_uid, GET_NAME(short_ch), ch_exp, ch_remort, 0); - top_remort_[short_ch->GetClass()].push_back(temp_player); - } - return; - } - auto &chart = TopPlayer::chart_[short_ch->GetClass()]; - - // Fast path: if chart is full and player can't get into top, skip entirely. - // Check against the last (weakest) entry in the sorted chart. - if (!reboot && chart.size() >= kPlayerChartSize) { - const auto &last = chart.back(); - const bool can_enter = ch_remort > last.remort_ - || (ch_remort == last.remort_ && ch_exp > last.exp_); - if (!can_enter) { - // Still need to remove if player is in chart (e.g., lost exp) - auto it = std::find_if(chart.begin(), chart.end(), [ch_uid](const TopPlayer &p) { return p.unique_ == ch_uid; }); - if (it != chart.end()) { - chart.erase(it); - } - return; - } - } - if (!reboot) { TopPlayer::Remove(short_ch); } std::list::iterator it_exp; - for (it_exp = chart.begin(); it_exp != chart.end(); ++it_exp) { - if (it_exp->remort_ < ch_remort - || (it_exp->remort_ == ch_remort && it_exp->exp_ < ch_exp)) { + for (it_exp = TopPlayer::chart_[short_ch->GetClass()].begin(); + it_exp != TopPlayer::chart_[short_ch->GetClass()].end(); ++it_exp) { + if (it_exp->remort_ < GetRealRemort(short_ch) + || (it_exp->remort_ == GetRealRemort(short_ch) && it_exp->exp_ < short_ch->get_exp())) { break; } } - TopPlayer temp_player(ch_uid, GET_NAME(short_ch), ch_exp, ch_remort, 0); - - if (it_exp != chart.end()) { - chart.insert(it_exp, temp_player); - } else { - chart.push_back(temp_player); + if (short_ch->get_name().empty()) { + return; // у нас все может быть } + TopPlayer temp_player(short_ch->get_uid(), GET_NAME(short_ch), short_ch->get_exp(), GetRealRemort(short_ch), 0); - // Trim chart to max size - while (chart.size() > kPlayerChartSize) { - chart.pop_back(); + if (it_exp != TopPlayer::chart_[short_ch->GetClass()].end()) { + TopPlayer::chart_[short_ch->GetClass()].insert(it_exp, temp_player); + } else { + TopPlayer::chart_[short_ch->GetClass()].push_back(temp_player); } } @@ -102,9 +64,8 @@ const PlayerChart &TopPlayer::Chart() { } void TopPlayer::PrintPlayersChart(CharData *ch) { - std::string out; + SendMsgToChar(" Лучшие персонажи игроков:\r\n", ch); - SendMsgToChar("&W Лучшие персонажи игроков:&n\r\n", ch); table_wrapper::Table table; for (const auto &it: TopPlayer::Chart()) { table @@ -116,32 +77,23 @@ void TopPlayer::PrintPlayersChart(CharData *ch) { table_wrapper::DecorateNoBorderTable(ch, table); table_wrapper::PrintTableToChar(ch, table); - SendMsgToChar(ch, "\r\n &WДостигшие максимум перевоплощений:&n\r\n"); - for (int i = to_underlying(ECharClass::kFirst); i <= to_underlying(ECharClass::kLast); i++) { - auto id = static_cast(i); - SendMsgToChar(ch, " %s: ", MUD::Class(id).GetName().c_str()); - if (top_remort_[id].size() > 0) { - for (const auto &it: top_remort_[id]) { - out += it.name_ + " "; - } - SendMsgToChar(ch, "%s", utils::OutWordsList(out, ch->player_specials->saved.stringLength).c_str()); - out.clear(); - } - SendMsgToChar(ch, "\r\n"); - } } void TopPlayer::PrintClassChart(CharData *ch, ECharClass id) { int count = 1; - std::ostringstream out; + std::ostringstream out; out << kColorWht << " Лучшие " << MUD::Class(id).GetPluralName() << ":" << kColorNrm << "\r\n"; for (auto &it: TopPlayer::chart_[id]) { + if (it.remort_ == kMaxRemort) + continue; it.number_ = count++; } table_wrapper::Table table; for (const auto &it: TopPlayer::chart_[id]) { + if (it.remort_ == kMaxRemort) + continue; table << it.number_ << it.name_ << it.remort_ @@ -196,9 +148,15 @@ void TopPlayer::PrintClassChart(CharData *ch, ECharClass id) { table_wrapper::DecorateNoBorderTable(ch, table4); table_wrapper::PrintTableToStream(out, table4); table_wrapper::Table table2; - if (top_remort_[id].size() > 0) { + upper.clear(); + for (const auto &it: TopPlayer::chart_[id]) { + if (it.remort_ != kMaxRemort) + continue; + upper.push_back(it); + } + if (upper.size() > 0) { out << kColorWht << "\r\n Достигшие максимум перевоплощений: " << kColorNrm << "\r\n"; - for (const auto &it: top_remort_[id]) { + for (auto &it : upper) { table2 << it.name_ << it.remort_ << GetDeclensionInNumber(it.remort_, EWhat::kRemort) << table_wrapper::kEndRow; diff --git a/src/gameplay/statistics/top.h b/src/gameplay/statistics/top.h index 8b0140fdd..f4b7f6506 100644 --- a/src/gameplay/statistics/top.h +++ b/src/gameplay/statistics/top.h @@ -43,7 +43,7 @@ class TopPlayer { int number_; static PlayerChart chart_; - static PlayerChart top_remort_; + static void PrintHelp(CharData *ch); static void PrintPlayersChart(CharData *ch); static void PrintClassChart(CharData *ch, ECharClass id); From d91c98be647b225005e2b1304814dd15e4d9a744 Mon Sep 17 00:00:00 2001 From: stribog Date: Sun, 17 May 2026 11:44:06 +0200 Subject: [PATCH 2/4] =?UTF-8?q?perf:=20TopPlayer::Refresh=20=E2=80=94=20?= =?UTF-8?q?=D0=BD=D0=B0=20pulse=20=D1=80=D0=B0=D0=B7=20=D0=B2=20RL-=D1=87?= =?UTF-8?q?=D0=B0=D1=81,=20=D0=BD=D0=B5=20=D0=BD=D0=B0=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=B6=D0=B4=D1=8B=D0=B9=20=D1=80=D0=B8=D0=BF=20(#3280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-kill TopPlayer::Refresh(ch) в perform_group_gain (fight_stuff.cpp:778) бил по profiler'у — для группы из K игроков на каждом рипе крутилось K линейных сканов chart_, без ограничения на длину. Именно из-за этого в #3142 ввели kPlayerChartSize=10, что в свою очередь сломало «Перед вами / Ваш текущий рейтинг / После вас» для всех вне топ-10. Теперь, после ревёрта #3142, обновляем chart_ периодически: - убран Refresh из perform_group_gain (и неиспользуемый include top.h) - добавлен TopPlayer::RefreshAll() — обходит character_list (онлайн), пропускной фильтр уже внутри Refresh - новый heartbeat-step «Top player chart refresh» с периодом RL-час (60 * 60 * kPassesPerSec), offset 43 -- не конкурирует с соседними hourly-шагами Логин/level/remort/admin set уже зовут Refresh точечно, периодический проход добивает изменения exp у активных игроков. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/engine/core/heartbeat.cpp | 5 +++++ src/gameplay/fight/fight_stuff.cpp | 2 -- src/gameplay/statistics/top.cpp | 9 +++++++++ src/gameplay/statistics/top.h | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/engine/core/heartbeat.cpp b/src/engine/core/heartbeat.cpp index b20040b57..5da2d2d54 100644 --- a/src/engine/core/heartbeat.cpp +++ b/src/engine/core/heartbeat.cpp @@ -36,6 +36,7 @@ #include "gameplay/communication/check_invoice.h" #include "gameplay/mechanics/depot.h" #include "gameplay/statistics/spell_usage.h" +#include "gameplay/statistics/top.h" #include "utils/tracing/trace_manager.h" #if defined WITH_SCRIPTING @@ -366,6 +367,10 @@ Heartbeat::steps_t &pulse_steps() { 60 * 60 * kPassesPerSec, 47, std::make_shared(print_rune_log)), + Heartbeat::PulseStep("Top player chart refresh", + 60 * 60 * kPassesPerSec, + 43, + std::make_shared(TopPlayer::RefreshAll)), Heartbeat::PulseStep("Mob stats saving", 60 * mob_stat::kSavePeriod * kPassesPerSec, 57, diff --git a/src/gameplay/fight/fight_stuff.cpp b/src/gameplay/fight/fight_stuff.cpp index c65f11834..7a8d0c251 100644 --- a/src/gameplay/fight/fight_stuff.cpp +++ b/src/gameplay/fight/fight_stuff.cpp @@ -17,7 +17,6 @@ #include "gameplay/clans/house.h" #include "pk.h" #include "gameplay/mechanics/stuff.h" -#include "gameplay/statistics/top.h" #include "engine/ui/color.h" #include "gameplay/statistics/mob_stat.h" #include "gameplay/mechanics/bonus.h" @@ -775,7 +774,6 @@ void perform_group_gain(CharData *ch, CharData *victim, int members, int koef) { if (!InTestZone(ch)) { EndowExpToChar(ch, exp); change_alignment(ch, victim); - TopPlayer::Refresh(ch); if (!(victim)->Temporary.get(EXTRA_GRP_KILL_COUNT) && !ch->IsNpc() && !ch->IsImmortal() diff --git a/src/gameplay/statistics/top.cpp b/src/gameplay/statistics/top.cpp index 309f372ff..36db43ee7 100644 --- a/src/gameplay/statistics/top.cpp +++ b/src/gameplay/statistics/top.cpp @@ -59,6 +59,15 @@ void TopPlayer::Refresh(CharData *short_ch, bool reboot) { } } +// Periodic full refresh of all online characters. Cheaper than calling Refresh +// on every mob kill (which was O(N) per group member per kill). Wired into the +// heartbeat once per RL hour -- see heartbeat.cpp. +void TopPlayer::RefreshAll() { + for (const auto &ch : character_list) { + TopPlayer::Refresh(ch.get()); + } +} + const PlayerChart &TopPlayer::Chart() { return chart_; } diff --git a/src/gameplay/statistics/top.h b/src/gameplay/statistics/top.h index f4b7f6506..a0bd027ab 100644 --- a/src/gameplay/statistics/top.h +++ b/src/gameplay/statistics/top.h @@ -34,6 +34,7 @@ class TopPlayer { static const PlayerChart &Chart(); static void Remove(CharData *ch); static void Refresh(CharData *ch, bool reboot = false); + static void RefreshAll(); private: long unique_; // уид From f3424cdee479df36faa2f07d01e00aa8aab6b3af Mon Sep 17 00:00:00 2001 From: stribog Date: Sun, 17 May 2026 11:56:39 +0200 Subject: [PATCH 3/4] =?UTF-8?q?refactor(top):=20RefreshAll()=20=E2=80=94?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B9=D1=82=D0=B8=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20descriptor=5Flist=20+=20kPlaying?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit character_list содержит мобов и чармисов помимо онлайн-игроков. Pull из descriptor_list с фильтром state == kPlaying ограничивает обход реально играющими PC и заодно пропускает соединения в меню / character generation. Оффлайн-игроки сохраняют свои записи в chart_ с буткового прохода (ActualizePlayersIndex -> TopPlayer::Refresh) — их состояние не меняется, пока они вне игры, обновлять нечего. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gameplay/statistics/top.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/gameplay/statistics/top.cpp b/src/gameplay/statistics/top.cpp index 36db43ee7..bdb9b6b0d 100644 --- a/src/gameplay/statistics/top.cpp +++ b/src/gameplay/statistics/top.cpp @@ -4,6 +4,8 @@ #include "engine/ui/color.h" #include "gameplay/mechanics/glory_const.h" #include "engine/entities/char_data.h" +#include "engine/core/comm.h" +#include "engine/network/descriptor_data.h" #include "engine/db/global_objects.h" #include "engine/ui/table_wrapper.h" #include "utils/utils_time.h" @@ -59,12 +61,20 @@ void TopPlayer::Refresh(CharData *short_ch, bool reboot) { } } -// Periodic full refresh of all online characters. Cheaper than calling Refresh -// on every mob kill (which was O(N) per group member per kill). Wired into the -// heartbeat once per RL hour -- see heartbeat.cpp. +// Periodic refresh of online players' chart entries. Replaces the per-kill +// Refresh in perform_group_gain (which scaled badly with group size and +// chart_ length). Wired into the heartbeat once per RL hour -- see +// heartbeat.cpp. +// +// Walks descriptor_list and filters to descriptors in kPlaying state -- that +// avoids touching mobs/charmices from character_list and skips connections +// that are still in menus / character generation. Offline players keep their +// chart_ entries from boot (state hasn't changed while they were offline). void TopPlayer::RefreshAll() { - for (const auto &ch : character_list) { - TopPlayer::Refresh(ch.get()); + for (DescriptorData *d = descriptor_list; d; d = d->next) { + if (d->state == EConState::kPlaying && d->character) { + TopPlayer::Refresh(d->character.get()); + } } } From d2fc00cbf98d0669a3f46532c5a63ca3f8fa7181 Mon Sep 17 00:00:00 2001 From: stribog Date: Sun, 17 May 2026 12:03:37 +0200 Subject: [PATCH 4/4] =?UTF-8?q?feat(top):=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=80?= =?UTF-8?q?=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8=20RefreshAll()=20?= =?UTF-8?q?=D0=B2=20SYSLOG=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=86=D0=B5=D0=BD?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Через utils::CExecutionTimer оборачиваем периодический проход и пишем в SYSLOG строку вида: TopPlayer::RefreshAll: N игроков обновлено за X.XXX мс Чтобы понять, какова реальная стоимость периодического рефреша на живых нагрузках (сейчас зовётся раз в RL-час из heartbeat). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gameplay/statistics/top.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gameplay/statistics/top.cpp b/src/gameplay/statistics/top.cpp index bdb9b6b0d..18085aa0a 100644 --- a/src/gameplay/statistics/top.cpp +++ b/src/gameplay/statistics/top.cpp @@ -8,6 +8,7 @@ #include "engine/network/descriptor_data.h" #include "engine/db/global_objects.h" #include "engine/ui/table_wrapper.h" +#include "utils/logger.h" #include "utils/utils_time.h" @@ -71,11 +72,16 @@ void TopPlayer::Refresh(CharData *short_ch, bool reboot) { // that are still in menus / character generation. Offline players keep their // chart_ entries from boot (state hasn't changed while they were offline). void TopPlayer::RefreshAll() { + utils::CExecutionTimer timer; + int refreshed = 0; for (DescriptorData *d = descriptor_list; d; d = d->next) { if (d->state == EConState::kPlaying && d->character) { TopPlayer::Refresh(d->character.get()); + ++refreshed; } } + log("TopPlayer::RefreshAll: %d игроков обновлено за %.3f мс", + refreshed, timer.delta().count() * 1000.0); } const PlayerChart &TopPlayer::Chart() {