1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "menus.h"
4
5#include <base/log.h>
6
7#include <engine/engine.h>
8#include <engine/favorites.h>
9#include <engine/friends.h>
10#include <engine/gfx/image_manipulation.h>
11#include <engine/keys.h>
12#include <engine/serverbrowser.h>
13#include <engine/shared/config.h>
14#include <engine/shared/localization.h>
15#include <engine/textrender.h>
16
17#include <game/client/animstate.h>
18#include <game/client/components/countryflags.h>
19#include <game/client/gameclient.h>
20#include <game/client/ui.h>
21#include <game/client/ui_listbox.h>
22#include <game/localization.h>
23
24using namespace FontIcons;
25
26static constexpr ColorRGBA gs_HighlightedTextColor = ColorRGBA(0.4f, 0.4f, 1.0f, 1.0f);
27
28static ColorRGBA PlayerBackgroundColor(bool Friend, bool Clan, bool Afk, bool Inside)
29{
30 static const ColorRGBA COLORS[] = {ColorRGBA(0.5f, 1.0f, 0.5f), ColorRGBA(0.4f, 0.4f, 1.0f), ColorRGBA(0.75f, 0.75f, 0.75f)};
31 static const ColorRGBA COLORS_AFK[] = {ColorRGBA(1.0f, 1.0f, 0.5f), ColorRGBA(0.4f, 0.75f, 1.0f), ColorRGBA(0.6f, 0.6f, 0.6f)};
32 int i;
33 if(Friend)
34 i = 0;
35 else if(Clan)
36 i = 1;
37 else
38 i = 2;
39 return (Afk ? COLORS_AFK[i] : COLORS[i]).WithAlpha(alpha: Inside ? 0.45f : 0.3f);
40}
41
42template<size_t N>
43static void FormatServerbrowserPing(char (&aBuffer)[N], const CServerInfo *pInfo)
44{
45 if(!pInfo->m_LatencyIsEstimated)
46 {
47 str_format(aBuffer, sizeof(aBuffer), "%d", pInfo->m_Latency);
48 return;
49 }
50 static const char *const LOCATION_NAMES[CServerInfo::NUM_LOCS] = {
51 "", // LOC_UNKNOWN
52 Localizable(pStr: "AFR"), // LOC_AFRICA
53 Localizable(pStr: "ASI"), // LOC_ASIA
54 Localizable(pStr: "AUS"), // LOC_AUSTRALIA
55 Localizable(pStr: "EUR"), // LOC_EUROPE
56 Localizable(pStr: "NA"), // LOC_NORTH_AMERICA
57 Localizable(pStr: "SA"), // LOC_SOUTH_AMERICA
58 Localizable(pStr: "CHN"), // LOC_CHINA
59 };
60 dbg_assert(0 <= pInfo->m_Location && pInfo->m_Location < CServerInfo::NUM_LOCS, "location out of range");
61 str_copy(aBuffer, Localize(pStr: LOCATION_NAMES[pInfo->m_Location]));
62}
63
64static ColorRGBA GetPingTextColor(int Latency)
65{
66 return color_cast<ColorRGBA>(hsl: ColorHSLA((300.0f - std::clamp(val: Latency, lo: 0, hi: 300)) / 1000.0f, 1.0f, 0.5f));
67}
68
69static ColorRGBA GetGametypeTextColor(const char *pGametype)
70{
71 ColorHSLA HslaColor;
72 if(str_comp(a: pGametype, b: "DM") == 0 || str_comp(a: pGametype, b: "TDM") == 0 || str_comp(a: pGametype, b: "CTF") == 0 || str_comp(a: pGametype, b: "LMS") == 0 || str_comp(a: pGametype, b: "LTS") == 0)
73 HslaColor = ColorHSLA(0.33f, 1.0f, 0.75f);
74 else if(str_find_nocase(haystack: pGametype, needle: "catch"))
75 HslaColor = ColorHSLA(0.17f, 1.0f, 0.75f);
76 else if(str_find_nocase(haystack: pGametype, needle: "dm") || str_find_nocase(haystack: pGametype, needle: "tdm") || str_find_nocase(haystack: pGametype, needle: "ctf") || str_find_nocase(haystack: pGametype, needle: "lms") || str_find_nocase(haystack: pGametype, needle: "lts"))
77 {
78 if(pGametype[0] == 'i' || pGametype[0] == 'g')
79 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
80 else
81 HslaColor = ColorHSLA(0.40f, 1.0f, 0.75f);
82 }
83 else if(str_find_nocase(haystack: pGametype, needle: "f-ddrace") || str_find_nocase(haystack: pGametype, needle: "freeze"))
84 HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f);
85 else if(str_find_nocase(haystack: pGametype, needle: "fng"))
86 HslaColor = ColorHSLA(0.83f, 1.0f, 0.75f);
87 else if(str_find_nocase(haystack: pGametype, needle: "gores"))
88 HslaColor = ColorHSLA(0.525f, 1.0f, 0.75f);
89 else if(str_find_nocase(haystack: pGametype, needle: "BW"))
90 HslaColor = ColorHSLA(0.05f, 1.0f, 0.75f);
91 else if(str_find_nocase(haystack: pGametype, needle: "ddracenet") || str_find_nocase(haystack: pGametype, needle: "ddnet") || str_find_nocase(haystack: pGametype, needle: "0xf"))
92 HslaColor = ColorHSLA(0.58f, 1.0f, 0.75f);
93 else if(str_find_nocase(haystack: pGametype, needle: "ddrace") || str_find_nocase(haystack: pGametype, needle: "mkrace"))
94 HslaColor = ColorHSLA(0.75f, 1.0f, 0.75f);
95 else if(str_find_nocase(haystack: pGametype, needle: "race") || str_find_nocase(haystack: pGametype, needle: "fastcap"))
96 HslaColor = ColorHSLA(0.46f, 1.0f, 0.75f);
97 else if(str_find_nocase(haystack: pGametype, needle: "s-ddr"))
98 HslaColor = ColorHSLA(1.0f, 1.0f, 0.7f);
99 else
100 HslaColor = ColorHSLA(1.0f, 1.0f, 1.0f);
101 return color_cast<ColorRGBA>(hsl: HslaColor);
102}
103
104void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemActivated)
105{
106 static CListBox s_ListBox;
107
108 CUIRect Headers;
109 View.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &View);
110 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
111 Headers.VSplitRight(Cut: s_ListBox.ScrollbarWidthMax(), pLeft: &Headers, pRight: nullptr);
112 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
113
114 struct SColumn
115 {
116 int m_Id;
117 int m_Sort;
118 const char *m_pCaption;
119 int m_Direction;
120 float m_Width;
121 CUIRect m_Rect;
122 };
123
124 enum
125 {
126 COL_FLAG_LOCK = 0,
127 COL_FLAG_FAV,
128 COL_COMMUNITY,
129 COL_NAME,
130 COL_GAMETYPE,
131 COL_MAP,
132 COL_FRIENDS,
133 COL_PLAYERS,
134 COL_PING,
135 };
136
137 enum
138 {
139 UI_ELEM_LOCK_ICON = 0,
140 UI_ELEM_FAVORITE_ICON,
141 UI_ELEM_NAME_1,
142 UI_ELEM_NAME_2,
143 UI_ELEM_NAME_3,
144 UI_ELEM_GAMETYPE,
145 UI_ELEM_MAP_1,
146 UI_ELEM_MAP_2,
147 UI_ELEM_MAP_3,
148 UI_ELEM_FINISH_ICON,
149 UI_ELEM_PLAYERS,
150 UI_ELEM_FRIEND_ICON,
151 UI_ELEM_PING,
152 UI_ELEM_KEY_ICON,
153 NUM_UI_ELEMS,
154 };
155
156 static SColumn s_aCols[] = {
157 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 2.0f, .m_Rect: {.x: 0}},
158 {.m_Id: COL_FLAG_LOCK, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 14.0f, .m_Rect: {.x: 0}},
159 {.m_Id: COL_FLAG_FAV, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 14.0f, .m_Rect: {.x: 0}},
160 {.m_Id: COL_COMMUNITY, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_Width: 28.0f, .m_Rect: {.x: 0}},
161 {.m_Id: COL_NAME, .m_Sort: IServerBrowser::SORT_NAME, .m_pCaption: Localizable(pStr: "Name"), .m_Direction: 0, .m_Width: 50.0f, .m_Rect: {.x: 0}},
162 {.m_Id: COL_GAMETYPE, .m_Sort: IServerBrowser::SORT_GAMETYPE, .m_pCaption: Localizable(pStr: "Type"), .m_Direction: 1, .m_Width: 50.0f, .m_Rect: {.x: 0}},
163 {.m_Id: COL_MAP, .m_Sort: IServerBrowser::SORT_MAP, .m_pCaption: Localizable(pStr: "Map"), .m_Direction: 1, .m_Width: 120.0f + (Headers.w - 480) / 8, .m_Rect: {.x: 0}},
164 {.m_Id: COL_FRIENDS, .m_Sort: IServerBrowser::SORT_NUMFRIENDS, .m_pCaption: "", .m_Direction: 1, .m_Width: 20.0f, .m_Rect: {.x: 0}},
165 {.m_Id: COL_PLAYERS, .m_Sort: IServerBrowser::SORT_NUMPLAYERS, .m_pCaption: Localizable(pStr: "Players"), .m_Direction: 1, .m_Width: 60.0f, .m_Rect: {.x: 0}},
166 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_Width: 4.0f, .m_Rect: {.x: 0}},
167 {.m_Id: COL_PING, .m_Sort: IServerBrowser::SORT_PING, .m_pCaption: Localizable(pStr: "Ping"), .m_Direction: 1, .m_Width: 40.0f, .m_Rect: {.x: 0}},
168 };
169
170 const int NumCols = std::size(s_aCols);
171
172 // do layout
173 for(int i = 0; i < NumCols; i++)
174 {
175 if(s_aCols[i].m_Direction == -1)
176 {
177 Headers.VSplitLeft(Cut: s_aCols[i].m_Width, pLeft: &s_aCols[i].m_Rect, pRight: &Headers);
178
179 if(i + 1 < NumCols)
180 {
181 Headers.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Headers);
182 }
183 }
184 }
185
186 for(int i = NumCols - 1; i >= 0; i--)
187 {
188 if(s_aCols[i].m_Direction == 1)
189 {
190 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
191 Headers.VSplitRight(Cut: 2.0f, pLeft: &Headers, pRight: nullptr);
192 }
193 }
194
195 for(auto &Col : s_aCols)
196 {
197 if(Col.m_Direction == 0)
198 Col.m_Rect = Headers;
199 }
200
201 const bool PlayersOrPing = (g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS || g_Config.m_BrSort == IServerBrowser::SORT_PING);
202
203 // do headers
204 for(const auto &Col : s_aCols)
205 {
206 int Checked = g_Config.m_BrSort == Col.m_Sort;
207 if(PlayersOrPing && g_Config.m_BrSortOrder == 2 && (Col.m_Sort == IServerBrowser::SORT_NUMPLAYERS || Col.m_Sort == IServerBrowser::SORT_PING))
208 Checked = 2;
209
210 if(DoButton_GridHeader(pId: &Col.m_Id, pText: Localize(pStr: Col.m_pCaption), Checked, pRect: &Col.m_Rect))
211 {
212 if(Col.m_Sort != -1)
213 {
214 if(g_Config.m_BrSort == Col.m_Sort)
215 g_Config.m_BrSortOrder = (g_Config.m_BrSortOrder + 1) % (PlayersOrPing ? 3 : 2);
216 else
217 g_Config.m_BrSortOrder = 0;
218 g_Config.m_BrSort = Col.m_Sort;
219 }
220 }
221
222 if(Col.m_Id == COL_FRIENDS)
223 {
224 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
225 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
226 Ui()->DoLabel(pRect: &Col.m_Rect, pText: FONT_ICON_HEART, Size: 14.0f, Align: TEXTALIGN_MC);
227 TextRender()->SetRenderFlags(0);
228 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
229 }
230 }
231
232 const int NumServers = ServerBrowser()->NumSortedServers();
233
234 // display important messages in the middle of the screen so no
235 // users misses it
236 {
237 if(!ServerBrowser()->NumServers() && ServerBrowser()->IsGettingServerlist())
238 {
239 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "Getting server list from master server"), Size: 16.0f, Align: TEXTALIGN_MC);
240 }
241 else if(!ServerBrowser()->NumServers())
242 {
243 if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN)
244 {
245 char aBuf[128];
246 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "No local servers found (ports %d-%d)"), IServerBrowser::LAN_PORT_BEGIN, IServerBrowser::LAN_PORT_END);
247 Ui()->DoLabel(pRect: &View, pText: aBuf, Size: 16.0f, Align: TEXTALIGN_MC);
248 }
249 else if(ServerBrowser()->IsServerlistError())
250 {
251 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "Could not get server list from master server"), Size: 16.0f, Align: TEXTALIGN_MC);
252 }
253 else
254 {
255 Ui()->DoLabel(pRect: &View, pText: Localize(pStr: "No servers found"), Size: 16.0f, Align: TEXTALIGN_MC);
256 }
257 }
258 else if(ServerBrowser()->NumServers() && !NumServers)
259 {
260 CUIRect Label, ResetButton;
261 View.HMargin(Cut: (View.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &Label);
262 Label.HSplitTop(Cut: 16.0f, pTop: &Label, pBottom: &ResetButton);
263 ResetButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ResetButton);
264 ResetButton.VMargin(Cut: (ResetButton.w - 200.0f) / 2.0f, pOtherRect: &ResetButton);
265 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "No servers match your filter criteria"), Size: 16.0f, Align: TEXTALIGN_MC);
266 static CButtonContainer s_ResetButton;
267 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
268 {
269 ResetServerbrowserFilters();
270 }
271 }
272 }
273
274 s_ListBox.SetActive(!Ui()->IsPopupOpen());
275 s_ListBox.DoStart(RowHeight: ms_ListheaderHeight, NumItems: NumServers, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false);
276
277 if(m_ServerBrowserShouldRevealSelection)
278 {
279 s_ListBox.ScrollToSelected();
280 m_ServerBrowserShouldRevealSelection = false;
281 }
282 m_SelectedIndex = -1;
283
284 const auto &&RenderBrowserIcons = [this](CUIElement::SUIElementRect &UIRect, CUIRect *pRect, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor, const char *pText, int TextAlign, bool SmallFont = false) {
285 const float FontSize = SmallFont ? 6.0f : 14.0f;
286 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
287 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
288 TextRender()->TextColor(Color: TextColor);
289 TextRender()->TextOutlineColor(Color: TextOutlineColor);
290 Ui()->DoLabelStreamed(RectEl&: UIRect, pRect, pText, Size: FontSize, Align: TextAlign);
291 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
292 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
293 TextRender()->SetRenderFlags(0);
294 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
295 };
296
297 std::vector<CUIElement *> &vpServerBrowserUiElements = m_avpServerBrowserUiElements[ServerBrowser()->GetCurrentType()];
298 if(vpServerBrowserUiElements.size() < (size_t)NumServers)
299 vpServerBrowserUiElements.resize(new_size: NumServers, x: nullptr);
300
301 for(int i = 0; i < NumServers; i++)
302 {
303 const CServerInfo *pItem = ServerBrowser()->SortedGet(Index: i);
304 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: pItem->m_aCommunityId);
305
306 if(vpServerBrowserUiElements[i] == nullptr)
307 {
308 vpServerBrowserUiElements[i] = Ui()->GetNewUIElement(RequestedRectCount: NUM_UI_ELEMS);
309 }
310 CUIElement *pUiElement = vpServerBrowserUiElements[i];
311
312 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: str_comp(a: pItem->m_aAddress, b: g_Config.m_UiServerAddress) == 0);
313 if(ListItem.m_Selected)
314 m_SelectedIndex = i;
315
316 if(!ListItem.m_Visible)
317 {
318 // reset active item, if not visible
319 if(Ui()->CheckActiveItem(pId: pItem))
320 Ui()->SetActiveItem(nullptr);
321
322 // don't render invisible items
323 continue;
324 }
325
326 const float FontSize = 12.0f;
327 char aTemp[64];
328 for(const auto &Col : s_aCols)
329 {
330 CUIRect Button;
331 Button.x = Col.m_Rect.x;
332 Button.y = ListItem.m_Rect.y;
333 Button.h = ListItem.m_Rect.h;
334 Button.w = Col.m_Rect.w;
335
336 const int Id = Col.m_Id;
337 if(Id == COL_FLAG_LOCK)
338 {
339 if(pItem->m_Flags & SERVER_FLAG_PASSWORD)
340 {
341 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_LOCK_ICON), &Button, ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_LOCK, TEXTALIGN_MC);
342 }
343 else if(pItem->m_RequiresLogin)
344 {
345 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_KEY_ICON), &Button, ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_KEY, TEXTALIGN_MC);
346 }
347 }
348 else if(Id == COL_FLAG_FAV)
349 {
350 if(pItem->m_Favorite != TRISTATE::NONE)
351 {
352 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FAVORITE_ICON), &Button, ColorRGBA(1.0f, 0.85f, 0.3f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_STAR, TEXTALIGN_MC);
353 }
354 }
355 else if(Id == COL_COMMUNITY)
356 {
357 if(pCommunity != nullptr)
358 {
359 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
360 if(pIcon != nullptr)
361 {
362 CUIRect CommunityIcon;
363 Button.Margin(Cut: 2.0f, pOtherRect: &CommunityIcon);
364 m_CommunityIcons.Render(pIcon, Rect: CommunityIcon, Active: true);
365 Ui()->DoButtonLogic(pId: &pItem->m_aCommunityId, Checked: 0, pRect: &CommunityIcon, Flags: BUTTONFLAG_NONE);
366 GameClient()->m_Tooltips.DoToolTip(pId: &pItem->m_aCommunityId, pNearRect: &CommunityIcon, pText: pCommunity->Name());
367 }
368 }
369 }
370 else if(Id == COL_NAME)
371 {
372 SLabelProperties Props;
373 Props.m_MaxWidth = Button.w;
374 Props.m_StopAtEnd = true;
375 Props.m_EnableWidthCheck = false;
376 bool Printed = false;
377 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_SERVERNAME))
378 Printed = PrintHighlighted(pName: pItem->m_aName, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
379 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_1), pRect: &Button, pText: pItem->m_aName, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: (int)(pFilteredStr - pItem->m_aName));
380 TextRender()->TextColor(Color: gs_HighlightedTextColor);
381 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_2), pRect: &Button, pText: pFilteredStr, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: FilterLen, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_NAME_1)->m_Cursor);
382 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
383 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_3), pRect: &Button, pText: pFilteredStr + FilterLen, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: -1, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_NAME_2)->m_Cursor);
384 });
385 if(!Printed)
386 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_NAME_1), pRect: &Button, pText: pItem->m_aName, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
387 }
388 else if(Id == COL_GAMETYPE)
389 {
390 SLabelProperties Props;
391 Props.m_MaxWidth = Button.w;
392 Props.m_StopAtEnd = true;
393 Props.m_EnableWidthCheck = false;
394 if(g_Config.m_UiColorizeGametype)
395 {
396 TextRender()->TextColor(Color: GetGametypeTextColor(pGametype: pItem->m_aGameType));
397 }
398 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_GAMETYPE), pRect: &Button, pText: pItem->m_aGameType, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
399 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
400 }
401 else if(Id == COL_MAP)
402 {
403 {
404 CUIRect Icon;
405 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
406 Button.VSplitLeft(Cut: Button.h, pLeft: &Icon, pRight: &Button);
407 if(g_Config.m_BrIndicateFinished && pItem->m_HasRank == CServerInfo::RANK_RANKED)
408 {
409 Icon.Margin(Cut: 2.0f, pOtherRect: &Icon);
410 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FINISH_ICON), &Icon, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), FONT_ICON_FLAG_CHECKERED, TEXTALIGN_MC);
411 }
412 }
413
414 SLabelProperties Props;
415 Props.m_MaxWidth = Button.w;
416 Props.m_StopAtEnd = true;
417 Props.m_EnableWidthCheck = false;
418 bool Printed = false;
419 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_MAPNAME))
420 Printed = PrintHighlighted(pName: pItem->m_aMap, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
421 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_1), pRect: &Button, pText: pItem->m_aMap, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: (int)(pFilteredStr - pItem->m_aMap));
422 TextRender()->TextColor(Color: gs_HighlightedTextColor);
423 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_2), pRect: &Button, pText: pFilteredStr, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: FilterLen, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_MAP_1)->m_Cursor);
424 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
425 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_3), pRect: &Button, pText: pFilteredStr + FilterLen, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props, StrLen: -1, pReadCursor: &pUiElement->Rect(Index: UI_ELEM_MAP_2)->m_Cursor);
426 });
427 if(!Printed)
428 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_MAP_1), pRect: &Button, pText: pItem->m_aMap, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: Props);
429 }
430 else if(Id == COL_FRIENDS)
431 {
432 if(pItem->m_FriendState != IFriends::FRIEND_NO)
433 {
434 RenderBrowserIcons(*pUiElement->Rect(Index: UI_ELEM_FRIEND_ICON), &Button, ColorRGBA(0.94f, 0.4f, 0.4f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_HEART, TEXTALIGN_MC);
435
436 if(pItem->m_FriendNum > 1)
437 {
438 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%d", pItem->m_FriendNum);
439 TextRender()->TextColor(r: 0.94f, g: 0.8f, b: 0.8f, a: 1.0f);
440 Ui()->DoLabel(pRect: &Button, pText: aTemp, Size: 9.0f, Align: TEXTALIGN_MC);
441 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
442 }
443 }
444 }
445 else if(Id == COL_PLAYERS)
446 {
447 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%i/%i", pItem->m_NumFilteredPlayers, ServerBrowser()->Max(Item: *pItem));
448 if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_PLAYER))
449 {
450 TextRender()->TextColor(Color: gs_HighlightedTextColor);
451 }
452 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_PLAYERS), pRect: &Button, pText: aTemp, Size: FontSize, Align: TEXTALIGN_MR);
453 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
454 }
455 else if(Id == COL_PING)
456 {
457 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
458 FormatServerbrowserPing(aBuffer&: aTemp, pInfo: pItem);
459 if(g_Config.m_UiColorizePing)
460 {
461 TextRender()->TextColor(Color: GetPingTextColor(Latency: pItem->m_Latency));
462 }
463 Ui()->DoLabelStreamed(RectEl&: *pUiElement->Rect(Index: UI_ELEM_PING), pRect: &Button, pText: aTemp, Size: FontSize, Align: TEXTALIGN_MR);
464 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
465 }
466 }
467 }
468
469 const int NewSelected = s_ListBox.DoEnd();
470 if(NewSelected != m_SelectedIndex)
471 {
472 m_SelectedIndex = NewSelected;
473 if(m_SelectedIndex >= 0)
474 {
475 // select the new server
476 const CServerInfo *pItem = ServerBrowser()->SortedGet(Index: NewSelected);
477 if(pItem)
478 {
479 str_copy(dst&: g_Config.m_UiServerAddress, src: pItem->m_aAddress);
480 m_ServerBrowserShouldRevealSelection = true;
481 }
482 }
483 }
484
485 WasListboxItemActivated = s_ListBox.WasItemActivated();
486}
487
488void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItemActivated)
489{
490 // Render bar that shows the loading progression.
491 // The bar is only shown while loading and fades out when it's done.
492 CUIRect RefreshBar;
493 StatusBox.HSplitTop(Cut: 5.0f, pTop: &RefreshBar, pBottom: &StatusBox);
494 static float s_LoadingProgressionFadeEnd = 0.0f;
495 if(ServerBrowser()->IsRefreshing() && ServerBrowser()->LoadingProgression() < 100)
496 {
497 s_LoadingProgressionFadeEnd = Client()->GlobalTime() + 2.0f;
498 }
499 const float LoadingProgressionTimeDiff = s_LoadingProgressionFadeEnd - Client()->GlobalTime();
500 if(LoadingProgressionTimeDiff > 0.0f)
501 {
502 const float RefreshBarAlpha = minimum(a: LoadingProgressionTimeDiff, b: 0.8f);
503 RefreshBar.h = 2.0f;
504 RefreshBar.w *= ServerBrowser()->LoadingProgression() / 100.0f;
505 RefreshBar.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, RefreshBarAlpha), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f);
506 }
507
508 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
509 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
510 const float SearchExcludeAddrStrMax = 130.0f;
511 const float SearchIconWidth = TextRender()->TextWidth(Size: 16.0f, pText: FONT_ICON_MAGNIFYING_GLASS);
512 const float ExcludeIconWidth = TextRender()->TextWidth(Size: 16.0f, pText: FONT_ICON_BAN);
513 const float ExcludeSearchIconMax = maximum(a: SearchIconWidth, b: ExcludeIconWidth);
514 TextRender()->SetRenderFlags(0);
515 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
516
517 CUIRect SearchInfoAndAddr, ServersAndConnect, ServersPlayersOnline, SearchAndInfo, ServerAddr, ConnectButtons;
518 StatusBox.VSplitRight(Cut: 135.0f, pLeft: &SearchInfoAndAddr, pRight: &ServersAndConnect);
519 if(SearchInfoAndAddr.w > 350.0f)
520 SearchInfoAndAddr.VSplitLeft(Cut: 350.0f, pLeft: &SearchInfoAndAddr, pRight: nullptr);
521 SearchInfoAndAddr.HSplitTop(Cut: 40.0f, pTop: &SearchAndInfo, pBottom: &ServerAddr);
522 ServersAndConnect.HSplitTop(Cut: 35.0f, pTop: &ServersPlayersOnline, pBottom: &ConnectButtons);
523 ConnectButtons.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ConnectButtons);
524
525 CUIRect QuickSearch, QuickExclude;
526 SearchAndInfo.HSplitTop(Cut: 20.0f, pTop: &QuickSearch, pBottom: &QuickExclude);
527 QuickSearch.Margin(Cut: 2.0f, pOtherRect: &QuickSearch);
528 QuickExclude.Margin(Cut: 2.0f, pOtherRect: &QuickExclude);
529
530 // render quick search
531 {
532 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
533 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
534 Ui()->DoLabel(pRect: &QuickSearch, pText: FONT_ICON_MAGNIFYING_GLASS, Size: 16.0f, Align: TEXTALIGN_ML);
535 TextRender()->SetRenderFlags(0);
536 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
537 QuickSearch.VSplitLeft(Cut: ExcludeSearchIconMax, pLeft: nullptr, pRight: &QuickSearch);
538 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickSearch);
539
540 char aBufSearch[64];
541 str_format(buffer: aBufSearch, buffer_size: sizeof(aBufSearch), format: "%s:", Localize(pStr: "Search"));
542 Ui()->DoLabel(pRect: &QuickSearch, pText: aBufSearch, Size: 14.0f, Align: TEXTALIGN_ML);
543 QuickSearch.VSplitLeft(Cut: SearchExcludeAddrStrMax, pLeft: nullptr, pRight: &QuickSearch);
544 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickSearch);
545
546 static CLineInput s_FilterInput(g_Config.m_BrFilterString, sizeof(g_Config.m_BrFilterString));
547 static char s_aTooltipText[64];
548 str_format(buffer: s_aTooltipText, buffer_size: sizeof(s_aTooltipText), format: "%s: \"solo; nameless tee; kobra 2\"", Localize(pStr: "Example of usage"));
549 GameClient()->m_Tooltips.DoToolTip(pId: &s_FilterInput, pNearRect: &QuickSearch, pText: s_aTooltipText);
550 if(!Ui()->IsPopupOpen() && Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
551 {
552 Ui()->SetActiveItem(&s_FilterInput);
553 s_FilterInput.SelectAll();
554 }
555 if(Ui()->DoClearableEditBox(pLineInput: &s_FilterInput, pRect: &QuickSearch, FontSize: 12.0f))
556 Client()->ServerBrowserUpdate();
557 }
558
559 // render quick exclude
560 {
561 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
562 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
563 Ui()->DoLabel(pRect: &QuickExclude, pText: FONT_ICON_BAN, Size: 16.0f, Align: TEXTALIGN_ML);
564 TextRender()->SetRenderFlags(0);
565 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
566 QuickExclude.VSplitLeft(Cut: ExcludeSearchIconMax, pLeft: nullptr, pRight: &QuickExclude);
567 QuickExclude.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickExclude);
568
569 char aBufExclude[64];
570 str_format(buffer: aBufExclude, buffer_size: sizeof(aBufExclude), format: "%s:", Localize(pStr: "Exclude"));
571 Ui()->DoLabel(pRect: &QuickExclude, pText: aBufExclude, Size: 14.0f, Align: TEXTALIGN_ML);
572 QuickExclude.VSplitLeft(Cut: SearchExcludeAddrStrMax, pLeft: nullptr, pRight: &QuickExclude);
573 QuickExclude.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickExclude);
574
575 static CLineInput s_ExcludeInput(g_Config.m_BrExcludeString, sizeof(g_Config.m_BrExcludeString));
576 static char s_aTooltipText[64];
577 str_format(buffer: s_aTooltipText, buffer_size: sizeof(s_aTooltipText), format: "%s: \"CHN; [A]\"", Localize(pStr: "Example of usage"));
578 GameClient()->m_Tooltips.DoToolTip(pId: &s_ExcludeInput, pNearRect: &QuickSearch, pText: s_aTooltipText);
579 if(!Ui()->IsPopupOpen() && Input()->KeyPress(Key: KEY_X) && Input()->ShiftIsPressed() && Input()->ModifierIsPressed())
580 {
581 Ui()->SetActiveItem(&s_ExcludeInput);
582 s_ExcludeInput.SelectAll();
583 }
584 if(Ui()->DoClearableEditBox(pLineInput: &s_ExcludeInput, pRect: &QuickExclude, FontSize: 12.0f))
585 Client()->ServerBrowserUpdate();
586 }
587
588 // render status
589 {
590 CUIRect ServersOnline, PlayersOnline;
591 ServersPlayersOnline.HSplitMid(pTop: &PlayersOnline, pBottom: &ServersOnline);
592
593 char aBuf[128];
594 if(ServerBrowser()->NumServers() != 1)
595 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d of %d servers"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers());
596 else
597 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d of %d server"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers());
598 Ui()->DoLabel(pRect: &ServersOnline, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
599
600 if(ServerBrowser()->NumSortedPlayers() != 1)
601 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d players"), ServerBrowser()->NumSortedPlayers());
602 else
603 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d player"), ServerBrowser()->NumSortedPlayers());
604 Ui()->DoLabel(pRect: &PlayersOnline, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
605 }
606
607 // address info
608 {
609 CUIRect ServerAddrLabel, ServerAddrEditBox;
610 ServerAddr.Margin(Cut: 2.0f, pOtherRect: &ServerAddr);
611 ServerAddr.VSplitLeft(Cut: SearchExcludeAddrStrMax + 5.0f + ExcludeSearchIconMax + 5.0f, pLeft: &ServerAddrLabel, pRight: &ServerAddrEditBox);
612
613 Ui()->DoLabel(pRect: &ServerAddrLabel, pText: Localize(pStr: "Server address:"), Size: 14.0f, Align: TEXTALIGN_ML);
614 static CLineInput s_ServerAddressInput(g_Config.m_UiServerAddress, sizeof(g_Config.m_UiServerAddress));
615 if(Ui()->DoClearableEditBox(pLineInput: &s_ServerAddressInput, pRect: &ServerAddrEditBox, FontSize: 12.0f))
616 m_ServerBrowserShouldRevealSelection = true;
617 }
618
619 // buttons
620 {
621 CUIRect ButtonRefresh, ButtonConnect;
622 ConnectButtons.VSplitMid(pLeft: &ButtonRefresh, pRight: &ButtonConnect, Spacing: 5.0f);
623
624 // refresh button
625 {
626 char aLabelBuf[32] = {0};
627 const auto &&RefreshLabelFunc = [this, aLabelBuf]() mutable {
628 if(ServerBrowser()->IsRefreshing() || ServerBrowser()->IsGettingServerlist())
629 str_format(buffer: aLabelBuf, buffer_size: sizeof(aLabelBuf), format: "%s%s", FONT_ICON_ARROW_ROTATE_RIGHT, FONT_ICON_ELLIPSIS);
630 else
631 str_copy(dst&: aLabelBuf, src: FONT_ICON_ARROW_ROTATE_RIGHT);
632 return aLabelBuf;
633 };
634
635 SMenuButtonProperties Props;
636 Props.m_HintRequiresStringCheck = true;
637 Props.m_UseIconFont = true;
638
639 static CButtonContainer s_RefreshButton;
640 if(Ui()->DoButton_Menu(UIElement&: m_RefreshButton, pId: &s_RefreshButton, GetTextLambda: RefreshLabelFunc, pRect: &ButtonRefresh, Props) || (!Ui()->IsPopupOpen() && (Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))))
641 {
642 RefreshBrowserTab(Force: true);
643 }
644 }
645
646 // connect button
647 {
648 const auto &&ConnectLabelFunc = []() { return FONT_ICON_RIGHT_TO_BRACKET; };
649
650 SMenuButtonProperties Props;
651 Props.m_UseIconFont = true;
652 Props.m_Color = ColorRGBA(0.5f, 1.0f, 0.5f, 0.5f);
653
654 static CButtonContainer s_ConnectButton;
655 if(Ui()->DoButton_Menu(UIElement&: m_ConnectButton, pId: &s_ConnectButton, GetTextLambda: ConnectLabelFunc, pRect: &ButtonConnect, Props) || WasListboxItemActivated || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
656 {
657 Connect(pAddress: g_Config.m_UiServerAddress);
658 }
659 }
660 }
661}
662
663void CMenus::Connect(const char *pAddress)
664{
665 if(Client()->State() == IClient::STATE_ONLINE && GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
666 {
667 str_copy(dst&: m_aNextServer, src: pAddress);
668 PopupConfirm(pTitle: Localize(pStr: "Disconnect"), pMessage: Localize(pStr: "Are you sure that you want to disconnect and switch to a different server?"), pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmSwitchServer);
669 }
670 else
671 Client()->Connect(pAddress);
672}
673
674void CMenus::PopupConfirmSwitchServer()
675{
676 Client()->Connect(pAddress: m_aNextServer);
677}
678
679void CMenus::RenderServerbrowserFilters(CUIRect View)
680{
681 const float RowHeight = 18.0f;
682 const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox
683
684 View.Margin(Cut: 5.0f, pOtherRect: &View);
685
686 CUIRect Button, ResetButton;
687 View.HSplitBottom(Cut: RowHeight, pTop: &View, pBottom: &ResetButton);
688 View.HSplitBottom(Cut: 3.0f, pTop: &View, pBottom: nullptr);
689
690 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
691 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterEmpty, pText: Localize(pStr: "Has people playing"), Checked: g_Config.m_BrFilterEmpty, pRect: &Button))
692 g_Config.m_BrFilterEmpty ^= 1;
693
694 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
695 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterSpectators, pText: Localize(pStr: "Count players only"), Checked: g_Config.m_BrFilterSpectators, pRect: &Button))
696 g_Config.m_BrFilterSpectators ^= 1;
697
698 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
699 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterFull, pText: Localize(pStr: "Server not full"), Checked: g_Config.m_BrFilterFull, pRect: &Button))
700 g_Config.m_BrFilterFull ^= 1;
701
702 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
703 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterFriends, pText: Localize(pStr: "Show friends only"), Checked: g_Config.m_BrFilterFriends, pRect: &Button))
704 g_Config.m_BrFilterFriends ^= 1;
705
706 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
707 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterPw, pText: Localize(pStr: "No password"), Checked: g_Config.m_BrFilterPw, pRect: &Button))
708 g_Config.m_BrFilterPw ^= 1;
709
710 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
711 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterLogin, pText: Localize(pStr: "No login required"), Checked: g_Config.m_BrFilterLogin, pRect: &Button))
712 g_Config.m_BrFilterLogin ^= 1;
713
714 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
715 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterGametypeStrict, pText: Localize(pStr: "Strict gametype filter"), Checked: g_Config.m_BrFilterGametypeStrict, pRect: &Button))
716 g_Config.m_BrFilterGametypeStrict ^= 1;
717
718 View.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &View);
719 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
720 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Game types:"), Size: FontSize, Align: TEXTALIGN_ML);
721 Button.VSplitRight(Cut: 60.0f, pLeft: nullptr, pRight: &Button);
722 static CLineInput s_GametypeInput(g_Config.m_BrFilterGametype, sizeof(g_Config.m_BrFilterGametype));
723 if(Ui()->DoEditBox(pLineInput: &s_GametypeInput, pRect: &Button, FontSize))
724 Client()->ServerBrowserUpdate();
725
726 // server address
727 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
728 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
729 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
730 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Server address:"), Size: FontSize, Align: TEXTALIGN_ML);
731 Button.VSplitRight(Cut: 60.0f, pLeft: nullptr, pRight: &Button);
732 static CLineInput s_FilterServerAddressInput(g_Config.m_BrFilterServerAddress, sizeof(g_Config.m_BrFilterServerAddress));
733 if(Ui()->DoEditBox(pLineInput: &s_FilterServerAddressInput, pRect: &Button, FontSize))
734 Client()->ServerBrowserUpdate();
735
736 // player country
737 {
738 CUIRect Flag;
739 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
740 Button.VSplitRight(Cut: 60.0f, pLeft: &Button, pRight: &Flag);
741 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterCountry, pText: Localize(pStr: "Player country:"), Checked: g_Config.m_BrFilterCountry, pRect: &Button))
742 g_Config.m_BrFilterCountry ^= 1;
743
744 const float OldWidth = Flag.w;
745 Flag.w = Flag.h * 2.0f;
746 Flag.x += (OldWidth - Flag.w) / 2.0f;
747 GameClient()->m_CountryFlags.Render(CountryCode: g_Config.m_BrFilterCountryIndex, Color: ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &g_Config.m_BrFilterCountryIndex ? 1.0f : (g_Config.m_BrFilterCountry ? 0.9f : 0.5f)), x: Flag.x, y: Flag.y, w: Flag.w, h: Flag.h);
748
749 if(Ui()->DoButtonLogic(pId: &g_Config.m_BrFilterCountryIndex, Checked: 0, pRect: &Flag, Flags: BUTTONFLAG_LEFT))
750 {
751 static SPopupMenuId s_PopupCountryId;
752 static SPopupCountrySelectionContext s_PopupCountryContext;
753 s_PopupCountryContext.m_pMenus = this;
754 s_PopupCountryContext.m_Selection = g_Config.m_BrFilterCountryIndex;
755 s_PopupCountryContext.m_New = true;
756 Ui()->DoPopupMenu(pId: &s_PopupCountryId, X: Flag.x, Y: Flag.y + Flag.h, Width: 490, Height: 210, pContext: &s_PopupCountryContext, pfnFunc: PopupCountrySelection);
757 }
758 }
759
760 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
761 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterConnectingPlayers, pText: Localize(pStr: "Filter connecting players"), Checked: g_Config.m_BrFilterConnectingPlayers, pRect: &Button))
762 g_Config.m_BrFilterConnectingPlayers ^= 1;
763
764 // map finish filters
765 if(ServerBrowser()->CommunityCache().AnyRanksAvailable())
766 {
767 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
768 if(DoButton_CheckBox(pId: &g_Config.m_BrIndicateFinished, pText: Localize(pStr: "Indicate map finish"), Checked: g_Config.m_BrIndicateFinished, pRect: &Button))
769 {
770 g_Config.m_BrIndicateFinished ^= 1;
771 if(g_Config.m_BrIndicateFinished)
772 ServerBrowser()->Refresh(Type: ServerBrowser()->GetCurrentType());
773 }
774
775 if(g_Config.m_BrIndicateFinished)
776 {
777 View.HSplitTop(Cut: RowHeight, pTop: &Button, pBottom: &View);
778 if(DoButton_CheckBox(pId: &g_Config.m_BrFilterUnfinishedMap, pText: Localize(pStr: "Unfinished map"), Checked: g_Config.m_BrFilterUnfinishedMap, pRect: &Button))
779 g_Config.m_BrFilterUnfinishedMap ^= 1;
780 }
781 else
782 {
783 g_Config.m_BrFilterUnfinishedMap = 0;
784 }
785 }
786
787 // countries and types filters
788 if(ServerBrowser()->CommunityCache().CountriesTypesFilterAvailable())
789 {
790 const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f);
791 const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f);
792
793 CUIRect TabContents, CountriesTab, TypesTab;
794 View.HSplitTop(Cut: 6.0f, pTop: nullptr, pBottom: &View);
795 View.HSplitTop(Cut: 19.0f, pTop: &Button, pBottom: &View);
796 View.HSplitTop(Cut: minimum(a: 4.0f * 22.0f + CScrollRegion::HEIGHT_MAGIC_FIX, b: View.h), pTop: &TabContents, pBottom: &View);
797 Button.VSplitMid(pLeft: &CountriesTab, pRight: &TypesTab);
798 TabContents.Draw(Color: ColorActive, Corners: IGraphics::CORNER_B, Rounding: 4.0f);
799
800 enum EFilterTab
801 {
802 FILTERTAB_COUNTRIES = 0,
803 FILTERTAB_TYPES,
804 };
805 static EFilterTab s_ActiveTab = FILTERTAB_COUNTRIES;
806
807 static CButtonContainer s_CountriesButton;
808 if(DoButton_MenuTab(pButtonContainer: &s_CountriesButton, pText: Localize(pStr: "Countries"), Checked: s_ActiveTab == FILTERTAB_COUNTRIES, pRect: &CountriesTab, Corners: IGraphics::CORNER_TL, pAnimator: nullptr, pDefaultColor: &ColorInactive, pActiveColor: &ColorActive, pHoverColor: nullptr, EdgeRounding: 4.0f))
809 {
810 s_ActiveTab = FILTERTAB_COUNTRIES;
811 }
812
813 static CButtonContainer s_TypesButton;
814 if(DoButton_MenuTab(pButtonContainer: &s_TypesButton, pText: Localize(pStr: "Types"), Checked: s_ActiveTab == FILTERTAB_TYPES, pRect: &TypesTab, Corners: IGraphics::CORNER_TR, pAnimator: nullptr, pDefaultColor: &ColorInactive, pActiveColor: &ColorActive, pHoverColor: nullptr, EdgeRounding: 4.0f))
815 {
816 s_ActiveTab = FILTERTAB_TYPES;
817 }
818
819 if(s_ActiveTab == FILTERTAB_COUNTRIES)
820 {
821 RenderServerbrowserCountriesFilter(View: TabContents);
822 }
823 else if(s_ActiveTab == FILTERTAB_TYPES)
824 {
825 RenderServerbrowserTypesFilter(View: TabContents);
826 }
827 }
828
829 static CButtonContainer s_ResetButton;
830 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
831 {
832 ResetServerbrowserFilters();
833 }
834}
835
836void CMenus::ResetServerbrowserFilters()
837{
838 g_Config.m_BrFilterString[0] = '\0';
839 g_Config.m_BrExcludeString[0] = '\0';
840 g_Config.m_BrFilterFull = 0;
841 g_Config.m_BrFilterEmpty = 0;
842 g_Config.m_BrFilterSpectators = 0;
843 g_Config.m_BrFilterFriends = 0;
844 g_Config.m_BrFilterCountry = 0;
845 g_Config.m_BrFilterCountryIndex = -1;
846 g_Config.m_BrFilterPw = 0;
847 g_Config.m_BrFilterGametype[0] = '\0';
848 g_Config.m_BrFilterGametypeStrict = 0;
849 g_Config.m_BrFilterConnectingPlayers = 1;
850 g_Config.m_BrFilterServerAddress[0] = '\0';
851 g_Config.m_BrFilterLogin = true;
852
853 if(g_Config.m_UiPage != PAGE_LAN)
854 {
855 if(ServerBrowser()->CommunityCache().AnyRanksAvailable())
856 {
857 g_Config.m_BrFilterUnfinishedMap = 0;
858 }
859 if(g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES)
860 {
861 ServerBrowser()->CommunitiesFilter().Clear();
862 }
863 ServerBrowser()->CountriesFilter().Clear();
864 ServerBrowser()->TypesFilter().Clear();
865 UpdateCommunityCache(Force: true);
866 }
867
868 Client()->ServerBrowserUpdate();
869}
870
871void CMenus::RenderServerbrowserDDNetFilter(CUIRect View,
872 IFilterList &Filter,
873 float ItemHeight, int MaxItems, int ItemsPerRow,
874 CScrollRegion &ScrollRegion, std::vector<unsigned char> &vItemIds,
875 bool UpdateCommunityCacheOnChange,
876 const std::function<const char *(int ItemIndex)> &GetItemName,
877 const std::function<void(int ItemIndex, CUIRect Item, const void *pItemId, bool Active)> &RenderItem)
878{
879 vItemIds.resize(new_size: MaxItems);
880
881 vec2 ScrollOffset(0.0f, 0.0f);
882 CScrollRegionParams ScrollParams;
883 ScrollParams.m_ScrollbarWidth = 10.0f;
884 ScrollParams.m_ScrollbarMargin = 3.0f;
885 ScrollParams.m_ScrollUnit = 2.0f * ItemHeight;
886 ScrollRegion.Begin(pClipRect: &View, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
887 View.y += ScrollOffset.y;
888
889 CUIRect Row;
890 int ColumnIndex = 0;
891 for(int ItemIndex = 0; ItemIndex < MaxItems; ++ItemIndex)
892 {
893 CUIRect Item;
894 if(ColumnIndex == 0)
895 View.HSplitTop(Cut: ItemHeight, pTop: &Row, pBottom: &View);
896 Row.VSplitLeft(Cut: View.w / ItemsPerRow, pLeft: &Item, pRight: &Row);
897 ColumnIndex = (ColumnIndex + 1) % ItemsPerRow;
898 if(!ScrollRegion.AddRect(Rect: Item))
899 continue;
900
901 const void *pItemId = &vItemIds[ItemIndex];
902 const char *pName = GetItemName(ItemIndex);
903 const bool Active = !Filter.Filtered(pElement: pName);
904
905 const int Click = Ui()->DoButtonLogic(pId: pItemId, Checked: 0, pRect: &Item, Flags: BUTTONFLAG_ALL);
906 if(Click == 1 || Click == 2)
907 {
908 // left/right click to toggle filter
909 if(Filter.Empty())
910 {
911 if(Click == 1)
912 {
913 // Left click: when all are active, only activate one and none
914 for(int j = 0; j < MaxItems; ++j)
915 {
916 if(const char *pItemName = GetItemName(j);
917 j != ItemIndex &&
918 !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
919 (&Filter == &ServerBrowser()->TypesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
920 Filter.Add(pElement: pItemName);
921 }
922 }
923 else if(Click == 2)
924 {
925 // Right click: when all are active, only deactivate one
926 if(MaxItems >= 2)
927 {
928 Filter.Add(pElement: GetItemName(ItemIndex));
929 }
930 }
931 }
932 else
933 {
934 bool AllFilteredExceptUs = true;
935 for(int j = 0; j < MaxItems; ++j)
936 {
937 if(const char *pItemName = GetItemName(j);
938 j != ItemIndex && !Filter.Filtered(pElement: pItemName) &&
939 !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) ||
940 (&Filter == &ServerBrowser()->TypesFilter() && str_comp(a: pItemName, b: IServerBrowser::COMMUNITY_TYPE_NONE) == 0)))
941 {
942 AllFilteredExceptUs = false;
943 break;
944 }
945 }
946 // When last one is removed, re-enable all currently selectable items.
947 // Don't use Clear, to avoid enabling also currently unselectable items.
948 if(AllFilteredExceptUs && Active)
949 {
950 for(int j = 0; j < MaxItems; ++j)
951 {
952 Filter.Remove(pElement: GetItemName(j));
953 }
954 }
955 else if(Active)
956 {
957 Filter.Add(pElement: pName);
958 }
959 else
960 {
961 Filter.Remove(pElement: pName);
962 }
963 }
964
965 Client()->ServerBrowserUpdate();
966 if(UpdateCommunityCacheOnChange)
967 UpdateCommunityCache(Force: true);
968 }
969 else if(Click == 3)
970 {
971 // middle click to reset (re-enable all currently selectable items)
972 for(int j = 0; j < MaxItems; ++j)
973 {
974 Filter.Remove(pElement: GetItemName(j));
975 }
976 Client()->ServerBrowserUpdate();
977 if(UpdateCommunityCacheOnChange)
978 UpdateCommunityCache(Force: true);
979 }
980
981 if(Ui()->HotItem() == pItemId && !ScrollRegion.Animating())
982 Item.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f), Corners: IGraphics::CORNER_ALL, Rounding: 2.0f);
983 RenderItem(ItemIndex, Item, pItemId, Active);
984 }
985
986 ScrollRegion.End();
987}
988
989void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View)
990{
991 CUIRect Tab;
992 View.HSplitTop(Cut: 19.0f, pTop: &Tab, pBottom: &View);
993 Tab.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_T, Rounding: 4.0f);
994 Ui()->DoLabel(pRect: &Tab, pText: Localize(pStr: "Communities"), Size: 12.0f, Align: TEXTALIGN_MC);
995 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 4.0f);
996
997 const int MaxEntries = ServerBrowser()->Communities().size();
998 const int EntriesPerRow = 1;
999
1000 static CScrollRegion s_ScrollRegion;
1001 static std::vector<unsigned char> s_vItemIds;
1002 static std::vector<unsigned char> s_vFavoriteButtonIds;
1003
1004 const float ItemHeight = 13.0f;
1005 const float Spacing = 2.0f;
1006
1007 const auto &&GetItemName = [&](int ItemIndex) {
1008 return ServerBrowser()->Communities()[ItemIndex].Id();
1009 };
1010 const auto &&GetItemDisplayName = [&](int ItemIndex) {
1011 return ServerBrowser()->Communities()[ItemIndex].Name();
1012 };
1013 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1014 const float Alpha = (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f);
1015
1016 CUIRect Icon, Label, FavoriteButton;
1017 Item.VSplitRight(Cut: Item.h, pLeft: &Item, pRight: &FavoriteButton);
1018 Item.Margin(Cut: Spacing, pOtherRect: &Item);
1019 Item.VSplitLeft(Cut: Item.h * 2.0f, pLeft: &Icon, pRight: &Label);
1020 Label.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &Label);
1021
1022 const char *pItemName = GetItemName(ItemIndex);
1023 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pItemName);
1024 if(pIcon != nullptr)
1025 {
1026 m_CommunityIcons.Render(pIcon, Rect: Icon, Active);
1027 }
1028
1029 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: Alpha);
1030 Ui()->DoLabel(pRect: &Label, pText: GetItemDisplayName(ItemIndex), Size: Label.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_ML);
1031 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1032
1033 const bool Favorite = ServerBrowser()->FavoriteCommunitiesFilter().Filtered(pElement: pItemName);
1034 if(DoButton_Favorite(pButtonId: &s_vFavoriteButtonIds[ItemIndex], pParentId: pItemId, Checked: Favorite, pRect: &FavoriteButton))
1035 {
1036 if(Favorite)
1037 {
1038 ServerBrowser()->FavoriteCommunitiesFilter().Remove(pElement: pItemName);
1039 }
1040 else
1041 {
1042 ServerBrowser()->FavoriteCommunitiesFilter().Add(pElement: pItemName);
1043 }
1044 }
1045 };
1046
1047 s_vFavoriteButtonIds.resize(new_size: MaxEntries);
1048 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->CommunitiesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: true, GetItemName, RenderItem);
1049}
1050
1051void CMenus::RenderServerbrowserCountriesFilter(CUIRect View)
1052{
1053 const int MaxEntries = ServerBrowser()->CommunityCache().SelectableCountries().size();
1054 const int EntriesPerRow = MaxEntries > 8 ? 5 : 4;
1055
1056 static CScrollRegion s_ScrollRegion;
1057 static std::vector<unsigned char> s_vItemIds;
1058
1059 const float ItemHeight = 18.0f;
1060 const float Spacing = 2.0f;
1061
1062 const auto &&GetItemName = [&](int ItemIndex) {
1063 return ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->Name();
1064 };
1065 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1066 Item.Margin(Cut: Spacing, pOtherRect: &Item);
1067 const float OldWidth = Item.w;
1068 Item.w = Item.h * 2.0f;
1069 Item.x += (OldWidth - Item.w) / 2.0f;
1070 GameClient()->m_CountryFlags.Render(CountryCode: ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->FlagId(), Color: ColorRGBA(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f)), x: Item.x, y: Item.y, w: Item.w, h: Item.h);
1071 };
1072
1073 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->CountriesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: false, GetItemName, RenderItem);
1074}
1075
1076void CMenus::RenderServerbrowserTypesFilter(CUIRect View)
1077{
1078 const int MaxEntries = ServerBrowser()->CommunityCache().SelectableTypes().size();
1079 const int EntriesPerRow = 3;
1080
1081 static CScrollRegion s_ScrollRegion;
1082 static std::vector<unsigned char> s_vItemIds;
1083
1084 const float ItemHeight = 13.0f;
1085 const float Spacing = 2.0f;
1086
1087 const auto &&GetItemName = [&](int ItemIndex) {
1088 return ServerBrowser()->CommunityCache().SelectableTypes()[ItemIndex]->Name();
1089 };
1090 const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) {
1091 Item.Margin(Cut: Spacing, pOtherRect: &Item);
1092 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f));
1093 Ui()->DoLabel(pRect: &Item, pText: GetItemName(ItemIndex), Size: Item.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1094 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1095 };
1096
1097 RenderServerbrowserDDNetFilter(View, Filter&: ServerBrowser()->TypesFilter(), ItemHeight: ItemHeight + 2.0f * Spacing, MaxItems: MaxEntries, ItemsPerRow: EntriesPerRow, ScrollRegion&: s_ScrollRegion, vItemIds&: s_vItemIds, UpdateCommunityCacheOnChange: false, GetItemName, RenderItem);
1098}
1099
1100CUi::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIRect View, bool Active)
1101{
1102 SPopupCountrySelectionContext *pPopupContext = static_cast<SPopupCountrySelectionContext *>(pContext);
1103 CMenus *pMenus = pPopupContext->m_pMenus;
1104
1105 static CListBox s_ListBox;
1106 s_ListBox.SetActive(Active);
1107 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: pMenus->GameClient()->m_CountryFlags.Num(), ItemsPerRow: 8, RowsPerScroll: 1, SelectedIndex: -1, pRect: &View, Background: false);
1108
1109 if(pPopupContext->m_New)
1110 {
1111 pPopupContext->m_New = false;
1112 s_ListBox.ScrollToSelected();
1113 }
1114
1115 for(size_t i = 0; i < pMenus->GameClient()->m_CountryFlags.Num(); ++i)
1116 {
1117 const CCountryFlags::CCountryFlag &Entry = pMenus->GameClient()->m_CountryFlags.GetByIndex(Index: i);
1118
1119 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Entry, Selected: Entry.m_CountryCode == pPopupContext->m_Selection);
1120 if(!Item.m_Visible)
1121 continue;
1122
1123 CUIRect FlagRect, Label;
1124 Item.m_Rect.Margin(Cut: 5.0f, pOtherRect: &FlagRect);
1125 FlagRect.HSplitBottom(Cut: 12.0f, pTop: &FlagRect, pBottom: &Label);
1126 Label.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Label);
1127 const float OldWidth = FlagRect.w;
1128 FlagRect.w = FlagRect.h * 2.0f;
1129 FlagRect.x += (OldWidth - FlagRect.w) / 2.0f;
1130 pMenus->GameClient()->m_CountryFlags.Render(CountryCode: Entry.m_CountryCode, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), x: FlagRect.x, y: FlagRect.y, w: FlagRect.w, h: FlagRect.h);
1131
1132 pMenus->Ui()->DoLabel(pRect: &Label, pText: Entry.m_aCountryCodeString, Size: 10.0f, Align: TEXTALIGN_MC);
1133 }
1134
1135 const int NewSelected = s_ListBox.DoEnd();
1136 pPopupContext->m_Selection = NewSelected >= 0 ? pMenus->GameClient()->m_CountryFlags.GetByIndex(Index: NewSelected).m_CountryCode : -1;
1137 if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated())
1138 {
1139 g_Config.m_BrFilterCountry = 1;
1140 g_Config.m_BrFilterCountryIndex = pPopupContext->m_Selection;
1141 pMenus->Client()->ServerBrowserUpdate();
1142 return CUi::POPUP_CLOSE_CURRENT;
1143 }
1144
1145 return CUi::POPUP_KEEP_OPEN;
1146}
1147
1148void CMenus::RenderServerbrowserInfo(CUIRect View)
1149{
1150 const CServerInfo *pSelectedServer = ServerBrowser()->SortedGet(Index: m_SelectedIndex);
1151
1152 const float RowHeight = 18.0f;
1153 const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox
1154
1155 CUIRect ServerDetails, Scoreboard;
1156 View.HSplitTop(Cut: 4.0f * 15.0f + RowHeight + 2.0f * 5.0f + 2.0f * 2.0f, pTop: &ServerDetails, pBottom: &Scoreboard);
1157
1158 if(pSelectedServer)
1159 {
1160 ServerDetails.Margin(Cut: 5.0f, pOtherRect: &ServerDetails);
1161
1162 // copy info button
1163 {
1164 CUIRect Button;
1165 ServerDetails.HSplitBottom(Cut: 15.0f, pTop: &ServerDetails, pBottom: &Button);
1166 static CButtonContainer s_CopyButton;
1167 if(DoButton_Menu(pButtonContainer: &s_CopyButton, pText: Localize(pStr: "Copy info"), Checked: 0, pRect: &Button))
1168 {
1169 char aInfo[256];
1170 str_format(
1171 buffer: aInfo,
1172 buffer_size: sizeof(aInfo),
1173 format: "%s\n"
1174 "Address: ddnet://%s\n",
1175 pSelectedServer->m_aName,
1176 pSelectedServer->m_aAddress);
1177 Input()->SetClipboardText(aInfo);
1178 }
1179 }
1180
1181 // favorite checkbox
1182 {
1183 CUIRect ButtonAddFav, ButtonLeakIp;
1184 ServerDetails.HSplitBottom(Cut: 2.0f, pTop: &ServerDetails, pBottom: nullptr);
1185 ServerDetails.HSplitBottom(Cut: RowHeight, pTop: &ServerDetails, pBottom: &ButtonAddFav);
1186 ServerDetails.HSplitBottom(Cut: 2.0f, pTop: &ServerDetails, pBottom: nullptr);
1187 ButtonAddFav.VSplitMid(pLeft: &ButtonAddFav, pRight: &ButtonLeakIp);
1188 static int s_AddFavButton = 0;
1189 if(DoButton_CheckBox_Tristate(pId: &s_AddFavButton, pText: Localize(pStr: "Favorite"), Checked: pSelectedServer->m_Favorite, pRect: &ButtonAddFav))
1190 {
1191 if(pSelectedServer->m_Favorite != TRISTATE::NONE)
1192 {
1193 Favorites()->Remove(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses);
1194 }
1195 else
1196 {
1197 Favorites()->Add(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses);
1198 if(g_Config.m_UiPage == PAGE_LAN)
1199 {
1200 Favorites()->AllowPing(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses, AllowPing: true);
1201 }
1202 }
1203 Client()->ServerBrowserUpdate();
1204 }
1205 if(pSelectedServer->m_Favorite != TRISTATE::NONE)
1206 {
1207 static int s_LeakIpButton = 0;
1208 if(DoButton_CheckBox_Tristate(pId: &s_LeakIpButton, pText: Localize(pStr: "Leak IP"), Checked: pSelectedServer->m_FavoriteAllowPing, pRect: &ButtonLeakIp))
1209 {
1210 Favorites()->AllowPing(pAddrs: pSelectedServer->m_aAddresses, NumAddrs: pSelectedServer->m_NumAddresses, AllowPing: pSelectedServer->m_FavoriteAllowPing == TRISTATE::NONE);
1211 Client()->ServerBrowserUpdate();
1212 }
1213 }
1214 }
1215
1216 CUIRect LeftColumn, RightColumn, Row;
1217 ServerDetails.VSplitLeft(Cut: 80.0f, pLeft: &LeftColumn, pRight: &RightColumn);
1218
1219 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1220 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1221
1222 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1223 Ui()->DoLabel(pRect: &Row, pText: pSelectedServer->m_aVersion, Size: FontSize, Align: TEXTALIGN_ML);
1224
1225 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1226 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Game type"), Size: FontSize, Align: TEXTALIGN_ML);
1227
1228 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1229 Ui()->DoLabel(pRect: &Row, pText: pSelectedServer->m_aGameType, Size: FontSize, Align: TEXTALIGN_ML);
1230
1231 LeftColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &LeftColumn);
1232 Ui()->DoLabel(pRect: &Row, pText: Localize(pStr: "Ping"), Size: FontSize, Align: TEXTALIGN_ML);
1233
1234 if(g_Config.m_UiColorizePing)
1235 TextRender()->TextColor(Color: GetPingTextColor(Latency: pSelectedServer->m_Latency));
1236 char aTemp[16];
1237 FormatServerbrowserPing(aBuffer&: aTemp, pInfo: pSelectedServer);
1238 RightColumn.HSplitTop(Cut: 15.0f, pTop: &Row, pBottom: &RightColumn);
1239 Ui()->DoLabel(pRect: &Row, pText: aTemp, Size: FontSize, Align: TEXTALIGN_ML);
1240 if(g_Config.m_UiColorizePing)
1241 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1242
1243 RenderServerbrowserInfoScoreboard(View: Scoreboard, pSelectedServer);
1244 }
1245 else
1246 {
1247 Ui()->DoLabel(pRect: &ServerDetails, pText: Localize(pStr: "No server selected"), Size: FontSize, Align: TEXTALIGN_MC);
1248 }
1249}
1250
1251void CMenus::RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo *pSelectedServer)
1252{
1253 const float FontSize = 10.0f;
1254
1255 static CListBox s_ListBox;
1256 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
1257 s_ListBox.DoAutoSpacing(Spacing: 2.0f);
1258 s_ListBox.SetScrollbarWidth(16.0f);
1259 s_ListBox.SetScrollbarMargin(5.0f);
1260 s_ListBox.DoStart(RowHeight: 25.0f, NumItems: pSelectedServer->m_NumReceivedClients, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false, BackgroundCorners: IGraphics::CORNER_NONE, ForceShowScrollbar: true);
1261
1262 for(int i = 0; i < pSelectedServer->m_NumReceivedClients; i++)
1263 {
1264 const CServerInfo::CClient &CurrentClient = pSelectedServer->m_aClients[i];
1265 const CListboxItem Item = s_ListBox.DoNextItem(pId: &CurrentClient);
1266 if(!Item.m_Visible)
1267 continue;
1268
1269 CUIRect Skin, Name, Clan, Score, Flag;
1270 Name = Item.m_Rect;
1271
1272 const ColorRGBA Color = PlayerBackgroundColor(Friend: CurrentClient.m_FriendState == IFriends::FRIEND_PLAYER, Clan: CurrentClient.m_FriendState == IFriends::FRIEND_CLAN, Afk: CurrentClient.m_Afk, Inside: false);
1273 Name.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
1274 Name.VSplitLeft(Cut: 1.0f, pLeft: nullptr, pRight: &Name);
1275 Name.VSplitLeft(Cut: 34.0f, pLeft: &Score, pRight: &Name);
1276 Name.VSplitLeft(Cut: 18.0f, pLeft: &Skin, pRight: &Name);
1277 Name.VSplitRight(Cut: 26.0f, pLeft: &Name, pRight: &Flag);
1278 Flag.HMargin(Cut: 6.0f, pOtherRect: &Flag);
1279 Name.HSplitTop(Cut: 12.0f, pTop: &Name, pBottom: &Clan);
1280
1281 // score
1282 char aTemp[16];
1283 if(!CurrentClient.m_Player)
1284 {
1285 str_copy(dst&: aTemp, src: "SPEC");
1286 }
1287 else if(pSelectedServer->m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_POINTS)
1288 {
1289 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "%d", CurrentClient.m_Score);
1290 }
1291 else
1292 {
1293 std::optional<int> Time = {};
1294
1295 if(pSelectedServer->m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT)
1296 {
1297 const int TempTime = absolute(a: CurrentClient.m_Score);
1298 if(TempTime != 0 && TempTime != 9999)
1299 Time = TempTime;
1300 }
1301 else
1302 {
1303 // CServerInfo::CLIENT_SCORE_KIND_POINTS
1304 if(CurrentClient.m_Score >= 0)
1305 Time = CurrentClient.m_Score;
1306 }
1307
1308 if(Time.has_value())
1309 {
1310 str_time(centisecs: (int64_t)Time.value() * 100, format: TIME_HOURS, buffer: aTemp, buffer_size: sizeof(aTemp));
1311 }
1312 else
1313 {
1314 aTemp[0] = '\0';
1315 }
1316 }
1317
1318 Ui()->DoLabel(pRect: &Score, pText: aTemp, Size: FontSize, Align: TEXTALIGN_ML);
1319
1320 // render tee if available
1321 if(CurrentClient.m_aSkin[0] != '\0')
1322 {
1323 const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(Size: vec2(Skin.w, Skin.h), pSkinName: CurrentClient.m_aSkin, CustomSkinColors: CurrentClient.m_CustomSkinColors, CustomSkinColorBody: CurrentClient.m_CustomSkinColorBody, CustomSkinColorFeet: CurrentClient.m_CustomSkinColorFeet);
1324 const CAnimState *pIdleState = CAnimState::GetIdle();
1325 vec2 OffsetToMid;
1326 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1327 const vec2 TeeRenderPos = vec2(Skin.x + TeeInfo.m_Size / 2.0f, Skin.y + Skin.h / 2.0f + OffsetToMid.y);
1328 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: CurrentClient.m_Afk ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1329 Ui()->DoButtonLogic(pId: &CurrentClient.m_aSkin, Checked: 0, pRect: &Skin, Flags: BUTTONFLAG_NONE);
1330 GameClient()->m_Tooltips.DoToolTip(pId: &CurrentClient.m_aSkin, pNearRect: &Skin, pText: CurrentClient.m_aSkin);
1331 }
1332 else if(CurrentClient.m_aaSkin7[protocol7::SKINPART_BODY][0] != '\0')
1333 {
1334 CTeeRenderInfo TeeInfo;
1335 TeeInfo.m_Size = minimum(a: Skin.w, b: Skin.h);
1336 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
1337 {
1338 GameClient()->m_Skins7.FindSkinPart(Part, pName: CurrentClient.m_aaSkin7[Part], AllowSpecialPart: true)->ApplyTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy]);
1339 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy], UseCustomColors: CurrentClient.m_aUseCustomSkinColor7[Part], Value: CurrentClient.m_aCustomSkinColor7[Part], Part);
1340 }
1341 const CAnimState *pIdleState = CAnimState::GetIdle();
1342 vec2 OffsetToMid;
1343 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1344 const vec2 TeeRenderPos = vec2(Skin.x + TeeInfo.m_Size / 2.0f, Skin.y + Skin.h / 2.0f + OffsetToMid.y);
1345 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: CurrentClient.m_Afk ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1346 }
1347
1348 // name
1349 CTextCursor NameCursor;
1350 NameCursor.SetPosition(vec2(Name.x, Name.y + (Name.h - (FontSize - 1.0f)) / 2.0f));
1351 NameCursor.m_FontSize = FontSize - 1.0f;
1352 NameCursor.m_Flags |= TEXTFLAG_STOP_AT_END;
1353 NameCursor.m_LineWidth = Name.w;
1354 const char *pName = CurrentClient.m_aName;
1355 bool Printed = false;
1356 if(g_Config.m_BrFilterString[0])
1357 Printed = PrintHighlighted(pName, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
1358 TextRender()->TextEx(pCursor: &NameCursor, pText: pName, Length: (int)(pFilteredStr - pName));
1359 TextRender()->TextColor(Color: gs_HighlightedTextColor);
1360 TextRender()->TextEx(pCursor: &NameCursor, pText: pFilteredStr, Length: FilterLen);
1361 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1362 TextRender()->TextEx(pCursor: &NameCursor, pText: pFilteredStr + FilterLen, Length: -1);
1363 });
1364 if(!Printed)
1365 TextRender()->TextEx(pCursor: &NameCursor, pText: pName, Length: -1);
1366
1367 // clan
1368 CTextCursor ClanCursor;
1369 ClanCursor.SetPosition(vec2(Clan.x, Clan.y + (Clan.h - (FontSize - 2.0f)) / 2.0f));
1370 ClanCursor.m_FontSize = FontSize - 2.0f;
1371 ClanCursor.m_Flags |= TEXTFLAG_STOP_AT_END;
1372 ClanCursor.m_LineWidth = Clan.w;
1373 const char *pClan = CurrentClient.m_aClan;
1374 Printed = false;
1375 if(g_Config.m_BrFilterString[0])
1376 Printed = PrintHighlighted(pName: pClan, PrintFn: [&](const char *pFilteredStr, const int FilterLen) {
1377 TextRender()->TextEx(pCursor: &ClanCursor, pText: pClan, Length: (int)(pFilteredStr - pClan));
1378 TextRender()->TextColor(r: 0.4f, g: 0.4f, b: 1.0f, a: 1.0f);
1379 TextRender()->TextEx(pCursor: &ClanCursor, pText: pFilteredStr, Length: FilterLen);
1380 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1381 TextRender()->TextEx(pCursor: &ClanCursor, pText: pFilteredStr + FilterLen, Length: -1);
1382 });
1383 if(!Printed)
1384 TextRender()->TextEx(pCursor: &ClanCursor, pText: pClan, Length: -1);
1385
1386 // flag
1387 GameClient()->m_CountryFlags.Render(CountryCode: CurrentClient.m_Country, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), x: Flag.x, y: Flag.y, w: Flag.w, h: Flag.h);
1388 }
1389
1390 const int NewSelected = s_ListBox.DoEnd();
1391 if(s_ListBox.WasItemSelected())
1392 {
1393 const CServerInfo::CClient &SelectedClient = pSelectedServer->m_aClients[NewSelected];
1394 if(SelectedClient.m_FriendState == IFriends::FRIEND_PLAYER)
1395 GameClient()->Friends()->RemoveFriend(pName: SelectedClient.m_aName, pClan: SelectedClient.m_aClan);
1396 else
1397 GameClient()->Friends()->AddFriend(pName: SelectedClient.m_aName, pClan: SelectedClient.m_aClan);
1398 FriendlistOnUpdate();
1399 Client()->ServerBrowserUpdate();
1400 }
1401}
1402
1403void CMenus::RenderServerbrowserFriends(CUIRect View)
1404{
1405 const float FontSize = 10.0f;
1406 static bool s_aListExtended[NUM_FRIEND_TYPES] = {true, true, false};
1407 const float SpacingH = 2.0f;
1408
1409 CUIRect List, ServerFriends;
1410 View.HSplitBottom(Cut: 70.0f, pTop: &List, pBottom: &ServerFriends);
1411 List.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &List);
1412 List.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &List);
1413
1414 // calculate friends
1415 // TODO: optimize this
1416 m_pRemoveFriend = nullptr;
1417 for(auto &vFriends : m_avFriends)
1418 vFriends.clear();
1419 m_avFriends[FRIEND_OFF].reserve(n: GameClient()->Friends()->NumFriends());
1420 for(int FriendIndex = 0; FriendIndex < GameClient()->Friends()->NumFriends(); ++FriendIndex)
1421 {
1422 m_avFriends[FRIEND_OFF].emplace_back(args: GameClient()->Friends()->GetFriend(Index: FriendIndex));
1423 }
1424 bool HasFriend = std::any_of(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: [&](const auto &Friend) {
1425 return Friend.Name()[0] != '\0';
1426 }),
1427 HasClan = std::any_of(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: [&](const auto &Friend) {
1428 return Friend.Name()[0] == '\0';
1429 });
1430
1431 for(int ServerIndex = 0; ServerIndex < ServerBrowser()->NumSortedServers(); ++ServerIndex)
1432 {
1433 const CServerInfo *pEntry = ServerBrowser()->SortedGet(Index: ServerIndex);
1434 if(pEntry->m_FriendState == IFriends::FRIEND_NO)
1435 continue;
1436
1437 for(int ClientIndex = 0; ClientIndex < pEntry->m_NumClients; ++ClientIndex)
1438 {
1439 const CServerInfo::CClient &CurrentClient = pEntry->m_aClients[ClientIndex];
1440 if(CurrentClient.m_FriendState == IFriends::FRIEND_NO)
1441 continue;
1442
1443 const int FriendIndex = CurrentClient.m_FriendState == IFriends::FRIEND_PLAYER ? FRIEND_PLAYER_ON : FRIEND_CLAN_ON;
1444 m_avFriends[FriendIndex].emplace_back(args: CurrentClient, args&: pEntry);
1445 const auto &&RemovalPredicate = [CurrentClient](const CFriendItem &Friend) {
1446 return (Friend.Name()[0] == '\0' || str_comp(a: Friend.Name(), b: CurrentClient.m_aName) == 0) && ((Friend.Name()[0] != '\0' && g_Config.m_ClFriendsIgnoreClan) || str_comp(a: Friend.Clan(), b: CurrentClient.m_aClan) == 0);
1447 };
1448 m_avFriends[FRIEND_OFF].erase(first: std::remove_if(first: m_avFriends[FRIEND_OFF].begin(), last: m_avFriends[FRIEND_OFF].end(), pred: RemovalPredicate), last: m_avFriends[FRIEND_OFF].end());
1449 }
1450 }
1451 for(auto &vFriends : m_avFriends)
1452 std::sort(first: vFriends.begin(), last: vFriends.end());
1453
1454 // friends list
1455 static CScrollRegion s_ScrollRegion;
1456 vec2 ScrollOffset(0.0f, 0.0f);
1457 CScrollRegionParams ScrollParams;
1458 ScrollParams.m_ScrollbarWidth = 16.0f;
1459 ScrollParams.m_ScrollbarMargin = 5.0f;
1460 ScrollParams.m_ScrollUnit = 80.0f;
1461 ScrollParams.m_Flags = CScrollRegionParams::FLAG_CONTENT_STATIC_WIDTH;
1462 s_ScrollRegion.Begin(pClipRect: &List, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
1463 List.y += ScrollOffset.y;
1464
1465 char aBuf[256];
1466 for(size_t FriendType = 0; FriendType < NUM_FRIEND_TYPES; ++FriendType)
1467 {
1468 // header
1469 CUIRect Header, GroupIcon, GroupLabel;
1470 List.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &List);
1471 s_ScrollRegion.AddRect(Rect: Header);
1472 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &s_aListExtended[FriendType] ? 0.4f : 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1473 Header.VSplitLeft(Cut: Header.h, pLeft: &GroupIcon, pRight: &GroupLabel);
1474 GroupIcon.Margin(Cut: 2.0f, pOtherRect: &GroupIcon);
1475 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1476 TextRender()->TextColor(Color: Ui()->HotItem() == &s_aListExtended[FriendType] ? TextRender()->DefaultTextColor() : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f));
1477 Ui()->DoLabel(pRect: &GroupIcon, pText: s_aListExtended[FriendType] ? FONT_ICON_SQUARE_MINUS : FONT_ICON_SQUARE_PLUS, Size: GroupIcon.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1478 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1479 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1480 switch(FriendType)
1481 {
1482 case FRIEND_PLAYER_ON:
1483 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Online friends (%d)"), (int)m_avFriends[FriendType].size());
1484 break;
1485 case FRIEND_CLAN_ON:
1486 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Online clanmates (%d)"), (int)m_avFriends[FriendType].size());
1487 break;
1488 case FRIEND_OFF:
1489 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Offline (%d)", pContext: "friends (server browser)"), (int)m_avFriends[FriendType].size());
1490 break;
1491 default:
1492 dbg_assert_failed("FriendType invalid");
1493 }
1494 Ui()->DoLabel(pRect: &GroupLabel, pText: aBuf, Size: FontSize, Align: TEXTALIGN_ML);
1495 if(Ui()->DoButtonLogic(pId: &s_aListExtended[FriendType], Checked: 0, pRect: &Header, Flags: BUTTONFLAG_LEFT))
1496 {
1497 s_aListExtended[FriendType] = !s_aListExtended[FriendType];
1498 }
1499
1500 // entries
1501 if(s_aListExtended[FriendType])
1502 {
1503 for(size_t FriendIndex = 0; FriendIndex < m_avFriends[FriendType].size(); ++FriendIndex)
1504 {
1505 // space
1506 {
1507 CUIRect Space;
1508 List.HSplitTop(Cut: SpacingH, pTop: &Space, pBottom: &List);
1509 s_ScrollRegion.AddRect(Rect: Space);
1510 }
1511
1512 CUIRect Rect;
1513 const auto &Friend = m_avFriends[FriendType][FriendIndex];
1514 List.HSplitTop(Cut: 11.0f + 10.0f + 2 * 2.0f + 1.0f + (Friend.ServerInfo() == nullptr ? 0.0f : 10.0f), pTop: &Rect, pBottom: &List);
1515 s_ScrollRegion.AddRect(Rect);
1516 if(s_ScrollRegion.RectClipped(Rect))
1517 continue;
1518
1519 const bool Inside = Ui()->HotItem() == Friend.ListItemId() || Ui()->HotItem() == Friend.RemoveButtonId() || Ui()->HotItem() == Friend.CommunityTooltipId() || Ui()->HotItem() == Friend.SkinTooltipId();
1520 int ButtonResult = Ui()->DoButtonLogic(pId: Friend.ListItemId(), Checked: 0, pRect: &Rect, Flags: BUTTONFLAG_LEFT);
1521
1522 if(Friend.ServerInfo())
1523 {
1524 GameClient()->m_Tooltips.DoToolTip(pId: Friend.ListItemId(), pNearRect: &Rect, pText: Localize(pStr: "Click to select server. Double click to join your friend."));
1525 }
1526 const ColorRGBA Color = PlayerBackgroundColor(Friend: FriendType == FRIEND_PLAYER_ON, Clan: FriendType == FRIEND_CLAN_ON, Afk: FriendType == FRIEND_OFF ? true : Friend.IsAfk(), Inside);
1527 Rect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1528 Rect.Margin(Cut: 2.0f, pOtherRect: &Rect);
1529
1530 CUIRect RemoveButton, NameLabel, ClanLabel, InfoLabel;
1531 Rect.HSplitTop(Cut: 16.0f, pTop: &RemoveButton, pBottom: nullptr);
1532 RemoveButton.VSplitRight(Cut: 13.0f, pLeft: nullptr, pRight: &RemoveButton);
1533 RemoveButton.HMargin(Cut: (RemoveButton.h - RemoveButton.w) / 2.0f, pOtherRect: &RemoveButton);
1534 Rect.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Rect);
1535
1536 if(Friend.ServerInfo())
1537 Rect.HSplitBottom(Cut: 10.0f, pTop: &Rect, pBottom: &InfoLabel);
1538 Rect.HSplitTop(Cut: 11.0f + 10.0f, pTop: &Rect, pBottom: nullptr);
1539
1540 // tee
1541 CUIRect Skin;
1542 Rect.VSplitLeft(Cut: Rect.h, pLeft: &Skin, pRight: &Rect);
1543 Rect.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &Rect);
1544 if(Friend.Skin()[0] != '\0')
1545 {
1546 const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(Size: vec2(Skin.w, Skin.h), pSkinName: Friend.Skin(), CustomSkinColors: Friend.CustomSkinColors(), CustomSkinColorBody: Friend.CustomSkinColorBody(), CustomSkinColorFeet: Friend.CustomSkinColorFeet());
1547 const CAnimState *pIdleState = CAnimState::GetIdle();
1548 vec2 OffsetToMid;
1549 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1550 const vec2 TeeRenderPos = vec2(Skin.x + Skin.w / 2.0f, Skin.y + Skin.h * 0.55f + OffsetToMid.y);
1551 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: Friend.IsAfk() ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1552 Ui()->DoButtonLogic(pId: Friend.SkinTooltipId(), Checked: 0, pRect: &Skin, Flags: BUTTONFLAG_NONE);
1553 GameClient()->m_Tooltips.DoToolTip(pId: Friend.SkinTooltipId(), pNearRect: &Skin, pText: Friend.Skin());
1554 }
1555 else if(Friend.Skin7(Part: protocol7::SKINPART_BODY)[0] != '\0')
1556 {
1557 CTeeRenderInfo TeeInfo;
1558 TeeInfo.m_Size = minimum(a: Skin.w, b: Skin.h);
1559 for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++)
1560 {
1561 GameClient()->m_Skins7.FindSkinPart(Part, pName: Friend.Skin7(Part), AllowSpecialPart: true)->ApplyTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy]);
1562 GameClient()->m_Skins7.ApplyColorTo(SixupRenderInfo&: TeeInfo.m_aSixup[g_Config.m_ClDummy], UseCustomColors: Friend.UseCustomSkinColor7(Part), Value: Friend.CustomSkinColor7(Part), Part);
1563 }
1564 const CAnimState *pIdleState = CAnimState::GetIdle();
1565 vec2 OffsetToMid;
1566 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: pIdleState, pInfo: &TeeInfo, TeeOffsetToMid&: OffsetToMid);
1567 const vec2 TeeRenderPos = vec2(Skin.x + Skin.w / 2.0f, Skin.y + Skin.h * 0.55f + OffsetToMid.y);
1568 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &TeeInfo, Emote: Friend.IsAfk() ? EMOTE_BLINK : EMOTE_NORMAL, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
1569 }
1570 Rect.HSplitTop(Cut: 11.0f, pTop: &NameLabel, pBottom: &ClanLabel);
1571
1572 // name
1573 Ui()->DoLabel(pRect: &NameLabel, pText: Friend.Name(), Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1574
1575 // clan
1576 Ui()->DoLabel(pRect: &ClanLabel, pText: Friend.Clan(), Size: FontSize - 2.0f, Align: TEXTALIGN_ML);
1577
1578 // server info
1579 if(Friend.ServerInfo())
1580 {
1581 // community icon
1582 const CCommunity *pCommunity = ServerBrowser()->Community(pCommunityId: Friend.ServerInfo()->m_aCommunityId);
1583 if(pCommunity != nullptr)
1584 {
1585 const CCommunityIcon *pIcon = m_CommunityIcons.Find(pCommunityId: pCommunity->Id());
1586 if(pIcon != nullptr)
1587 {
1588 CUIRect CommunityIcon;
1589 InfoLabel.VSplitLeft(Cut: 21.0f, pLeft: &CommunityIcon, pRight: &InfoLabel);
1590 InfoLabel.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &InfoLabel);
1591 m_CommunityIcons.Render(pIcon, Rect: CommunityIcon, Active: true);
1592 Ui()->DoButtonLogic(pId: Friend.CommunityTooltipId(), Checked: 0, pRect: &CommunityIcon, Flags: BUTTONFLAG_NONE);
1593 GameClient()->m_Tooltips.DoToolTip(pId: Friend.CommunityTooltipId(), pNearRect: &CommunityIcon, pText: pCommunity->Name());
1594 }
1595 }
1596
1597 // server info text
1598 char aLatency[16];
1599 FormatServerbrowserPing(aBuffer&: aLatency, pInfo: Friend.ServerInfo());
1600 if(aLatency[0] != '\0')
1601 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s | %s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType, aLatency);
1602 else
1603 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType);
1604 Ui()->DoLabel(pRect: &InfoLabel, pText: aBuf, Size: FontSize - 2.0f, Align: TEXTALIGN_ML);
1605 }
1606
1607 // remove button
1608 if(Inside)
1609 {
1610 TextRender()->TextColor(Color: Ui()->HotItem() == Friend.RemoveButtonId() ? TextRender()->DefaultTextColor() : ColorRGBA(0.4f, 0.4f, 0.4f, 1.0f));
1611 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1612 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1613 Ui()->DoLabel(pRect: &RemoveButton, pText: FONT_ICON_TRASH, Size: RemoveButton.h * CUi::ms_FontmodHeight, Align: TEXTALIGN_MC);
1614 TextRender()->SetRenderFlags(0);
1615 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1616 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1617 if(Ui()->DoButtonLogic(pId: Friend.RemoveButtonId(), Checked: 0, pRect: &RemoveButton, Flags: BUTTONFLAG_LEFT))
1618 {
1619 m_pRemoveFriend = &Friend;
1620 ButtonResult = 0;
1621 }
1622 GameClient()->m_Tooltips.DoToolTip(pId: Friend.RemoveButtonId(), pNearRect: &RemoveButton, pText: Friend.FriendState() == IFriends::FRIEND_PLAYER ? Localize(pStr: "Click to remove this player from your friends list.") : Localize(pStr: "Click to remove this clan from your friends list."));
1623 }
1624
1625 // handle click and double click on item
1626 if(ButtonResult && Friend.ServerInfo())
1627 {
1628 str_copy(dst&: g_Config.m_UiServerAddress, src: Friend.ServerInfo()->m_aAddress);
1629 m_ServerBrowserShouldRevealSelection = true;
1630 if(ButtonResult == 1 && Ui()->DoDoubleClickLogic(pId: Friend.ListItemId()))
1631 {
1632 Connect(pAddress: g_Config.m_UiServerAddress);
1633 }
1634 }
1635 }
1636
1637 // Render empty description
1638 const char *pText = nullptr;
1639 if(FriendType == FRIEND_PLAYER_ON && !HasFriend)
1640 pText = Localize(pStr: "Add friends by entering their name below or by clicking their name in the player list.");
1641 else if(FriendType == FRIEND_CLAN_ON && !HasClan)
1642 pText = Localize(pStr: "Add clanmates by entering their clan below and leaving the name blank.");
1643 if(pText != nullptr)
1644 {
1645 const float DescriptionMargin = 2.0f;
1646 const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(Size: FontSize, pText, StrLength: -1, LineWidth: List.w - 2 * DescriptionMargin);
1647 CUIRect EmptyDescription;
1648 List.HSplitTop(Cut: BoundingBox.m_H + 2 * DescriptionMargin, pTop: &EmptyDescription, pBottom: &List);
1649 s_ScrollRegion.AddRect(Rect: EmptyDescription);
1650 EmptyDescription.Margin(Cut: DescriptionMargin, pOtherRect: &EmptyDescription);
1651 SLabelProperties DescriptionProps;
1652 DescriptionProps.m_MaxWidth = EmptyDescription.w;
1653 Ui()->DoLabel(pRect: &EmptyDescription, pText, Size: FontSize, Align: TEXTALIGN_ML, LabelProps: DescriptionProps);
1654 }
1655 }
1656
1657 // space
1658 {
1659 CUIRect Space;
1660 List.HSplitTop(Cut: SpacingH, pTop: &Space, pBottom: &List);
1661 s_ScrollRegion.AddRect(Rect: Space);
1662 }
1663 }
1664 s_ScrollRegion.End();
1665
1666 if(m_pRemoveFriend != nullptr)
1667 {
1668 char aMessage[256];
1669 str_format(buffer: aMessage, buffer_size: sizeof(aMessage),
1670 format: m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? Localize(pStr: "Are you sure that you want to remove the player '%s' from your friends list?") : Localize(pStr: "Are you sure that you want to remove the clan '%s' from your friends list?"),
1671 m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? m_pRemoveFriend->Name() : m_pRemoveFriend->Clan());
1672 PopupConfirm(pTitle: Localize(pStr: "Remove friend"), pMessage: aMessage, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmRemoveFriend);
1673 }
1674
1675 // add friend
1676 if(GameClient()->Friends()->NumFriends() < IFriends::MAX_FRIENDS)
1677 {
1678 CUIRect Button;
1679 ServerFriends.Margin(Cut: 5.0f, pOtherRect: &ServerFriends);
1680
1681 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1682 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Name"));
1683 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: FontSize + 2.0f, Align: TEXTALIGN_ML);
1684 Button.VSplitLeft(Cut: 80.0f, pLeft: nullptr, pRight: &Button);
1685 static CLineInputBuffered<MAX_NAME_LENGTH> s_NameInput;
1686 Ui()->DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: FontSize + 2.0f);
1687
1688 ServerFriends.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &ServerFriends);
1689 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1690 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Clan"));
1691 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: FontSize + 2.0f, Align: TEXTALIGN_ML);
1692 Button.VSplitLeft(Cut: 80.0f, pLeft: nullptr, pRight: &Button);
1693 static CLineInputBuffered<MAX_CLAN_LENGTH> s_ClanInput;
1694 Ui()->DoEditBox(pLineInput: &s_ClanInput, pRect: &Button, FontSize: FontSize + 2.0f);
1695
1696 ServerFriends.HSplitTop(Cut: 3.0f, pTop: nullptr, pBottom: &ServerFriends);
1697 ServerFriends.HSplitTop(Cut: 18.0f, pTop: &Button, pBottom: &ServerFriends);
1698 static CButtonContainer s_AddButton;
1699 if(DoButton_Menu(pButtonContainer: &s_AddButton, pText: s_NameInput.IsEmpty() && !s_ClanInput.IsEmpty() ? Localize(pStr: "Add Clan") : Localize(pStr: "Add Friend"), Checked: 0, pRect: &Button))
1700 {
1701 GameClient()->Friends()->AddFriend(pName: s_NameInput.GetString(), pClan: s_ClanInput.GetString());
1702 s_NameInput.Clear();
1703 s_ClanInput.Clear();
1704 FriendlistOnUpdate();
1705 Client()->ServerBrowserUpdate();
1706 }
1707 }
1708}
1709
1710void CMenus::FriendlistOnUpdate()
1711{
1712 // TODO: friends are currently updated every frame; optimize and only update friends when necessary
1713}
1714
1715void CMenus::PopupConfirmRemoveFriend()
1716{
1717 GameClient()->Friends()->RemoveFriend(pName: m_pRemoveFriend->FriendState() == IFriends::FRIEND_PLAYER ? m_pRemoveFriend->Name() : "", pClan: m_pRemoveFriend->Clan());
1718 FriendlistOnUpdate();
1719 Client()->ServerBrowserUpdate();
1720 m_pRemoveFriend = nullptr;
1721}
1722
1723enum
1724{
1725 UI_TOOLBOX_PAGE_FILTERS = 0,
1726 UI_TOOLBOX_PAGE_INFO,
1727 UI_TOOLBOX_PAGE_FRIENDS,
1728 NUM_UI_TOOLBOX_PAGES,
1729};
1730
1731void CMenus::RenderServerbrowserTabBar(CUIRect TabBar)
1732{
1733 CUIRect FilterTabButton, InfoTabButton, FriendsTabButton;
1734 TabBar.VSplitLeft(Cut: TabBar.w / 3.0f, pLeft: &FilterTabButton, pRight: &TabBar);
1735 TabBar.VSplitMid(pLeft: &InfoTabButton, pRight: &FriendsTabButton);
1736
1737 const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f);
1738 const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f);
1739
1740 if(!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_TAB))
1741 {
1742 const int Direction = Input()->ShiftIsPressed() ? -1 : 1;
1743 g_Config.m_UiToolboxPage = (g_Config.m_UiToolboxPage + NUM_UI_TOOLBOX_PAGES + Direction) % NUM_UI_TOOLBOX_PAGES;
1744 }
1745
1746 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1747 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1748
1749 static CButtonContainer s_FilterTabButton;
1750 if(DoButton_MenuTab(pButtonContainer: &s_FilterTabButton, pText: FONT_ICON_LIST_UL, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FILTERS, pRect: &FilterTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FILTER], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1751 {
1752 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FILTERS;
1753 }
1754 GameClient()->m_Tooltips.DoToolTip(pId: &s_FilterTabButton, pNearRect: &FilterTabButton, pText: Localize(pStr: "Server filter"));
1755
1756 static CButtonContainer s_InfoTabButton;
1757 if(DoButton_MenuTab(pButtonContainer: &s_InfoTabButton, pText: FONT_ICON_INFO, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_INFO, pRect: &InfoTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_INFO], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1758 {
1759 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_INFO;
1760 }
1761 GameClient()->m_Tooltips.DoToolTip(pId: &s_InfoTabButton, pNearRect: &InfoTabButton, pText: Localize(pStr: "Server info"));
1762
1763 static CButtonContainer s_FriendsTabButton;
1764 if(DoButton_MenuTab(pButtonContainer: &s_FriendsTabButton, pText: FONT_ICON_HEART, Checked: g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FRIENDS, pRect: &FriendsTabButton, Corners: IGraphics::CORNER_T, pAnimator: &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FRIENDS], pDefaultColor: &ColorInactive, pActiveColor: &ColorActive))
1765 {
1766 g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FRIENDS;
1767 }
1768 GameClient()->m_Tooltips.DoToolTip(pId: &s_FriendsTabButton, pNearRect: &FriendsTabButton, pText: Localize(pStr: "Friends"));
1769
1770 TextRender()->SetRenderFlags(0);
1771 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1772}
1773
1774void CMenus::RenderServerbrowserToolBox(CUIRect ToolBox)
1775{
1776 ToolBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), Corners: IGraphics::CORNER_B, Rounding: 4.0f);
1777
1778 switch(g_Config.m_UiToolboxPage)
1779 {
1780 case UI_TOOLBOX_PAGE_FILTERS:
1781 RenderServerbrowserFilters(View: ToolBox);
1782 return;
1783 case UI_TOOLBOX_PAGE_INFO:
1784 RenderServerbrowserInfo(View: ToolBox);
1785 return;
1786 case UI_TOOLBOX_PAGE_FRIENDS:
1787 RenderServerbrowserFriends(View: ToolBox);
1788 return;
1789 default:
1790 dbg_assert_failed("ui_toolbox_page invalid");
1791 }
1792}
1793
1794void CMenus::RenderServerbrowser(CUIRect MainView)
1795{
1796 UpdateCommunityCache(Force: false);
1797
1798 switch(g_Config.m_UiPage)
1799 {
1800 case PAGE_INTERNET:
1801 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_INTERNET);
1802 break;
1803 case PAGE_LAN:
1804 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_LAN);
1805 break;
1806 case PAGE_FAVORITES:
1807 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_BROWSER_FAVORITES);
1808 break;
1809 case PAGE_FAVORITE_COMMUNITY_1:
1810 case PAGE_FAVORITE_COMMUNITY_2:
1811 case PAGE_FAVORITE_COMMUNITY_3:
1812 case PAGE_FAVORITE_COMMUNITY_4:
1813 case PAGE_FAVORITE_COMMUNITY_5:
1814 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + CMenuBackground::POS_BROWSER_CUSTOM0);
1815 break;
1816 default:
1817 dbg_assert_failed("ui_page invalid for RenderServerbrowser");
1818 }
1819
1820 // clang-format off
1821 /*
1822 +---------------------------+ +---communities---+
1823 | | | |
1824 | | +------tabs-------+
1825 | server list | | |
1826 | | | tool |
1827 | | | box |
1828 +---------------------------+ | |
1829 status box +-----------------+
1830 */
1831 // clang-format on
1832
1833 CUIRect ServerList, StatusBox, ToolBox, TabBar;
1834 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1835 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1836 MainView.VSplitRight(Cut: 205.0f, pLeft: &ServerList, pRight: &ToolBox);
1837 ServerList.VSplitRight(Cut: 5.0f, pLeft: &ServerList, pRight: nullptr);
1838
1839 if((g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) && !ServerBrowser()->Communities().empty())
1840 {
1841 CUIRect CommunityFilter;
1842 ToolBox.HSplitTop(Cut: 19.0f + 4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, pTop: &CommunityFilter, pBottom: &ToolBox);
1843 ToolBox.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ToolBox);
1844 RenderServerbrowserCommunitiesFilter(View: CommunityFilter);
1845 }
1846
1847 ToolBox.HSplitTop(Cut: 24.0f, pTop: &TabBar, pBottom: &ToolBox);
1848 ServerList.HSplitBottom(Cut: 65.0f, pTop: &ServerList, pBottom: &StatusBox);
1849
1850 bool WasListboxItemActivated;
1851 RenderServerbrowserServerList(View: ServerList, WasListboxItemActivated);
1852 RenderServerbrowserStatusBox(StatusBox, WasListboxItemActivated);
1853
1854 RenderServerbrowserTabBar(TabBar);
1855 RenderServerbrowserToolBox(ToolBox);
1856}
1857
1858template<typename F>
1859bool CMenus::PrintHighlighted(const char *pName, F &&PrintFn)
1860{
1861 const char *pStr = g_Config.m_BrFilterString;
1862 char aFilterStr[sizeof(g_Config.m_BrFilterString)];
1863 char aFilterStrTrimmed[sizeof(g_Config.m_BrFilterString)];
1864 while((pStr = str_next_token(str: pStr, delim: IServerBrowser::SEARCH_EXCLUDE_TOKEN, buffer: aFilterStr, buffer_size: sizeof(aFilterStr))))
1865 {
1866 str_copy(dst&: aFilterStrTrimmed, src: str_utf8_skip_whitespaces(str: aFilterStr));
1867 str_utf8_trim_right(param: aFilterStrTrimmed);
1868 // highlight the parts that matches
1869 const char *pFilteredStr;
1870 int FilterLen = str_length(str: aFilterStrTrimmed);
1871 if(aFilterStrTrimmed[0] == '"' && aFilterStrTrimmed[FilterLen - 1] == '"')
1872 {
1873 aFilterStrTrimmed[FilterLen - 1] = '\0';
1874 pFilteredStr = str_comp(a: pName, b: &aFilterStrTrimmed[1]) == 0 ? pName : nullptr;
1875 FilterLen -= 2;
1876 }
1877 else
1878 {
1879 const char *pFilteredStrEnd;
1880 pFilteredStr = str_utf8_find_nocase(haystack: pName, needle: aFilterStrTrimmed, end: &pFilteredStrEnd);
1881 if(pFilteredStr != nullptr && pFilteredStrEnd != nullptr)
1882 FilterLen = pFilteredStrEnd - pFilteredStr;
1883 }
1884 if(pFilteredStr)
1885 {
1886 PrintFn(pFilteredStr, FilterLen);
1887 return true;
1888 }
1889 }
1890 return false;
1891}
1892
1893CTeeRenderInfo CMenus::GetTeeRenderInfo(vec2 Size, const char *pSkinName, bool CustomSkinColors, int CustomSkinColorBody, int CustomSkinColorFeet) const
1894{
1895 CTeeRenderInfo TeeInfo;
1896 TeeInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: pSkinName));
1897 TeeInfo.ApplyColors(CustomColoredSkin: CustomSkinColors, ColorBody: CustomSkinColorBody, ColorFeet: CustomSkinColorFeet);
1898 TeeInfo.m_Size = minimum(a: Size.x, b: Size.y);
1899 return TeeInfo;
1900}
1901
1902void CMenus::ConchainFriendlistUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1903{
1904 pfnCallback(pResult, pCallbackUserData);
1905 CMenus *pThis = ((CMenus *)pUserData);
1906 if(pResult->NumArguments() >= 1 && (pThis->Client()->State() == IClient::STATE_OFFLINE || pThis->Client()->State() == IClient::STATE_ONLINE))
1907 {
1908 pThis->FriendlistOnUpdate();
1909 pThis->Client()->ServerBrowserUpdate();
1910 }
1911}
1912
1913void CMenus::ConchainFavoritesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1914{
1915 pfnCallback(pResult, pCallbackUserData);
1916 if(pResult->NumArguments() >= 1 && g_Config.m_UiPage == PAGE_FAVORITES)
1917 ((CMenus *)pUserData)->ServerBrowser()->Refresh(Type: IServerBrowser::TYPE_FAVORITES);
1918}
1919
1920void CMenus::ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1921{
1922 pfnCallback(pResult, pCallbackUserData);
1923 CMenus *pThis = static_cast<CMenus *>(pUserData);
1924 if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES || (g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5)))
1925 {
1926 pThis->UpdateCommunityCache(Force: true);
1927 pThis->Client()->ServerBrowserUpdate();
1928 }
1929}
1930
1931void CMenus::ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1932{
1933 pfnCallback(pResult, pCallbackUserData);
1934 CMenus *pThis = static_cast<CMenus *>(pUserData);
1935 if(pResult->NumArguments() >= 1)
1936 {
1937 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
1938 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= pThis->ServerBrowser()->FavoriteCommunities().size())
1939 {
1940 // Reset page to internet when there is no favorite community for this page.
1941 g_Config.m_UiPage = PAGE_INTERNET;
1942 }
1943
1944 pThis->SetMenuPage(g_Config.m_UiPage);
1945 }
1946}
1947
1948void CMenus::UpdateCommunityCache(bool Force)
1949{
1950 if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 &&
1951 (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size())
1952 {
1953 // Reset page to internet when there is no favorite community for this page,
1954 // i.e. when favorite community is removed via console while the page is open.
1955 // This also updates the community cache because the page is changed.
1956 SetMenuPage(PAGE_INTERNET);
1957 }
1958 else
1959 {
1960 ServerBrowser()->CommunityCache().Update(Force);
1961 }
1962}
1963