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