diff --git a/CMakeLists.txt b/CMakeLists.txt
index 571ae59a0..89ddfc482 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3027,6 +3027,8 @@ if(CLIENT)
components/bestclient/gif_bubbles.h
components/bestclient/ego_finished_maps.cpp
components/bestclient/ego_finished_maps.h
+ components/bestclient/legit_finished_maps.cpp
+ components/bestclient/legit_finished_maps.h
components/bestclient/ego_tiles_prediction.cpp
components/bestclient/ego_tiles_prediction.h
components/bestclient/edgehelper.cpp
diff --git a/data/BestClient/languages/russian.txt b/data/BestClient/languages/russian.txt
index fbc18a18e..f94822c3f 100644
--- a/data/BestClient/languages/russian.txt
+++ b/data/BestClient/languages/russian.txt
@@ -1202,6 +1202,10 @@ Short EGO/KoG server name
Show finished map on EGO
== Показывать пройденную карту на EGO
+[BestClient]
+Show finished map on Legit Network
+== Показывать пройденную карту на Legit Network
+
[BestClient]
Use BestClient MasterServer
== Использовать мастерсервер BestClient
diff --git a/data/BestClient/languages/simplified_chinese.txt b/data/BestClient/languages/simplified_chinese.txt
index 6f55254a0..ed56a5c05 100644
--- a/data/BestClient/languages/simplified_chinese.txt
+++ b/data/BestClient/languages/simplified_chinese.txt
@@ -1202,6 +1202,10 @@ Short EGO/KoG server name
Show finished map on EGO
== 在 EGO 上显示已完成地图
+[BestClient]
+Show finished map on Legit Network
+== 在 Legit Network 上显示已完成地图
+
[BestClient]
Use BestClient MasterServer
== 使用 BestClient 主服务器
diff --git a/src/engine/shared/config_variables_bestclient.h b/src/engine/shared/config_variables_bestclient.h
index f5d28c8e0..b9ff899c7 100644
--- a/src/engine/shared/config_variables_bestclient.h
+++ b/src/engine/shared/config_variables_bestclient.h
@@ -316,6 +316,7 @@ MACRO_CONFIG_INT(BcAutoServerListRefresh, bc_auto_server_list_refresh, 0, 0, 1,
MACRO_CONFIG_INT(BcAutoServerListRefreshSeconds, bc_auto_server_list_refresh_seconds, 10, 1, 300, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Auto refresh interval for the server browser list in seconds")
MACRO_CONFIG_INT(BcUseShortKogServerName, bc_use_short_kog_server_name, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Use short names for E-Gores and KoG servers in browser list")
MACRO_CONFIG_INT(BcShowFinishedMapOnEgo, bc_show_finished_map_on_ego, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show completed EGO maps in the server browser")
+MACRO_CONFIG_INT(BcShowFinishedMapOnLegit, bc_show_finished_map_on_legit, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show completed Legit Network maps in the server browser")
MACRO_CONFIG_INT(BcMastersrv, bc_mastersrv, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Use the BestClient master server mirror for the server browser")
MACRO_CONFIG_INT(BcServerMapPreview, bc_server_map_preview, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show a live map preview under the selected server")
MACRO_CONFIG_INT(BrFilterBestclient, br_filter_bestclient, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Filter out servers with no BestClient users")
diff --git a/src/game/client/components/bestclient/legit_finished_maps.cpp b/src/game/client/components/bestclient/legit_finished_maps.cpp
new file mode 100644
index 000000000..d395e4196
--- /dev/null
+++ b/src/game/client/components/bestclient/legit_finished_maps.cpp
@@ -0,0 +1,159 @@
+#include "legit_finished_maps.h"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace
+{
+enum class ELegitMode
+{
+ NONE,
+ GORES,
+ DDRACE,
+};
+
+ELegitMode LegitMode(const CServerInfo *pServer)
+{
+ if(!pServer || (str_comp_nocase(pServer->m_aCommunityId, "legit") != 0 && str_find_nocase(pServer->m_aName, "Legit Network") == nullptr))
+ return ELegitMode::NONE;
+
+ if(str_find_nocase(pServer->m_aGameType, "Gores") != nullptr || str_find_nocase(pServer->m_aName, "Gores") != nullptr)
+ return ELegitMode::GORES;
+ if(str_find_nocase(pServer->m_aGameType, "DDraceNetwork") != nullptr)
+ return ELegitMode::DDRACE;
+ return ELegitMode::NONE;
+}
+
+bool ParseMapArray(const json_value *pRoot, const char *pKey, std::unordered_set *pMaps)
+{
+ const json_value *pArray = json_object_get(pRoot, pKey);
+ if(pArray == &json_value_none || pArray->type != json_array)
+ return false;
+
+ for(unsigned i = 0; i < pArray->u.array.length; ++i)
+ {
+ const json_value *pMap = pArray->u.array.values[i];
+ if(pMap && pMap->type == json_string && pMap->u.string.ptr[0] != '\0')
+ pMaps->emplace(pMap->u.string.ptr);
+ }
+ return true;
+}
+
+bool ParseFinishedMaps(const json_value *pRoot, std::unordered_set *pGoresMaps, std::unordered_set *pDDraceMaps)
+{
+ if(!pRoot || pRoot == &json_value_none || pRoot->type != json_object)
+ return false;
+ return ParseMapArray(pRoot, "gores", pGoresMaps) && ParseMapArray(pRoot, "ddrace", pDDraceMaps);
+}
+}
+
+bool CLegitFinishedMaps::Enabled() const
+{
+ return g_Config.m_BcShowFinishedMapOnLegit != 0;
+}
+
+bool CLegitFinishedMaps::IsFinishedMap(const CServerInfo *pServer) const
+{
+ switch(LegitMode(pServer))
+ {
+ case ELegitMode::GORES:
+ return Enabled() && m_GoresFinishedMaps.contains(pServer->m_aMap);
+ case ELegitMode::DDRACE:
+ return Enabled() && m_DDraceFinishedMaps.contains(pServer->m_aMap);
+ case ELegitMode::NONE:
+ return false;
+ }
+ return false;
+}
+
+void CLegitFinishedMaps::ResetRequest()
+{
+ if(m_pTask)
+ m_pTask->Abort();
+ m_pTask.reset();
+}
+
+void CLegitFinishedMaps::OnReset()
+{
+ ResetRequest();
+}
+
+void CLegitFinishedMaps::StartRequest()
+{
+ char aEscapedPlayerName[256];
+ EscapeUrl(aEscapedPlayerName, sizeof(aEscapedPlayerName), m_aPlayerName);
+
+ char aUrl[512];
+ str_format(aUrl, sizeof(aUrl), "https://legit.tw/api_finished_maps.php?name=%s", aEscapedPlayerName);
+
+ m_pTask = HttpGet(aUrl);
+ m_pTask->Timeout(CTimeout{8000, 0, 500, 5});
+ m_pTask->LogProgress(HTTPLOG::FAILURE);
+ m_pTask->FailOnErrorStatus(false);
+ Http()->Run(m_pTask);
+}
+
+void CLegitFinishedMaps::ProcessRequest()
+{
+ if(!m_pTask || !m_pTask->Done())
+ return;
+
+ bool Success = false;
+ if(m_pTask->State() == EHttpState::DONE && m_pTask->StatusCode() == 200)
+ {
+ json_value *pRoot = m_pTask->ResultJson();
+ std::unordered_set GoresMaps;
+ std::unordered_set DDraceMaps;
+ Success = ParseFinishedMaps(pRoot, &GoresMaps, &DDraceMaps);
+ if(Success)
+ {
+ m_GoresFinishedMaps = std::move(GoresMaps);
+ m_DDraceFinishedMaps = std::move(DDraceMaps);
+ str_copy(m_aLoadedPlayerName, m_aPlayerName, sizeof(m_aLoadedPlayerName));
+ }
+ if(pRoot)
+ json_value_free(pRoot);
+ }
+
+ if(!Success)
+ m_NextRetry = time_get() + RETRY_DELAY_SECONDS * time_freq();
+ m_pTask.reset();
+}
+
+void CLegitFinishedMaps::OnUpdate()
+{
+ if(!Enabled())
+ {
+ ResetRequest();
+ m_GoresFinishedMaps.clear();
+ m_DDraceFinishedMaps.clear();
+ m_aPlayerName[0] = '\0';
+ m_aLoadedPlayerName[0] = '\0';
+ m_NextRetry = 0;
+ return;
+ }
+
+ const char *pPlayerName = Client()->PlayerName();
+ if(str_comp(m_aPlayerName, pPlayerName) != 0)
+ {
+ ResetRequest();
+ m_GoresFinishedMaps.clear();
+ m_DDraceFinishedMaps.clear();
+ m_aLoadedPlayerName[0] = '\0';
+ m_NextRetry = 0;
+ str_copy(m_aPlayerName, pPlayerName, sizeof(m_aPlayerName));
+ }
+
+ ProcessRequest();
+
+ if(!m_aPlayerName[0] || m_pTask || str_comp(m_aPlayerName, m_aLoadedPlayerName) == 0 || time_get() < m_NextRetry)
+ return;
+
+ StartRequest();
+}
diff --git a/src/game/client/components/bestclient/legit_finished_maps.h b/src/game/client/components/bestclient/legit_finished_maps.h
new file mode 100644
index 000000000..7234c8319
--- /dev/null
+++ b/src/game/client/components/bestclient/legit_finished_maps.h
@@ -0,0 +1,43 @@
+#ifndef GAME_CLIENT_COMPONENTS_BESTCLIENT_LEGIT_FINISHED_MAPS_H
+#define GAME_CLIENT_COMPONENTS_BESTCLIENT_LEGIT_FINISHED_MAPS_H
+
+#include
+
+#include
+#include
+#include
+
+class IHttpRequest;
+class CServerInfo;
+
+class CLegitFinishedMaps : public CComponent
+{
+public:
+ int Sizeof() const override { return sizeof(*this); }
+
+ void OnReset() override;
+ void OnUpdate() override;
+
+ bool IsFinishedMap(const CServerInfo *pServer) const;
+
+private:
+ enum
+ {
+ PLAYER_NAME_LENGTH = 64,
+ RETRY_DELAY_SECONDS = 30,
+ };
+
+ std::unordered_set m_GoresFinishedMaps;
+ std::unordered_set m_DDraceFinishedMaps;
+ std::shared_ptr m_pTask;
+ char m_aPlayerName[PLAYER_NAME_LENGTH] = "";
+ char m_aLoadedPlayerName[PLAYER_NAME_LENGTH] = "";
+ int64_t m_NextRetry = 0;
+
+ bool Enabled() const;
+ void ResetRequest();
+ void StartRequest();
+ void ProcessRequest();
+};
+
+#endif
diff --git a/src/game/client/components/bestclient/menus_bestclient.cpp b/src/game/client/components/bestclient/menus_bestclient.cpp
index 38f27fe5e..2e57a8b24 100644
--- a/src/game/client/components/bestclient/menus_bestclient.cpp
+++ b/src/game/client/components/bestclient/menus_bestclient.cpp
@@ -3962,7 +3962,7 @@ void CMenus::RenderSettingsBestClientOthers(CUIRect MainView)
Column.HSplitTop(MarginBetweenViews, nullptr, &Column);
- const float BrowserUtilsBlockHeight = 6.0f * LineSize + 5.0f * MarginSmall;
+ const float BrowserUtilsBlockHeight = 7.0f * LineSize + 6.0f * MarginSmall;
CUIRect BrowserUtilsBlock;
Column.HSplitTop(BrowserUtilsBlockHeight, &BrowserUtilsBlock, &Column);
@@ -3995,6 +3995,11 @@ void CMenus::RenderSettingsBestClientOthers(CUIRect MainView)
BcMenuBadges::DrawNew(Graphics(), Ui(), TextRender(), &Content, MarginSmall);
DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_BcShowFinishedMapOnEgo, BcLocalize("Show finished map on EGO"), &g_Config.m_BcShowFinishedMapOnEgo, &Content, LineSize);
+ BrowserUtilsBlock.HSplitTop(MarginSmall, nullptr, &BrowserUtilsBlock);
+ BrowserUtilsBlock.HSplitTop(LineSize, &Content, &BrowserUtilsBlock);
+ BcMenuBadges::DrawNew(Graphics(), Ui(), TextRender(), &Content, MarginSmall);
+ DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_BcShowFinishedMapOnLegit, BcLocalize("Show finished map on Legit Network"), &g_Config.m_BcShowFinishedMapOnLegit, &Content, LineSize);
+
BrowserUtilsBlock.HSplitTop(MarginSmall, nullptr, &BrowserUtilsBlock);
BrowserUtilsBlock.HSplitTop(LineSize, &Content, &BrowserUtilsBlock);
DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_BcMastersrv, BcLocalize("Use BestClient MasterServer"), &g_Config.m_BcMastersrv, &Content, LineSize);
diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp
index 3a3b9d961..86bfbd439 100644
--- a/src/game/client/components/menus_browser.cpp
+++ b/src/game/client/components/menus_browser.cpp
@@ -505,7 +505,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct
Button.VMargin(4.0f, &Button);
Button.VSplitLeft(Button.h, &Icon, &Button);
// bestclient
- if((g_Config.m_BrIndicateFinished && pItem->m_HasRank == CServerInfo::RANK_RANKED) || GameClient()->m_EgoFinishedMaps.IsFinishedMap(pItem))
+ if((g_Config.m_BrIndicateFinished && pItem->m_HasRank == CServerInfo::RANK_RANKED) || GameClient()->m_EgoFinishedMaps.IsFinishedMap(pItem) || GameClient()->m_LegitFinishedMaps.IsFinishedMap(pItem))
// bestclient
{
Icon.Margin(2.0f, &Icon);
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index 1fe839fe3..8975c6cbe 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -198,6 +198,7 @@ void CGameClient::OnConsoleInit()
&m_BrowserUtils, // bestclient
&m_TwitchChat, // bestclient
&m_EgoFinishedMaps, // bestclient
+ &m_LegitFinishedMaps, // bestclient
&m_ShowPoints, // bestclient
&m_ClientIndicator, // bestclient
&m_VoiceChat, // bestclient
diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h
index b1558b5fc..a5b0a506b 100644
--- a/src/game/client/gameclient.h
+++ b/src/game/client/gameclient.h
@@ -45,6 +45,7 @@
#include "components/bestclient/music_player.h" // bestclient
#include "components/bestclient/browser_utils.h" // bestclient
#include "components/bestclient/ego_finished_maps.h" // bestclient
+#include "components/bestclient/legit_finished_maps.h" // bestclient
#include "components/bestclient/ego_tiles_prediction.h" // bestclient
#include "components/bestclient/edgehelper.h" // bestclient
#include "components/bestclient/fast_practice.h" // bestclient
@@ -256,6 +257,7 @@ class CGameClient : public IGameClient
CBrowserUtils m_BrowserUtils; // bestclient
CTwitchChat m_TwitchChat; // bestclient
CEgoFinishedMaps m_EgoFinishedMaps; // bestclient
+ CLegitFinishedMaps m_LegitFinishedMaps; // bestclient
CEdgeHelper m_EdgeHelper; // bestclient
CFinishPrediction m_FinishPrediction; // bestclient
CKeystrokes m_Keystrokes; // bestclient