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 "countryflags.h"
4#include "menus.h"
5#include "skins.h"
6
7#include <base/dbg.h>
8#include <base/fs.h>
9#include <base/log.h>
10#include <base/math.h>
11#include <base/str.h>
12
13#include <engine/font_icons.h>
14#include <engine/graphics.h>
15#include <engine/shared/config.h>
16#include <engine/shared/localization.h>
17#include <engine/shared/protocol7.h>
18#include <engine/storage.h>
19#include <engine/textrender.h>
20#include <engine/updater.h>
21
22#include <generated/protocol.h>
23
24#include <game/client/animstate.h>
25#include <game/client/components/chat.h>
26#include <game/client/components/menu_background.h>
27#include <game/client/components/sounds.h>
28#include <game/client/gameclient.h>
29#include <game/client/skin.h>
30#include <game/client/ui.h>
31#include <game/client/ui_listbox.h>
32#include <game/client/ui_scrollregion.h>
33#include <game/localization.h>
34
35#include <array>
36#include <chrono>
37#include <memory>
38#include <numeric>
39#include <string>
40#include <vector>
41
42using namespace std::chrono_literals;
43
44void CMenus::RenderSettingsGeneral(CUIRect MainView)
45{
46 char aBuf[128 + IO_MAX_PATH_LENGTH];
47 CUIRect Label, Button, Left, Right, Game, ClientSettings;
48 MainView.HSplitTop(Cut: 150.0f, pTop: &Game, pBottom: &ClientSettings);
49
50 // game
51 {
52 // headline
53 Game.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Game);
54 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Game"), Size: 20.0f, Align: TEXTALIGN_ML);
55 Game.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Game);
56 Game.VSplitMid(pLeft: &Left, pRight: nullptr, Spacing: 20.0f);
57
58 // dynamic camera
59 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
60 const bool IsDyncam = g_Config.m_ClDyncam || g_Config.m_ClMouseFollowfactor > 0;
61 if(DoButton_CheckBox(pId: &g_Config.m_ClDyncam, pText: Localize(pStr: "Dynamic Camera"), Checked: IsDyncam, pRect: &Button))
62 {
63 if(IsDyncam)
64 {
65 g_Config.m_ClDyncam = 0;
66 g_Config.m_ClMouseFollowfactor = 0;
67 }
68 else
69 {
70 g_Config.m_ClDyncam = 1;
71 }
72 }
73
74 // smooth dynamic camera
75 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
76 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
77 if(g_Config.m_ClDyncam)
78 {
79 if(DoButton_CheckBox(pId: &g_Config.m_ClDyncamSmoothness, pText: Localize(pStr: "Smooth Dynamic Camera"), Checked: g_Config.m_ClDyncamSmoothness, pRect: &Button))
80 {
81 if(g_Config.m_ClDyncamSmoothness)
82 {
83 g_Config.m_ClDyncamSmoothness = 0;
84 }
85 else
86 {
87 g_Config.m_ClDyncamSmoothness = 50;
88 g_Config.m_ClDyncamStabilizing = 50;
89 }
90 }
91 }
92
93 // weapon pickup
94 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
95 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
96 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoswitchWeapons, pText: Localize(pStr: "Switch weapon on pickup"), Checked: g_Config.m_ClAutoswitchWeapons, pRect: &Button))
97 g_Config.m_ClAutoswitchWeapons ^= 1;
98
99 // weapon out of ammo autoswitch
100 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
101 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
102 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoswitchWeaponsOutOfAmmo, pText: Localize(pStr: "Switch weapon when out of ammo"), Checked: g_Config.m_ClAutoswitchWeaponsOutOfAmmo, pRect: &Button))
103 g_Config.m_ClAutoswitchWeaponsOutOfAmmo ^= 1;
104 }
105
106 // client
107 {
108 // headline
109 ClientSettings.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &ClientSettings);
110 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Client"), Size: 20.0f, Align: TEXTALIGN_ML);
111 ClientSettings.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ClientSettings);
112 ClientSettings.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
113
114 // skip main menu
115 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
116 if(DoButton_CheckBox(pId: &g_Config.m_ClSkipStartMenu, pText: Localize(pStr: "Skip the main menu"), Checked: g_Config.m_ClSkipStartMenu, pRect: &Button))
117 g_Config.m_ClSkipStartMenu ^= 1;
118
119 Left.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Left);
120 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
121 str_copy(dst&: aBuf, src: " ");
122 str_append(dst&: aBuf, src: Localize(pStr: "Hz", pContext: "Hertz"));
123 Ui()->DoScrollbarOption(pId: &g_Config.m_ClRefreshRate, pOption: &g_Config.m_ClRefreshRate, pRect: &Button, pStr: Localize(pStr: "Update Rate"), Min: 10, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_NOCLAMPVALUE | CUi::SCROLLBAR_OPTION_DELAYUPDATE, pSuffix: aBuf);
124 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
125 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
126 static int s_LowerRefreshRate;
127 if(DoButton_CheckBox(pId: &s_LowerRefreshRate, pText: Localize(pStr: "Save power by lowering update rate (higher input latency)"), Checked: g_Config.m_ClRefreshRate <= 480 && g_Config.m_ClRefreshRate != 0, pRect: &Button))
128 g_Config.m_ClRefreshRate = g_Config.m_ClRefreshRate > 480 || g_Config.m_ClRefreshRate == 0 ? 480 : 0;
129
130 CUIRect SettingsButton;
131 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &SettingsButton);
132 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
133 static CButtonContainer s_SettingsButtonId;
134 if(DoButton_Menu(pButtonContainer: &s_SettingsButtonId, pText: Localize(pStr: "Settings file"), Checked: 0, pRect: &SettingsButton))
135 {
136 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, CONFIG_FILE, pBuffer: aBuf, BufferSize: sizeof(aBuf));
137 Client()->ViewFile(pFilename: aBuf);
138 }
139 GameClient()->m_Tooltips.DoToolTip(pId: &s_SettingsButtonId, pNearRect: &SettingsButton, pText: Localize(pStr: "Open the settings file"));
140
141 CUIRect SavesButton;
142 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &SavesButton);
143 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
144 static CButtonContainer s_SavesButtonId;
145 if(DoButton_Menu(pButtonContainer: &s_SavesButtonId, pText: Localize(pStr: "Saves file"), Checked: 0, pRect: &SavesButton))
146 {
147 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: SAVES_FILE, pBuffer: aBuf, BufferSize: sizeof(aBuf));
148 Client()->ViewFile(pFilename: aBuf);
149 }
150 GameClient()->m_Tooltips.DoToolTip(pId: &s_SavesButtonId, pNearRect: &SavesButton, pText: Localize(pStr: "Open the saves file"));
151
152 CUIRect ConfigButton;
153 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &ConfigButton);
154 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
155 static CButtonContainer s_ConfigButtonId;
156 if(DoButton_Menu(pButtonContainer: &s_ConfigButtonId, pText: Localize(pStr: "Config directory"), Checked: 0, pRect: &ConfigButton))
157 {
158 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "", pBuffer: aBuf, BufferSize: sizeof(aBuf));
159 Client()->ViewFile(pFilename: aBuf);
160 }
161 GameClient()->m_Tooltips.DoToolTip(pId: &s_ConfigButtonId, pNearRect: &ConfigButton, pText: Localize(pStr: "Open the directory that contains the configuration and user files"));
162
163 CUIRect DirectoryButton;
164 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &DirectoryButton);
165 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
166 static CButtonContainer s_ThemesButtonId;
167 if(DoButton_Menu(pButtonContainer: &s_ThemesButtonId, pText: Localize(pStr: "Themes directory"), Checked: 0, pRect: &DirectoryButton))
168 {
169 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "themes", pBuffer: aBuf, BufferSize: sizeof(aBuf));
170 Storage()->CreateFolder(pFoldername: "themes", Type: IStorage::TYPE_SAVE);
171 Client()->ViewFile(pFilename: aBuf);
172 }
173 GameClient()->m_Tooltips.DoToolTip(pId: &s_ThemesButtonId, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom themes"));
174
175 Left.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Left);
176 RenderThemeSelection(MainView: Left);
177
178 // auto demo settings
179 {
180 Right.HSplitTop(Cut: 40.0f, pTop: nullptr, pBottom: &Right);
181 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
182 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoDemoRecord, pText: Localize(pStr: "Automatically record demos"), Checked: g_Config.m_ClAutoDemoRecord, pRect: &Button))
183 g_Config.m_ClAutoDemoRecord ^= 1;
184
185 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
186 if(g_Config.m_ClAutoDemoRecord)
187 Ui()->DoScrollbarOption(pId: &g_Config.m_ClAutoDemoMax, pOption: &g_Config.m_ClAutoDemoMax, pRect: &Button, pStr: Localize(pStr: "Max demos"), Min: 1, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE);
188
189 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
190 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
191 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoScreenshot, pText: Localize(pStr: "Automatically take game over screenshot"), Checked: g_Config.m_ClAutoScreenshot, pRect: &Button))
192 g_Config.m_ClAutoScreenshot ^= 1;
193
194 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
195 if(g_Config.m_ClAutoScreenshot)
196 Ui()->DoScrollbarOption(pId: &g_Config.m_ClAutoScreenshotMax, pOption: &g_Config.m_ClAutoScreenshotMax, pRect: &Button, pStr: Localize(pStr: "Max Screenshots"), Min: 1, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE);
197 }
198
199 // auto statboard screenshot
200 {
201 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
202 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
203 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoStatboardScreenshot, pText: Localize(pStr: "Automatically take statboard screenshot"), Checked: g_Config.m_ClAutoStatboardScreenshot, pRect: &Button))
204 {
205 g_Config.m_ClAutoStatboardScreenshot ^= 1;
206 }
207
208 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
209 if(g_Config.m_ClAutoStatboardScreenshot)
210 Ui()->DoScrollbarOption(pId: &g_Config.m_ClAutoStatboardScreenshotMax, pOption: &g_Config.m_ClAutoStatboardScreenshotMax, pRect: &Button, pStr: Localize(pStr: "Max Screenshots"), Min: 1, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE);
211 }
212
213 // auto statboard csv
214 {
215 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
216 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
217 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoCSV, pText: Localize(pStr: "Automatically create statboard csv"), Checked: g_Config.m_ClAutoCSV, pRect: &Button))
218 {
219 g_Config.m_ClAutoCSV ^= 1;
220 }
221
222 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
223 if(g_Config.m_ClAutoCSV)
224 Ui()->DoScrollbarOption(pId: &g_Config.m_ClAutoCSVMax, pOption: &g_Config.m_ClAutoCSVMax, pRect: &Button, pStr: Localize(pStr: "Max CSVs"), Min: 1, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE);
225 }
226 }
227}
228
229void CMenus::SetNeedSendInfo()
230{
231 if(m_Dummy)
232 m_NeedSendDummyinfo = true;
233 else
234 m_NeedSendinfo = true;
235}
236
237void CMenus::RenderSettingsPlayer(CUIRect MainView)
238{
239 CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo, QuickSearch;
240 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
241 TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f);
242 TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab);
243 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
244
245 static CButtonContainer s_PlayerTabButton;
246 if(DoButton_MenuTab(pButtonContainer: &s_PlayerTabButton, pText: Localize(pStr: "Player"), Checked: !m_Dummy, pRect: &PlayerTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
247 {
248 m_Dummy = false;
249 }
250
251 static CButtonContainer s_DummyTabButton;
252 if(DoButton_MenuTab(pButtonContainer: &s_DummyTabButton, pText: Localize(pStr: "Dummy"), Checked: m_Dummy, pRect: &DummyTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
253 {
254 m_Dummy = true;
255 }
256
257 if(Client()->State() == IClient::STATE_ONLINE &&
258 GameClient()->m_aNextChangeInfo[m_Dummy] > Client()->GameTick(Conn: m_Dummy))
259 {
260 char aChangeInfo[128], aTimeLeft[32];
261 str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left"), (GameClient()->m_aNextChangeInfo[m_Dummy] - Client()->GameTick(Conn: m_Dummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed());
262 str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s", Localize(pStr: "Player info change cooldown"), aTimeLeft);
263 Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML);
264 }
265
266 static CLineInput s_NameInput;
267 static CLineInput s_ClanInput;
268
269 int *pCountry;
270 if(!m_Dummy)
271 {
272 pCountry = &g_Config.m_PlayerCountry;
273 s_NameInput.SetBuffer(pStr: g_Config.m_PlayerName, MaxSize: sizeof(g_Config.m_PlayerName));
274 s_NameInput.SetEmptyText(Client()->PlayerName());
275 s_ClanInput.SetBuffer(pStr: g_Config.m_PlayerClan, MaxSize: sizeof(g_Config.m_PlayerClan));
276 }
277 else
278 {
279 pCountry = &g_Config.m_ClDummyCountry;
280 s_NameInput.SetBuffer(pStr: g_Config.m_ClDummyName, MaxSize: sizeof(g_Config.m_ClDummyName));
281 s_NameInput.SetEmptyText(Client()->DummyName());
282 s_ClanInput.SetBuffer(pStr: g_Config.m_ClDummyClan, MaxSize: sizeof(g_Config.m_ClDummyClan));
283 }
284
285 // player name
286 CUIRect Button, Label;
287 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
288 Button.VSplitLeft(Cut: 80.0f, pLeft: &Label, pRight: &Button);
289 Button.VSplitLeft(Cut: 150.0f, pLeft: &Button, pRight: nullptr);
290 char aBuf[128];
291 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Name"));
292 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
293 if(Ui()->DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 14.0f))
294 {
295 SetNeedSendInfo();
296 }
297
298 // player clan
299 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
300 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
301 Button.VSplitLeft(Cut: 80.0f, pLeft: &Label, pRight: &Button);
302 Button.VSplitLeft(Cut: 150.0f, pLeft: &Button, pRight: nullptr);
303 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Clan"));
304 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
305 if(Ui()->DoEditBox(pLineInput: &s_ClanInput, pRect: &Button, FontSize: 14.0f))
306 {
307 SetNeedSendInfo();
308 }
309
310 // country flag selector
311 static CLineInputBuffered<25> s_FlagFilterInput;
312
313 std::vector<const CCountryFlags::CCountryFlag *> vpFilteredFlags;
314 for(size_t i = 0; i < GameClient()->m_CountryFlags.Num(); ++i)
315 {
316 const CCountryFlags::CCountryFlag &Entry = GameClient()->m_CountryFlags.GetByIndex(Index: i);
317 if(!str_find_nocase(haystack: Entry.m_aCountryCodeString, needle: s_FlagFilterInput.GetString()))
318 continue;
319 vpFilteredFlags.push_back(x: &Entry);
320 }
321
322 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
323 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &QuickSearch);
324 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
325 QuickSearch.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: nullptr);
326
327 int OldSelected = -1;
328 static CListBox s_ListBox;
329 s_ListBox.DoStart(RowHeight: 48.0f, NumItems: vpFilteredFlags.size(), ItemsPerRow: 10, RowsPerScroll: 3, SelectedIndex: OldSelected, pRect: &MainView);
330
331 for(size_t i = 0; i < vpFilteredFlags.size(); i++)
332 {
333 const CCountryFlags::CCountryFlag *pEntry = vpFilteredFlags[i];
334
335 if(pEntry->m_CountryCode == *pCountry)
336 OldSelected = i;
337
338 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pEntry->m_CountryCode, Selected: OldSelected >= 0 && (size_t)OldSelected == i);
339 if(!Item.m_Visible)
340 continue;
341
342 CUIRect FlagRect;
343 Item.m_Rect.Margin(Cut: 5.0f, pOtherRect: &FlagRect);
344 FlagRect.HSplitBottom(Cut: 12.0f, pTop: &FlagRect, pBottom: &Label);
345 Label.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Label);
346 const float OldWidth = FlagRect.w;
347 FlagRect.w = FlagRect.h * 2;
348 FlagRect.x += (OldWidth - FlagRect.w) / 2.0f;
349 GameClient()->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);
350
351 if(pEntry->m_Texture.IsValid())
352 {
353 Ui()->DoLabel(pRect: &Label, pText: pEntry->m_aCountryCodeString, Size: 10.0f, Align: TEXTALIGN_MC);
354 }
355 }
356
357 const int NewSelected = s_ListBox.DoEnd();
358 if(OldSelected != NewSelected)
359 {
360 *pCountry = vpFilteredFlags[NewSelected]->m_CountryCode;
361 SetNeedSendInfo();
362 }
363
364 Ui()->DoEditBox_Search(pLineInput: &s_FlagFilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive());
365}
366
367void CMenus::RenderSettingsTee(CUIRect MainView)
368{
369 CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo;
370 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
371 TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f);
372 TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab);
373 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
374
375 static CButtonContainer s_PlayerTabButton;
376 if(DoButton_MenuTab(pButtonContainer: &s_PlayerTabButton, pText: Localize(pStr: "Player"), Checked: !m_Dummy, pRect: &PlayerTab, Corners: IGraphics::CORNER_L, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
377 {
378 m_Dummy = false;
379 m_SkinListScrollToSelected = true;
380 }
381
382 static CButtonContainer s_DummyTabButton;
383 if(DoButton_MenuTab(pButtonContainer: &s_DummyTabButton, pText: Localize(pStr: "Dummy"), Checked: m_Dummy, pRect: &DummyTab, Corners: IGraphics::CORNER_R, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
384 {
385 m_Dummy = true;
386 m_SkinListScrollToSelected = true;
387 }
388
389 if(Client()->State() == IClient::STATE_ONLINE &&
390 GameClient()->m_aNextChangeInfo[m_Dummy] > Client()->GameTick(Conn: m_Dummy))
391 {
392 char aChangeInfo[128], aTimeLeft[32];
393 str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left"), (GameClient()->m_aNextChangeInfo[m_Dummy] - Client()->GameTick(Conn: m_Dummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed());
394 str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s", Localize(pStr: "Player info change cooldown"), aTimeLeft);
395 Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML);
396 }
397
398 if(g_Config.m_Debug)
399 {
400 const CSkins::CSkinLoadingStats Stats = GameClient()->m_Skins.LoadingStats();
401 char aStats[256];
402 str_format(buffer: aStats, buffer_size: sizeof(aStats), format: "unloaded: %" PRIzu ", pending: %" PRIzu ", loading: %" PRIzu ",\nloaded: %" PRIzu ", error: %" PRIzu ", notfound: %" PRIzu,
403 Stats.m_NumUnloaded, Stats.m_NumPending, Stats.m_NumLoading, Stats.m_NumLoaded, Stats.m_NumError, Stats.m_NumNotFound);
404 Ui()->DoLabel(pRect: &ChangeInfo, pText: aStats, Size: 9.0f, Align: TEXTALIGN_MR);
405 }
406
407 char *pSkinName;
408 size_t SkinNameSize;
409 int *pUseCustomColor;
410 unsigned *pColorBody;
411 unsigned *pColorFeet;
412 int *pEmote;
413 if(!m_Dummy)
414 {
415 pSkinName = g_Config.m_ClPlayerSkin;
416 SkinNameSize = sizeof(g_Config.m_ClPlayerSkin);
417 pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor;
418 pColorBody = &g_Config.m_ClPlayerColorBody;
419 pColorFeet = &g_Config.m_ClPlayerColorFeet;
420 pEmote = &g_Config.m_ClPlayerDefaultEyes;
421 }
422 else
423 {
424 pSkinName = g_Config.m_ClDummySkin;
425 SkinNameSize = sizeof(g_Config.m_ClDummySkin);
426 pUseCustomColor = &g_Config.m_ClDummyUseCustomColor;
427 pColorBody = &g_Config.m_ClDummyColorBody;
428 pColorFeet = &g_Config.m_ClDummyColorFeet;
429 pEmote = &g_Config.m_ClDummyDefaultEyes;
430 }
431
432 const float EyeButtonSize = 40.0f;
433 const bool RenderEyesBelow = MainView.w < 750.0f;
434 CUIRect YourSkin, Checkboxes, SkinPrefix, Eyes, Button, Label;
435 MainView.HSplitTop(Cut: 90.0f, pTop: &YourSkin, pBottom: &MainView);
436 if(RenderEyesBelow)
437 {
438 YourSkin.VSplitLeft(Cut: MainView.w * 0.45f, pLeft: &YourSkin, pRight: &Checkboxes);
439 Checkboxes.VSplitLeft(Cut: MainView.w * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
440 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
441 MainView.HSplitTop(Cut: EyeButtonSize, pTop: &Eyes, pBottom: &MainView);
442 Eyes.VSplitRight(Cut: EyeButtonSize * (float)NUM_EMOTES + 5.0f * (float)(NUM_EMOTES - 1), pLeft: nullptr, pRight: &Eyes);
443 }
444 else
445 {
446 YourSkin.VSplitRight(Cut: 3 * EyeButtonSize + 2 * 5.0f, pLeft: &YourSkin, pRight: &Eyes);
447 const float RemainderWidth = YourSkin.w;
448 YourSkin.VSplitLeft(Cut: RemainderWidth * 0.4f, pLeft: &YourSkin, pRight: &Checkboxes);
449 Checkboxes.VSplitLeft(Cut: RemainderWidth * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
450 SkinPrefix.VSplitRight(Cut: 20.0f, pLeft: &SkinPrefix, pRight: nullptr);
451 }
452 YourSkin.VSplitRight(Cut: 20.0f, pLeft: &YourSkin, pRight: nullptr);
453 Checkboxes.VSplitRight(Cut: 20.0f, pLeft: &Checkboxes, pRight: nullptr);
454
455 // Checkboxes
456 bool ShouldRefresh = false;
457 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
458 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadSkins, pText: Localize(pStr: "Download skins"), Checked: g_Config.m_ClDownloadSkins, pRect: &Button))
459 {
460 g_Config.m_ClDownloadSkins ^= 1;
461 ShouldRefresh = true;
462 }
463
464 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
465 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadCommunitySkins, pText: Localize(pStr: "Download community skins"), Checked: g_Config.m_ClDownloadCommunitySkins, pRect: &Button))
466 {
467 g_Config.m_ClDownloadCommunitySkins ^= 1;
468 ShouldRefresh = true;
469 }
470
471 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
472 if(DoButton_CheckBox(pId: &g_Config.m_ClVanillaSkinsOnly, pText: Localize(pStr: "Vanilla skins only"), Checked: g_Config.m_ClVanillaSkinsOnly, pRect: &Button))
473 {
474 g_Config.m_ClVanillaSkinsOnly ^= 1;
475 ShouldRefresh = true;
476 }
477
478 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
479 if(DoButton_CheckBox(pId: &g_Config.m_ClFatSkins, pText: Localize(pStr: "Fat skins (DDFat)"), Checked: g_Config.m_ClFatSkins, pRect: &Button))
480 {
481 g_Config.m_ClFatSkins ^= 1;
482 }
483
484 // Skin prefix
485 {
486 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &SkinPrefix);
487 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Skin prefix"), Size: 14.0f, Align: TEXTALIGN_ML);
488
489 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
490 static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix));
491 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinPrefixInput, pRect: &Button, FontSize: 14.0f))
492 {
493 ShouldRefresh = true;
494 }
495
496 SkinPrefix.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &SkinPrefix);
497
498 static const char *s_apSkinPrefixes[] = {"kitty", "santa"};
499 static CButtonContainer s_aPrefixButtons[std::size(s_apSkinPrefixes)];
500 for(size_t i = 0; i < std::size(s_apSkinPrefixes); i++)
501 {
502 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
503 Button.HMargin(Cut: 2.0f, pOtherRect: &Button);
504 if(DoButton_Menu(pButtonContainer: &s_aPrefixButtons[i], pText: s_apSkinPrefixes[i], Checked: 0, pRect: &Button))
505 {
506 str_copy(dst&: g_Config.m_ClSkinPrefix, src: s_apSkinPrefixes[i]);
507 ShouldRefresh = true;
508 }
509 }
510 }
511
512 // Player skin area
513 CUIRect CustomColorsButton, RandomSkinButton;
514 YourSkin.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &YourSkin);
515 YourSkin.HSplitBottom(Cut: 20.0f, pTop: &YourSkin, pBottom: &CustomColorsButton);
516 CustomColorsButton.VSplitRight(Cut: 30.0f, pLeft: &CustomColorsButton, pRight: &RandomSkinButton);
517 CustomColorsButton.VSplitRight(Cut: 20.0f, pLeft: &CustomColorsButton, pRight: nullptr);
518 YourSkin.VSplitLeft(Cut: 65.0f, pLeft: &YourSkin, pRight: &Button);
519 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
520 Button.HMargin(Cut: (Button.h - 20.0f) / 2.0f, pOtherRect: &Button);
521
522 char aBuf[128 + IO_MAX_PATH_LENGTH];
523 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Your skin"));
524 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
525
526 CSkins::CSkinList &SkinList = GameClient()->m_Skins.SkinList();
527 const CSkin *pDefaultSkin = GameClient()->m_Skins.Find(pName: "default");
528 const CSkins::CSkinContainer *pOwnSkinContainer = GameClient()->m_Skins.FindContainerOrNullptr(pName: pSkinName[0] == '\0' ? "default" : pSkinName);
529 if(pOwnSkinContainer != nullptr && pOwnSkinContainer->IsSpecial())
530 {
531 pOwnSkinContainer = nullptr; // Special skins cannot be selected, show as missing due to invalid name
532 }
533
534 CTeeRenderInfo OwnSkinInfo;
535 OwnSkinInfo.Apply(pSkin: pOwnSkinContainer == nullptr || pOwnSkinContainer->Skin() == nullptr ? pDefaultSkin : pOwnSkinContainer->Skin().get());
536 OwnSkinInfo.ApplyColors(CustomColoredSkin: *pUseCustomColor, ColorBody: *pColorBody, ColorFeet: *pColorFeet);
537 OwnSkinInfo.m_Size = 50.0f;
538
539 // Tee
540 {
541 vec2 OffsetToMid;
542 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, TeeOffsetToMid&: OffsetToMid);
543 const vec2 TeeRenderPos = vec2(YourSkin.x + YourSkin.w / 2.0f, YourSkin.y + YourSkin.h / 2.0f + OffsetToMid.y);
544 // tee looking towards cursor, and it is happy when you touch it
545 const vec2 DeltaPosition = Ui()->MousePos() - TeeRenderPos;
546 const float Distance = length(a: DeltaPosition);
547 const float InteractionDistance = 20.0f;
548 const vec2 TeeDirection = Distance < InteractionDistance ? normalize(v: vec2(DeltaPosition.x, maximum(a: DeltaPosition.y, b: 0.5f))) : normalize(v: DeltaPosition);
549 const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : *pEmote;
550 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: TeeEmote, Dir: TeeDirection, Pos: TeeRenderPos);
551 }
552
553 // Skin loading status
554 const auto &&RenderSkinStatus = [&](CUIRect Parent, const CSkins::CSkinContainer *pSkinContainer, const void *pStatusTooltipId) {
555 if(pSkinContainer != nullptr && pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED)
556 {
557 return;
558 }
559
560 CUIRect StatusIcon;
561 Parent.HSplitTop(Cut: 20.0f, pTop: &StatusIcon, pBottom: nullptr);
562 StatusIcon.VSplitLeft(Cut: 20.0f, pLeft: &StatusIcon, pRight: nullptr);
563
564 if(pSkinContainer != nullptr &&
565 (pSkinContainer->State() == CSkins::CSkinContainer::EState::UNLOADED ||
566 pSkinContainer->State() == CSkins::CSkinContainer::EState::PENDING ||
567 pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADING))
568 {
569 Ui()->RenderProgressSpinner(Center: StatusIcon.Center(), OuterRadius: 5.0f);
570 }
571 else
572 {
573 TextRender()->TextColor(Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
574 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
575 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
576 Ui()->DoLabel(pRect: &StatusIcon, pText: pSkinContainer == nullptr || pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR ? FontIcon::TRIANGLE_EXCLAMATION : FontIcon::QUESTION, Size: 12.0f, Align: TEXTALIGN_MC);
577 TextRender()->SetRenderFlags(0);
578 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
579 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
580 Ui()->DoButtonLogic(pId: pStatusTooltipId, Checked: 0, pRect: &StatusIcon, Flags: BUTTONFLAG_NONE);
581 const char *pErrorTooltip;
582 if(pSkinContainer == nullptr)
583 {
584 pErrorTooltip = Localize(pStr: "This skin name cannot be used.");
585 }
586 else if(pSkinContainer->State() == CSkins::CSkinContainer::EState::ERROR)
587 {
588 pErrorTooltip = Localize(pStr: "Skin could not be loaded due to an error. Check the local console for details.");
589 }
590 else
591 {
592 pErrorTooltip = Localize(pStr: "Skin could not be found.");
593 }
594 GameClient()->m_Tooltips.DoToolTip(pId: pStatusTooltipId, pNearRect: &StatusIcon, pText: pErrorTooltip);
595 }
596 };
597 static char s_StatusTooltipId;
598 RenderSkinStatus(YourSkin, pOwnSkinContainer, &s_StatusTooltipId);
599
600 // Skin name
601 static CLineInput s_SkinInput;
602 s_SkinInput.SetBuffer(pStr: pSkinName, MaxSize: SkinNameSize);
603 s_SkinInput.SetEmptyText("default");
604 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinInput, pRect: &Button, FontSize: 14.0f))
605 {
606 SetNeedSendInfo();
607 m_SkinListScrollToSelected = true;
608 SkinList.ForceRefresh();
609 }
610
611 // Random skin button
612 static CButtonContainer s_RandomSkinButton;
613 static const char *s_apDice[] = {FontIcon::DICE_ONE, FontIcon::DICE_TWO, FontIcon::DICE_THREE, FontIcon::DICE_FOUR, FontIcon::DICE_FIVE, FontIcon::DICE_SIX};
614 static int s_CurrentDie = rand() % std::size(s_apDice);
615 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
616 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
617 if(DoButton_Menu(pButtonContainer: &s_RandomSkinButton, pText: s_apDice[s_CurrentDie], Checked: 0, pRect: &RandomSkinButton, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: -0.2f))
618 {
619 GameClient()->m_Skins.RandomizeSkin(Dummy: m_Dummy);
620 SetNeedSendInfo();
621 m_SkinListScrollToSelected = true;
622 s_CurrentDie = rand() % std::size(s_apDice);
623 }
624 TextRender()->SetRenderFlags(0);
625 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
626 GameClient()->m_Tooltips.DoToolTip(pId: &s_RandomSkinButton, pNearRect: &RandomSkinButton, pText: Localize(pStr: "Create a random skin"));
627
628 // Custom colors button
629 if(DoButton_CheckBox(pId: pUseCustomColor, pText: Localize(pStr: "Custom colors"), Checked: *pUseCustomColor, pRect: &CustomColorsButton))
630 {
631 *pUseCustomColor = *pUseCustomColor ? 0 : 1;
632 SetNeedSendInfo();
633 }
634
635 // Default eyes
636 {
637 CTeeRenderInfo EyeSkinInfo = OwnSkinInfo;
638 EyeSkinInfo.m_Size = EyeButtonSize;
639 vec2 OffsetToMid;
640 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, TeeOffsetToMid&: OffsetToMid);
641
642 CUIRect EyesRow;
643 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
644 static CButtonContainer s_aEyeButtons[NUM_EMOTES];
645 for(int CurrentEyeEmote = 0; CurrentEyeEmote < NUM_EMOTES; CurrentEyeEmote++)
646 {
647 EyesRow.VSplitLeft(Cut: EyeButtonSize, pLeft: &Button, pRight: &EyesRow);
648 EyesRow.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &EyesRow);
649 if(!RenderEyesBelow && (CurrentEyeEmote + 1) % 3 == 0)
650 {
651 Eyes.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Eyes);
652 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
653 }
654
655 const ColorRGBA EyeButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + (*pEmote == CurrentEyeEmote ? 0.25f : 0.0f));
656 if(DoButton_Menu(pButtonContainer: &s_aEyeButtons[CurrentEyeEmote], pText: "", Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: EyeButtonColor))
657 {
658 *pEmote = CurrentEyeEmote;
659 if((int)m_Dummy == g_Config.m_ClDummy)
660 GameClient()->m_Emoticon.EyeEmote(EyeEmote: CurrentEyeEmote);
661 }
662 GameClient()->m_Tooltips.DoToolTip(pId: &s_aEyeButtons[CurrentEyeEmote], pNearRect: &Button, pText: Localize(pStr: "Choose default eyes when joining a server"));
663 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, Emote: CurrentEyeEmote, Dir: vec2(1.0f, 0.0f), Pos: vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2.0f + OffsetToMid.y));
664 }
665 }
666
667 // Custom color pickers
668 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
669 if(*pUseCustomColor)
670 {
671 CUIRect CustomColors;
672 MainView.HSplitTop(Cut: 95.0f, pTop: &CustomColors, pBottom: &MainView);
673 CUIRect aRects[2];
674 CustomColors.VSplitMid(pLeft: &aRects[0], pRight: &aRects[1], Spacing: 20.0f);
675
676 unsigned *apColors[] = {pColorBody, pColorFeet};
677 const char *apParts[] = {Localize(pStr: "Body"), Localize(pStr: "Feet")};
678
679 for(int i = 0; i < 2; i++)
680 {
681 aRects[i].HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &aRects[i]);
682 Ui()->DoLabel(pRect: &Label, pText: apParts[i], Size: 14.0f, Align: TEXTALIGN_ML);
683 if(RenderHslaScrollbars(pRect: &aRects[i], pColor: apColors[i], Alpha: false, DarkestLight: ColorHSLA::DARKEST_LGT))
684 {
685 SetNeedSendInfo();
686 }
687 }
688 }
689 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
690
691 // Layout bottom controls and use remainder for skin selector
692 CUIRect QuickSearch, DatabaseButton, DirectoryButton, RefreshButton;
693 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &QuickSearch);
694 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
695 QuickSearch.VSplitLeft(Cut: 220.0f, pLeft: &QuickSearch, pRight: &DatabaseButton);
696 DatabaseButton.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &DatabaseButton);
697 DatabaseButton.VSplitLeft(Cut: 150.0f, pLeft: &DatabaseButton, pRight: &DirectoryButton);
698 DirectoryButton.VSplitRight(Cut: 175.0f, pLeft: nullptr, pRight: &DirectoryButton);
699 DirectoryButton.VSplitRight(Cut: 25.0f, pLeft: &DirectoryButton, pRight: &RefreshButton);
700 DirectoryButton.VSplitRight(Cut: 10.0f, pLeft: &DirectoryButton, pRight: nullptr);
701
702 // Skin selector
703 static CListBox s_ListBox;
704 std::vector<CSkins::CSkinListEntry> &vSkinList = SkinList.Skins();
705 int OldSelected = -1;
706 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: vSkinList.size(), ItemsPerRow: 4, RowsPerScroll: 2, SelectedIndex: OldSelected, pRect: &MainView);
707 for(size_t i = 0; i < vSkinList.size(); ++i)
708 {
709 CSkins::CSkinListEntry &SkinListEntry = vSkinList[i];
710 const CSkins::CSkinContainer *pSkinContainer = vSkinList[i].SkinContainer();
711
712 if(!m_Dummy ? SkinListEntry.IsSelectedMain() : SkinListEntry.IsSelectedDummy())
713 {
714 OldSelected = i;
715 if(m_SkinListScrollToSelected)
716 {
717 s_ListBox.ScrollToSelected();
718 m_SkinListScrollToSelected = false;
719 }
720 }
721
722 const CListboxItem Item = s_ListBox.DoNextItem(pId: SkinListEntry.ListItemId(), Selected: OldSelected >= 0 && (size_t)OldSelected == i);
723 if(!Item.m_Visible)
724 {
725 continue;
726 }
727
728 SkinListEntry.RequestLoad();
729 const CSkin *pSkin = pSkinContainer->State() == CSkins::CSkinContainer::EState::LOADED ? pSkinContainer->Skin().get() : pDefaultSkin;
730
731 Item.m_Rect.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &Label);
732
733 {
734 CTeeRenderInfo Info = OwnSkinInfo;
735 Info.Apply(pSkin);
736 vec2 OffsetToMid;
737 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid);
738 const vec2 TeeRenderPos = vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2 + OffsetToMid.y);
739 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: *pEmote, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
740 }
741
742 {
743 SLabelProperties Props;
744 Props.m_MaxWidth = Label.w - 5.0f;
745 const auto &NameMatch = SkinListEntry.NameMatch();
746 if(NameMatch.has_value())
747 {
748 const auto [MatchStart, MatchLength] = NameMatch.value();
749 Props.m_vColorSplits.emplace_back(args: MatchStart, args: MatchLength, args: ColorRGBA(0.4f, 0.4f, 1.0f, 1.0f));
750 }
751 Ui()->DoLabel(pRect: &Label, pText: pSkinContainer->Name(), Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
752 }
753
754 if(g_Config.m_Debug)
755 {
756 Graphics()->TextureClear();
757 Graphics()->QuadsBegin();
758 Graphics()->SetColor(*pUseCustomColor ? color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorBody).UnclampLighting(Darkest: ColorHSLA::DARKEST_LGT)) : pSkin->m_BloodColor);
759 IGraphics::CQuadItem QuadItem(Label.x, Label.y, 12.0f, 12.0f);
760 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
761 Graphics()->QuadsEnd();
762 }
763
764 // render skin favorite icon
765 {
766 CUIRect FavIcon;
767 Item.m_Rect.HSplitTop(Cut: 20.0f, pTop: &FavIcon, pBottom: nullptr);
768 FavIcon.VSplitRight(Cut: 20.0f, pLeft: nullptr, pRight: &FavIcon);
769 if(DoButton_Favorite(pButtonId: SkinListEntry.FavoriteButtonId(), pParentId: SkinListEntry.ListItemId(), Checked: SkinListEntry.IsFavorite(), pRect: &FavIcon))
770 {
771 if(SkinListEntry.IsFavorite())
772 {
773 GameClient()->m_Skins.RemoveFavorite(pName: pSkinContainer->Name());
774 }
775 else
776 {
777 GameClient()->m_Skins.AddFavorite(pName: pSkinContainer->Name());
778 }
779 }
780 }
781
782 RenderSkinStatus(Item.m_Rect, pSkinContainer, SkinListEntry.ErrorTooltipId());
783 }
784
785 const int NewSelected = s_ListBox.DoEnd();
786 if(OldSelected != NewSelected)
787 {
788 str_copy(dst: pSkinName, src: vSkinList[NewSelected].SkinContainer()->Name(), dst_size: SkinNameSize);
789 SkinList.ForceRefresh();
790 SetNeedSendInfo();
791 }
792
793 static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString));
794 if(SkinList.UnfilteredCount() > 0 && vSkinList.empty())
795 {
796 CUIRect FilterLabel, ResetButton;
797 MainView.HMargin(Cut: (MainView.h - (16.0f + 18.0f + 8.0f)) / 2.0f, pOtherRect: &FilterLabel);
798 FilterLabel.HSplitTop(Cut: 16.0f, pTop: &FilterLabel, pBottom: &ResetButton);
799 ResetButton.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ResetButton);
800 ResetButton.VMargin(Cut: (ResetButton.w - 200.0f) / 2.0f, pOtherRect: &ResetButton);
801 Ui()->DoLabel(pRect: &FilterLabel, pText: Localize(pStr: "No skins match your filter criteria"), Size: 16.0f, Align: TEXTALIGN_MC);
802 static CButtonContainer s_ResetButton;
803 if(DoButton_Menu(pButtonContainer: &s_ResetButton, pText: Localize(pStr: "Reset filter"), Checked: 0, pRect: &ResetButton))
804 {
805 s_SkinFilterInput.Clear();
806 SkinList.ForceRefresh();
807 }
808 }
809
810 if(Ui()->DoEditBox_Search(pLineInput: &s_SkinFilterInput, pRect: &QuickSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
811 {
812 SkinList.ForceRefresh();
813 }
814
815 static CButtonContainer s_SkinDatabaseButton;
816 if(DoButton_Menu(pButtonContainer: &s_SkinDatabaseButton, pText: Localize(pStr: "Skin Database"), Checked: 0, pRect: &DatabaseButton))
817 {
818 Client()->ViewLink(pLink: "https://ddnet.org/skins/");
819 }
820
821 static CButtonContainer s_DirectoryButton;
822 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Skins directory"), Checked: 0, pRect: &DirectoryButton))
823 {
824 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "skins", pBuffer: aBuf, BufferSize: sizeof(aBuf));
825 Storage()->CreateFolder(pFoldername: "skins", Type: IStorage::TYPE_SAVE);
826 Client()->ViewFile(pFilename: aBuf);
827 }
828 GameClient()->m_Tooltips.DoToolTip(pId: &s_DirectoryButton, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom skins"));
829
830 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
831 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
832 static CButtonContainer s_SkinRefreshButton;
833 if(DoButton_Menu(pButtonContainer: &s_SkinRefreshButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
834 {
835 ShouldRefresh = true;
836 }
837 TextRender()->SetRenderFlags(0);
838 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
839
840 if(ShouldRefresh)
841 {
842 GameClient()->RefreshSkins(SkinDescriptorFlags: CSkinDescriptor::FLAG_SIX);
843 }
844}
845
846void CMenus::RenderSettingsGraphics(CUIRect MainView)
847{
848 CUIRect Button;
849 char aBuf[128];
850 bool CheckSettings = false;
851
852 static const int MAX_RESOLUTIONS = 256;
853 static CVideoMode s_aModes[MAX_RESOLUTIONS];
854 static int s_NumNodes = Graphics()->GetVideoModes(pModes: s_aModes, MaxModes: MAX_RESOLUTIONS, Screen: g_Config.m_GfxScreen);
855 static int s_GfxFsaaSamples = g_Config.m_GfxFsaaSamples;
856 static bool s_GfxBackendChanged = false;
857 static bool s_GfxGpuChanged = false;
858
859 static int s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes;
860
861 static bool s_WasInit = false;
862 static bool s_ModesReload = false;
863 if(!s_WasInit)
864 {
865 s_WasInit = true;
866
867 Graphics()->AddWindowPropChangeListener(pFunc: []() {
868 s_ModesReload = true;
869 });
870 }
871
872 if(s_ModesReload || g_Config.m_GfxDisplayAllVideoModes != s_InitDisplayAllVideoModes)
873 {
874 s_NumNodes = Graphics()->GetVideoModes(pModes: s_aModes, MaxModes: MAX_RESOLUTIONS, Screen: g_Config.m_GfxScreen);
875 s_ModesReload = false;
876 s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes;
877 }
878
879 CUIRect ModeList, ModeLabel;
880 MainView.VSplitLeft(Cut: 350.0f, pLeft: &MainView, pRight: &ModeList);
881 ModeList.HSplitTop(Cut: 24.0f, pTop: &ModeLabel, pBottom: &ModeList);
882 MainView.VSplitLeft(Cut: 340.0f, pLeft: &MainView, pRight: nullptr);
883
884 // display mode list
885 static CListBox s_ListBox;
886 static const float sc_RowHeightResList = 22.0f;
887 static const float sc_FontSizeResListHeader = 12.0f;
888 static const float sc_FontSizeResList = 10.0f;
889
890 {
891 int G = std::gcd(m: g_Config.m_GfxScreenWidth, n: g_Config.m_GfxScreenHeight);
892 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %dx%d @%dhz (%d:%d)",
893 Localize(pStr: "Current"),
894 (int)(g_Config.m_GfxScreenWidth * Graphics()->ScreenHiDPIScale()),
895 (int)(g_Config.m_GfxScreenHeight * Graphics()->ScreenHiDPIScale()),
896 g_Config.m_GfxScreenRefreshRate,
897 g_Config.m_GfxScreenWidth / G,
898 g_Config.m_GfxScreenHeight / G);
899 Ui()->DoLabel(pRect: &ModeLabel, pText: aBuf, Size: sc_FontSizeResListHeader, Align: TEXTALIGN_MC);
900 }
901
902 int OldSelected = -1;
903 s_ListBox.SetActive(!Ui()->IsPopupOpen());
904 s_ListBox.DoStart(RowHeight: sc_RowHeightResList, NumItems: s_NumNodes, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: OldSelected, pRect: &ModeList);
905
906 for(int i = 0; i < s_NumNodes; ++i)
907 {
908 if(g_Config.m_GfxScreenWidth == s_aModes[i].m_WindowWidth &&
909 g_Config.m_GfxScreenHeight == s_aModes[i].m_WindowHeight &&
910 g_Config.m_GfxScreenRefreshRate == s_aModes[i].m_RefreshRate)
911 {
912 OldSelected = i;
913 }
914
915 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_aModes[i], Selected: OldSelected == i);
916 if(!Item.m_Visible)
917 continue;
918
919 int G = std::gcd(m: s_aModes[i].m_WindowWidth, n: s_aModes[i].m_WindowHeight);
920 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " %dx%d @%dhz (%d:%d)", s_aModes[i].m_CanvasWidth, s_aModes[i].m_CanvasHeight, s_aModes[i].m_RefreshRate, s_aModes[i].m_WindowWidth / G, s_aModes[i].m_WindowHeight / G);
921 Ui()->DoLabel(pRect: &Item.m_Rect, pText: aBuf, Size: sc_FontSizeResList, Align: TEXTALIGN_ML);
922 }
923
924 const int NewSelected = s_ListBox.DoEnd();
925 if(OldSelected != NewSelected)
926 {
927 g_Config.m_GfxScreenWidth = s_aModes[NewSelected].m_WindowWidth;
928 g_Config.m_GfxScreenHeight = s_aModes[NewSelected].m_WindowHeight;
929 g_Config.m_GfxScreenRefreshRate = s_aModes[NewSelected].m_RefreshRate;
930 Graphics()->ResizeToScreen();
931 }
932
933 // switches
934 CUIRect WindowModeDropDown;
935 MainView.HSplitTop(Cut: 20.0f, pTop: &WindowModeDropDown, pBottom: &MainView);
936
937 const char *apWindowModes[] = {Localize(pStr: "Windowed"), Localize(pStr: "Windowed borderless"), Localize(pStr: "Windowed fullscreen"), Localize(pStr: "Desktop fullscreen"), Localize(pStr: "Fullscreen")};
938 static const int s_NumWindowMode = std::size(apWindowModes);
939
940 const int OldWindowMode = (g_Config.m_GfxFullscreen ? (g_Config.m_GfxFullscreen == 1 ? 4 : (g_Config.m_GfxFullscreen == 2 ? 3 : 2)) : (g_Config.m_GfxBorderless ? 1 : 0));
941
942 static CUi::SDropDownState s_WindowModeDropDownState;
943 static CScrollRegion s_WindowModeDropDownScrollRegion;
944 s_WindowModeDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_WindowModeDropDownScrollRegion;
945 const int NewWindowMode = Ui()->DoDropDown(pRect: &WindowModeDropDown, CurSelection: OldWindowMode, pStrs: apWindowModes, Num: s_NumWindowMode, State&: s_WindowModeDropDownState);
946 if(OldWindowMode != NewWindowMode)
947 {
948 if(NewWindowMode == 0)
949 Graphics()->SetWindowParams(FullscreenMode: 0, IsBorderless: false);
950 else if(NewWindowMode == 1)
951 Graphics()->SetWindowParams(FullscreenMode: 0, IsBorderless: true);
952 else if(NewWindowMode == 2)
953 Graphics()->SetWindowParams(FullscreenMode: 3, IsBorderless: false);
954 else if(NewWindowMode == 3)
955 Graphics()->SetWindowParams(FullscreenMode: 2, IsBorderless: false);
956 else if(NewWindowMode == 4)
957 Graphics()->SetWindowParams(FullscreenMode: 1, IsBorderless: false);
958 }
959
960 if(Graphics()->GetNumScreens() > 1)
961 {
962 CUIRect ScreenDropDown;
963 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
964 MainView.HSplitTop(Cut: 20.0f, pTop: &ScreenDropDown, pBottom: &MainView);
965
966 const int NumScreens = Graphics()->GetNumScreens();
967 static std::vector<std::string> s_vScreenNames;
968 static std::vector<const char *> s_vpScreenNames;
969 s_vScreenNames.resize(sz: NumScreens);
970 s_vpScreenNames.resize(sz: NumScreens);
971
972 for(int i = 0; i < NumScreens; ++i)
973 {
974 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d: %s", Localize(pStr: "Screen"), i, Graphics()->GetScreenName(Screen: i));
975 s_vScreenNames[i] = aBuf;
976 s_vpScreenNames[i] = s_vScreenNames[i].c_str();
977 }
978
979 static CUi::SDropDownState s_ScreenDropDownState;
980 static CScrollRegion s_ScreenDropDownScrollRegion;
981 s_ScreenDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ScreenDropDownScrollRegion;
982 const int NewScreen = Ui()->DoDropDown(pRect: &ScreenDropDown, CurSelection: g_Config.m_GfxScreen, pStrs: s_vpScreenNames.data(), Num: s_vpScreenNames.size(), State&: s_ScreenDropDownState);
983 if(NewScreen != g_Config.m_GfxScreen)
984 Graphics()->SwitchWindowScreen(Index: NewScreen, MoveToCenter: true);
985 }
986
987 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
988 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
989 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "V-Sync"), Localize(pStr: "may cause delay"));
990 if(DoButton_CheckBox(pId: &g_Config.m_GfxVsync, pText: aBuf, Checked: g_Config.m_GfxVsync, pRect: &Button))
991 {
992 Graphics()->SetVSync(!g_Config.m_GfxVsync);
993 }
994
995 bool MultiSamplingChanged = false;
996 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
997 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "FSAA samples"), Localize(pStr: "may cause delay"));
998 int GfxFsaaSamplesMouseButton = DoButton_CheckBox_Number(pId: &g_Config.m_GfxFsaaSamples, pText: aBuf, Checked: g_Config.m_GfxFsaaSamples, pRect: &Button);
999 int CurFSAA = g_Config.m_GfxFsaaSamples == 0 ? 1 : g_Config.m_GfxFsaaSamples;
1000 if(GfxFsaaSamplesMouseButton == 1) // inc
1001 {
1002 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) + 1);
1003 if(g_Config.m_GfxFsaaSamples > 64)
1004 g_Config.m_GfxFsaaSamples = 0;
1005 MultiSamplingChanged = true;
1006 }
1007 else if(GfxFsaaSamplesMouseButton == 2) // dec
1008 {
1009 if(CurFSAA == 1)
1010 g_Config.m_GfxFsaaSamples = 64;
1011 else if(CurFSAA == 2)
1012 g_Config.m_GfxFsaaSamples = 0;
1013 else
1014 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) - 1);
1015 MultiSamplingChanged = true;
1016 }
1017
1018 uint32_t MultiSamplingCountBackend = 0;
1019 if(MultiSamplingChanged)
1020 {
1021 if(Graphics()->SetMultiSampling(ReqMultiSamplingCount: g_Config.m_GfxFsaaSamples, MultiSamplingCountBackend))
1022 {
1023 // try again with 0 if mouse click was increasing multi sampling
1024 // else just accept the current value as is
1025 if((uint32_t)g_Config.m_GfxFsaaSamples > MultiSamplingCountBackend && GfxFsaaSamplesMouseButton == 1)
1026 Graphics()->SetMultiSampling(ReqMultiSamplingCount: 0, MultiSamplingCountBackend);
1027 g_Config.m_GfxFsaaSamples = (int)MultiSamplingCountBackend;
1028 }
1029 else
1030 {
1031 CheckSettings = true;
1032 }
1033 }
1034
1035 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1036 if(DoButton_CheckBox(pId: &g_Config.m_GfxHighDetail, pText: Localize(pStr: "High Detail"), Checked: g_Config.m_GfxHighDetail, pRect: &Button))
1037 g_Config.m_GfxHighDetail ^= 1;
1038 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_GfxHighDetail, pNearRect: &Button, pText: Localize(pStr: "Allows maps to render with more detail"));
1039
1040 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1041 if(DoButton_CheckBox(pId: &g_Config.m_ClShowfps, pText: Localize(pStr: "Show FPS"), Checked: g_Config.m_ClShowfps, pRect: &Button))
1042 g_Config.m_ClShowfps ^= 1;
1043 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowfps, pNearRect: &Button, pText: Localize(pStr: "Renders your frame rate in the top right"));
1044
1045 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1046 str_copy(dst&: aBuf, src: " ");
1047 str_append(dst&: aBuf, src: Localize(pStr: "Hz", pContext: "Hertz"));
1048 Ui()->DoScrollbarOption(pId: &g_Config.m_GfxRefreshRate, pOption: &g_Config.m_GfxRefreshRate, pRect: &Button, pStr: Localize(pStr: "Refresh Rate"), Min: 10, Max: 1000, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_NOCLAMPVALUE | CUi::SCROLLBAR_OPTION_DELAYUPDATE, pSuffix: aBuf);
1049
1050 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1051 static CButtonContainer s_UiColorResetId;
1052 DoLine_ColorPicker(pResetId: &s_UiColorResetId, LineSize: 25.0f, LabelSize: 13.0f, BottomMargin: 2.0f, pMainRect: &MainView, pText: Localize(pStr: "UI Color"), pColorValue: &g_Config.m_UiColor, DefaultColor: color_cast<ColorRGBA>(hsl: ColorHSLA(0xE4A046AFU, true)), CheckBoxSpacing: false, pCheckBoxValue: nullptr, Alpha: true);
1053
1054 // Backend list
1055 struct SMenuBackendInfo
1056 {
1057 int m_Major = 0;
1058 int m_Minor = 0;
1059 int m_Patch = 0;
1060 const char *m_pBackendName = "";
1061 bool m_Found = false;
1062 };
1063 std::array<std::array<SMenuBackendInfo, EGraphicsDriverAgeType::GRAPHICS_DRIVER_AGE_TYPE_COUNT>, EBackendType::BACKEND_TYPE_COUNT> aaSupportedBackends{};
1064 uint32_t FoundBackendCount = 0;
1065 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1066 {
1067 if(EBackendType(i) == BACKEND_TYPE_AUTO)
1068 continue;
1069 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1070 {
1071 auto &Info = aaSupportedBackends[i][n];
1072 if(Graphics()->GetDriverVersion(DriverAgeType: EGraphicsDriverAgeType(n), Major&: Info.m_Major, Minor&: Info.m_Minor, Patch&: Info.m_Patch, pName&: Info.m_pBackendName, BackendType: EBackendType(i)))
1073 {
1074 // don't count blocked opengl drivers
1075 if(EBackendType(i) != BACKEND_TYPE_OPENGL || EGraphicsDriverAgeType(n) == GRAPHICS_DRIVER_AGE_TYPE_LEGACY || g_Config.m_GfxDriverIsBlocked == 0)
1076 {
1077 Info.m_Found = true;
1078 ++FoundBackendCount;
1079 }
1080 }
1081 }
1082 }
1083
1084 if(FoundBackendCount > 1)
1085 {
1086 CUIRect Text, BackendDropDown;
1087 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1088 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1089 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1090 MainView.HSplitTop(Cut: 20.0f, pTop: &BackendDropDown, pBottom: &MainView);
1091 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Renderer"), Size: 16.0f, Align: TEXTALIGN_MC);
1092
1093 static std::vector<std::string> s_vBackendIdNames;
1094 static std::vector<const char *> s_vpBackendIdNamesCStr;
1095 static std::vector<SMenuBackendInfo> s_vBackendInfos;
1096
1097 size_t BackendCount = FoundBackendCount + 1;
1098 s_vBackendIdNames.resize(sz: BackendCount);
1099 s_vpBackendIdNamesCStr.resize(sz: BackendCount);
1100 s_vBackendInfos.resize(sz: BackendCount);
1101
1102 char aTmpBackendName[256];
1103
1104 auto IsInfoDefault = [](const SMenuBackendInfo &CheckInfo) {
1105 return str_comp_nocase(a: CheckInfo.m_pBackendName, b: DefaultConfig::GfxBackend) == 0 && CheckInfo.m_Major == DefaultConfig::GfxGLMajor && CheckInfo.m_Minor == DefaultConfig::GfxGLMinor && CheckInfo.m_Patch == DefaultConfig::GfxGLPatch;
1106 };
1107
1108 int OldSelectedBackend = -1;
1109 uint32_t CurCounter = 0;
1110 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1111 {
1112 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1113 {
1114 auto &Info = aaSupportedBackends[i][n];
1115 if(Info.m_Found)
1116 {
1117 bool IsDefault = IsInfoDefault(Info);
1118 str_format(buffer: aTmpBackendName, buffer_size: sizeof(aTmpBackendName), format: "%s (%d.%d.%d)%s%s", Info.m_pBackendName, Info.m_Major, Info.m_Minor, Info.m_Patch, IsDefault ? " - " : "", IsDefault ? Localize(pStr: "default") : "");
1119 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1120 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1121 if(str_comp_nocase(a: Info.m_pBackendName, b: g_Config.m_GfxBackend) == 0 && g_Config.m_GfxGLMajor == Info.m_Major && g_Config.m_GfxGLMinor == Info.m_Minor && g_Config.m_GfxGLPatch == Info.m_Patch)
1122 {
1123 OldSelectedBackend = CurCounter;
1124 }
1125
1126 s_vBackendInfos[CurCounter] = Info;
1127 ++CurCounter;
1128 }
1129 }
1130 }
1131
1132 if(OldSelectedBackend != -1)
1133 {
1134 // no custom selected
1135 BackendCount -= 1;
1136 }
1137 else
1138 {
1139 // custom selected one
1140 str_format(buffer: aTmpBackendName, buffer_size: sizeof(aTmpBackendName), format: "%s (%s %d.%d.%d)", Localize(pStr: "custom"), g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch);
1141 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1142 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1143 OldSelectedBackend = CurCounter;
1144
1145 s_vBackendInfos[CurCounter].m_pBackendName = "custom";
1146 s_vBackendInfos[CurCounter].m_Major = g_Config.m_GfxGLMajor;
1147 s_vBackendInfos[CurCounter].m_Minor = g_Config.m_GfxGLMinor;
1148 s_vBackendInfos[CurCounter].m_Patch = g_Config.m_GfxGLPatch;
1149 }
1150
1151 static int s_OldSelectedBackend = -1;
1152 if(s_OldSelectedBackend == -1)
1153 s_OldSelectedBackend = OldSelectedBackend;
1154
1155 static CUi::SDropDownState s_BackendDropDownState;
1156 static CScrollRegion s_BackendDropDownScrollRegion;
1157 s_BackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_BackendDropDownScrollRegion;
1158 const int NewBackend = Ui()->DoDropDown(pRect: &BackendDropDown, CurSelection: OldSelectedBackend, pStrs: s_vpBackendIdNamesCStr.data(), Num: BackendCount, State&: s_BackendDropDownState);
1159 if(OldSelectedBackend != NewBackend)
1160 {
1161 str_copy(dst&: g_Config.m_GfxBackend, src: s_vBackendInfos[NewBackend].m_pBackendName);
1162 g_Config.m_GfxGLMajor = s_vBackendInfos[NewBackend].m_Major;
1163 g_Config.m_GfxGLMinor = s_vBackendInfos[NewBackend].m_Minor;
1164 g_Config.m_GfxGLPatch = s_vBackendInfos[NewBackend].m_Patch;
1165
1166 CheckSettings = true;
1167 s_GfxBackendChanged = s_OldSelectedBackend != NewBackend;
1168 }
1169 }
1170
1171 // GPU list
1172 const auto &GpuList = Graphics()->GetGpus();
1173 if(GpuList.m_vGpus.size() > 1)
1174 {
1175 CUIRect Text, GpuDropDown;
1176 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1177 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1178 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1179 MainView.HSplitTop(Cut: 20.0f, pTop: &GpuDropDown, pBottom: &MainView);
1180 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Graphics card"), Size: 16.0f, Align: TEXTALIGN_MC);
1181
1182 static std::vector<const char *> s_vpGpuIdNames;
1183
1184 size_t GpuCount = GpuList.m_vGpus.size() + 1;
1185 s_vpGpuIdNames.resize(sz: GpuCount);
1186
1187 char aCurDeviceName[256 + 4];
1188
1189 int OldSelectedGpu = -1;
1190 for(size_t i = 0; i < GpuCount; ++i)
1191 {
1192 if(i == 0)
1193 {
1194 str_format(buffer: aCurDeviceName, buffer_size: sizeof(aCurDeviceName), format: "%s (%s)", Localize(pStr: "auto"), GpuList.m_AutoGpu.m_aName);
1195 s_vpGpuIdNames[i] = aCurDeviceName;
1196 if(str_comp(a: "auto", b: g_Config.m_GfxGpuName) == 0)
1197 {
1198 OldSelectedGpu = 0;
1199 }
1200 }
1201 else
1202 {
1203 s_vpGpuIdNames[i] = GpuList.m_vGpus[i - 1].m_aName;
1204 if(str_comp(a: GpuList.m_vGpus[i - 1].m_aName, b: g_Config.m_GfxGpuName) == 0)
1205 {
1206 OldSelectedGpu = i;
1207 }
1208 }
1209 }
1210
1211 static int s_OldSelectedGpu = -1;
1212 if(s_OldSelectedGpu == -1)
1213 s_OldSelectedGpu = OldSelectedGpu;
1214
1215 static CUi::SDropDownState s_GpuDropDownState;
1216 static CScrollRegion s_GpuDropDownScrollRegion;
1217 s_GpuDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_GpuDropDownScrollRegion;
1218 const int NewGpu = Ui()->DoDropDown(pRect: &GpuDropDown, CurSelection: OldSelectedGpu, pStrs: s_vpGpuIdNames.data(), Num: GpuCount, State&: s_GpuDropDownState);
1219 if(OldSelectedGpu != NewGpu)
1220 {
1221 if(NewGpu == 0)
1222 str_copy(dst&: g_Config.m_GfxGpuName, src: "auto");
1223 else
1224 str_copy(dst&: g_Config.m_GfxGpuName, src: GpuList.m_vGpus[NewGpu - 1].m_aName);
1225 CheckSettings = true;
1226 s_GfxGpuChanged = NewGpu != s_OldSelectedGpu;
1227 }
1228 }
1229
1230 // check if the new settings require a restart
1231 if(CheckSettings)
1232 {
1233 m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples &&
1234 !s_GfxBackendChanged &&
1235 !s_GfxGpuChanged);
1236 }
1237}
1238
1239void CMenus::RenderSettingsSound(CUIRect MainView)
1240{
1241 CUIRect Button;
1242 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1243 if(DoButton_CheckBox(pId: &g_Config.m_SndEnable, pText: Localize(pStr: "Use sounds"), Checked: g_Config.m_SndEnable, pRect: &Button))
1244 {
1245 g_Config.m_SndEnable ^= 1;
1246 UpdateMusicState();
1247 }
1248
1249 m_NeedRestartSound = g_Config.m_SndEnable && !Sound()->IsSoundEnabled();
1250
1251 if(!g_Config.m_SndEnable)
1252 return;
1253
1254 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1255 if(DoButton_CheckBox(pId: &g_Config.m_SndMusic, pText: Localize(pStr: "Play background music"), Checked: g_Config.m_SndMusic, pRect: &Button))
1256 {
1257 g_Config.m_SndMusic ^= 1;
1258 UpdateMusicState();
1259 }
1260
1261 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1262 if(DoButton_CheckBox(pId: &g_Config.m_SndNonactiveMute, pText: Localize(pStr: "Mute when not active"), Checked: g_Config.m_SndNonactiveMute, pRect: &Button))
1263 g_Config.m_SndNonactiveMute ^= 1;
1264
1265 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1266 if(DoButton_CheckBox(pId: &g_Config.m_SndGame, pText: Localize(pStr: "Enable game sounds"), Checked: g_Config.m_SndGame, pRect: &Button))
1267 g_Config.m_SndGame ^= 1;
1268
1269 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1270 if(DoButton_CheckBox(pId: &g_Config.m_SndGun, pText: Localize(pStr: "Enable gun sound"), Checked: g_Config.m_SndGun, pRect: &Button))
1271 g_Config.m_SndGun ^= 1;
1272
1273 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1274 if(DoButton_CheckBox(pId: &g_Config.m_SndLongPain, pText: Localize(pStr: "Enable long pain sound (used when shooting in freeze)"), Checked: g_Config.m_SndLongPain, pRect: &Button))
1275 g_Config.m_SndLongPain ^= 1;
1276
1277 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1278 if(DoButton_CheckBox(pId: &g_Config.m_SndServerMessage, pText: Localize(pStr: "Enable server message sound"), Checked: g_Config.m_SndServerMessage, pRect: &Button))
1279 g_Config.m_SndServerMessage ^= 1;
1280
1281 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1282 if(DoButton_CheckBox(pId: &g_Config.m_SndChat, pText: Localize(pStr: "Enable regular chat sound"), Checked: g_Config.m_SndChat, pRect: &Button))
1283 g_Config.m_SndChat ^= 1;
1284
1285 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1286 if(DoButton_CheckBox(pId: &g_Config.m_SndTeamChat, pText: Localize(pStr: "Enable team chat sound"), Checked: g_Config.m_SndTeamChat, pRect: &Button))
1287 g_Config.m_SndTeamChat ^= 1;
1288
1289 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1290 if(DoButton_CheckBox(pId: &g_Config.m_SndHighlight, pText: Localize(pStr: "Enable highlighted chat sound"), Checked: g_Config.m_SndHighlight, pRect: &Button))
1291 g_Config.m_SndHighlight ^= 1;
1292
1293 // volume slider
1294 {
1295 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1296 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1297 Ui()->DoScrollbarOption(pId: &g_Config.m_SndVolume, pOption: &g_Config.m_SndVolume, pRect: &Button, pStr: Localize(pStr: "Sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1298 }
1299
1300 // volume slider game sounds
1301 {
1302 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1303 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1304 Ui()->DoScrollbarOption(pId: &g_Config.m_SndGameVolume, pOption: &g_Config.m_SndGameVolume, pRect: &Button, pStr: Localize(pStr: "Game sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1305 }
1306
1307 // volume slider gui sounds
1308 {
1309 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1310 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1311 Ui()->DoScrollbarOption(pId: &g_Config.m_SndChatVolume, pOption: &g_Config.m_SndChatVolume, pRect: &Button, pStr: Localize(pStr: "Chat sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1312 }
1313
1314 // volume slider map sounds
1315 {
1316 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1317 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1318 Ui()->DoScrollbarOption(pId: &g_Config.m_SndMapVolume, pOption: &g_Config.m_SndMapVolume, pRect: &Button, pStr: Localize(pStr: "Map sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1319 }
1320
1321 // volume slider background music
1322 {
1323 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1324 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1325 Ui()->DoScrollbarOption(pId: &g_Config.m_SndBackgroundMusicVolume, pOption: &g_Config.m_SndBackgroundMusicVolume, pRect: &Button, pStr: Localize(pStr: "Background music volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1326 }
1327}
1328
1329void CMenus::RenderLanguageSettings(CUIRect MainView)
1330{
1331 const float CreditsFontSize = 14.0f;
1332 const float CreditsMargin = 10.0f;
1333
1334 CUIRect List, CreditsScroll;
1335 MainView.HSplitBottom(Cut: 4.0f * CreditsFontSize + 2.0f * CreditsMargin, pTop: &List, pBottom: &CreditsScroll);
1336 List.HSplitBottom(Cut: 5.0f, pTop: &List, pBottom: nullptr);
1337
1338 RenderLanguageSelection(MainView: List);
1339
1340 CreditsScroll.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1341
1342 static CScrollRegion s_CreditsScrollRegion;
1343 CScrollRegionParams ScrollParams;
1344 ScrollParams.m_ScrollUnit = CreditsFontSize;
1345 s_CreditsScrollRegion.Begin(pClipRect: &CreditsScroll, pParams: &ScrollParams);
1346
1347 CTextCursor Cursor;
1348 Cursor.m_FontSize = CreditsFontSize;
1349 Cursor.m_LineWidth = CreditsScroll.w - 2.0f * CreditsMargin;
1350
1351 const unsigned OldRenderFlags = TextRender()->GetRenderFlags();
1352 TextRender()->SetRenderFlags(OldRenderFlags | TEXT_RENDER_FLAG_ONE_TIME_USE);
1353 STextContainerIndex CreditsTextContainer;
1354 TextRender()->CreateTextContainer(TextContainerIndex&: CreditsTextContainer, pCursor: &Cursor, pText: Localize(pStr: "English translation by the DDNet Team", pContext: "Translation credits: Add your own name here when you update translations"));
1355 TextRender()->SetRenderFlags(OldRenderFlags);
1356 if(CreditsTextContainer.Valid())
1357 {
1358 CUIRect CreditsLabel;
1359 CreditsScroll.HSplitTop(Cut: TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: CreditsTextContainer).m_H + 2.0f * CreditsMargin, pTop: &CreditsLabel, pBottom: &CreditsScroll);
1360 s_CreditsScrollRegion.AddRect(Rect: CreditsLabel);
1361 CreditsLabel.Margin(Cut: CreditsMargin, pOtherRect: &CreditsLabel);
1362 TextRender()->RenderTextContainer(TextContainerIndex: CreditsTextContainer, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: CreditsLabel.x, Y: CreditsLabel.y);
1363 TextRender()->DeleteTextContainer(TextContainerIndex&: CreditsTextContainer);
1364 }
1365
1366 s_CreditsScrollRegion.End();
1367}
1368
1369bool CMenus::RenderLanguageSelection(CUIRect MainView)
1370{
1371 static int s_SelectedLanguage = -2; // -2 = unloaded, -1 = unset
1372 static CListBox s_ListBox;
1373
1374 if(s_SelectedLanguage == -2)
1375 {
1376 s_SelectedLanguage = -1;
1377 for(size_t i = 0; i < g_Localization.Languages().size(); i++)
1378 {
1379 if(str_comp(a: g_Localization.Languages()[i].m_Filename.c_str(), b: g_Config.m_ClLanguagefile) == 0)
1380 {
1381 s_SelectedLanguage = i;
1382 s_ListBox.ScrollToSelected();
1383 break;
1384 }
1385 }
1386 }
1387
1388 const int OldSelected = s_SelectedLanguage;
1389
1390 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: g_Localization.Languages().size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_SelectedLanguage, pRect: &MainView);
1391
1392 for(const auto &Language : g_Localization.Languages())
1393 {
1394 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Language.m_Name, Selected: s_SelectedLanguage != -1 && !str_comp(a: g_Localization.Languages()[s_SelectedLanguage].m_Name.c_str(), b: Language.m_Name.c_str()));
1395 if(!Item.m_Visible)
1396 continue;
1397
1398 CUIRect FlagRect, Label;
1399 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h * 2.0f, pLeft: &FlagRect, pRight: &Label);
1400 FlagRect.VMargin(Cut: 6.0f, pOtherRect: &FlagRect);
1401 FlagRect.HMargin(Cut: 3.0f, pOtherRect: &FlagRect);
1402 GameClient()->m_CountryFlags.Render(CountryCode: Language.m_CountryCode, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), x: FlagRect.x, y: FlagRect.y, w: FlagRect.w, h: FlagRect.h);
1403
1404 Ui()->DoLabel(pRect: &Label, pText: Language.m_Name.c_str(), Size: 16.0f, Align: TEXTALIGN_ML);
1405 }
1406
1407 s_SelectedLanguage = s_ListBox.DoEnd();
1408
1409 if(OldSelected != s_SelectedLanguage)
1410 {
1411 str_copy(dst&: g_Config.m_ClLanguagefile, src: g_Localization.Languages()[s_SelectedLanguage].m_Filename.c_str());
1412 GameClient()->OnLanguageChange();
1413 }
1414
1415 return s_ListBox.WasItemActivated();
1416}
1417
1418void CMenus::RenderSettings(CUIRect MainView)
1419{
1420 // render background
1421 CUIRect Button, TabBar, RestartBar;
1422 MainView.VSplitRight(Cut: 120.0f, pLeft: &MainView, pRight: &TabBar);
1423 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1424 MainView.Margin(Cut: 20.0f, pOtherRect: &MainView);
1425
1426 const bool NeedRestart = m_NeedRestartGraphics || m_NeedRestartSound || m_NeedRestartUpdate;
1427 if(NeedRestart)
1428 {
1429 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &RestartBar);
1430 MainView.HSplitBottom(Cut: 10.0f, pTop: &MainView, pBottom: nullptr);
1431 }
1432
1433 TabBar.HSplitTop(Cut: 50.0f, pTop: &Button, pBottom: &TabBar);
1434 Button.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_BR, Rounding: 10.0f);
1435
1436 const char *apTabs[SETTINGS_LENGTH] = {
1437 Localize(pStr: "Language"),
1438 Localize(pStr: "General"),
1439 Localize(pStr: "Player"),
1440 Client()->IsSixup() ? "Tee 0.7" : Localize(pStr: "Tee"),
1441 Localize(pStr: "Appearance"),
1442 Localize(pStr: "Controls"),
1443 Localize(pStr: "Graphics"),
1444 Localize(pStr: "Sound"),
1445 Localize(pStr: "DDNet"),
1446 Localize(pStr: "Assets")};
1447 static CButtonContainer s_aTabButtons[SETTINGS_LENGTH];
1448
1449 for(int i = 0; i < SETTINGS_LENGTH; i++)
1450 {
1451 TabBar.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &TabBar);
1452 TabBar.HSplitTop(Cut: 26.0f, pTop: &Button, pBottom: &TabBar);
1453 if(DoButton_MenuTab(pButtonContainer: &s_aTabButtons[i], pText: apTabs[i], Checked: g_Config.m_UiSettingsPage == i, pRect: &Button, Corners: IGraphics::CORNER_R, pAnimator: &m_aAnimatorsSettingsTab[i]))
1454 g_Config.m_UiSettingsPage = i;
1455 }
1456
1457 if(g_Config.m_UiSettingsPage == SETTINGS_LANGUAGE)
1458 {
1459 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_LANGUAGE);
1460 RenderLanguageSettings(MainView);
1461 }
1462 else if(g_Config.m_UiSettingsPage == SETTINGS_GENERAL)
1463 {
1464 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GENERAL);
1465 RenderSettingsGeneral(MainView);
1466 }
1467 else if(g_Config.m_UiSettingsPage == SETTINGS_PLAYER)
1468 {
1469 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_PLAYER);
1470 RenderSettingsPlayer(MainView);
1471 }
1472 else if(g_Config.m_UiSettingsPage == SETTINGS_TEE)
1473 {
1474 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_TEE);
1475 if(Client()->IsSixup())
1476 RenderSettingsTee7(MainView);
1477 else
1478 RenderSettingsTee(MainView);
1479 }
1480 else if(g_Config.m_UiSettingsPage == SETTINGS_APPEARANCE)
1481 {
1482 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_APPEARANCE);
1483 RenderSettingsAppearance(MainView);
1484 }
1485 else if(g_Config.m_UiSettingsPage == SETTINGS_CONTROLS)
1486 {
1487 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_CONTROLS);
1488 m_MenusSettingsControls.Render(MainView);
1489 }
1490 else if(g_Config.m_UiSettingsPage == SETTINGS_GRAPHICS)
1491 {
1492 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GRAPHICS);
1493 RenderSettingsGraphics(MainView);
1494 }
1495 else if(g_Config.m_UiSettingsPage == SETTINGS_SOUND)
1496 {
1497 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_SOUND);
1498 RenderSettingsSound(MainView);
1499 }
1500 else if(g_Config.m_UiSettingsPage == SETTINGS_DDNET)
1501 {
1502 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_DDNET);
1503 RenderSettingsDDNet(MainView);
1504 }
1505 else if(g_Config.m_UiSettingsPage == SETTINGS_ASSETS)
1506 {
1507 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_ASSETS);
1508 RenderSettingsCustom(MainView);
1509 }
1510 else
1511 {
1512 dbg_assert_failed("ui_settings_page invalid");
1513 }
1514
1515 if(NeedRestart)
1516 {
1517 CUIRect RestartWarning, RestartButton;
1518 RestartBar.VSplitRight(Cut: 125.0f, pLeft: &RestartWarning, pRight: &RestartButton);
1519 RestartWarning.VSplitRight(Cut: 10.0f, pLeft: &RestartWarning, pRight: nullptr);
1520 if(m_NeedRestartUpdate)
1521 {
1522 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
1523 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "DDNet Client needs to be restarted to complete update!"), Size: 14.0f, Align: TEXTALIGN_ML);
1524 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1525 }
1526 else
1527 {
1528 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "You must restart the game for all settings to take effect."), Size: 14.0f, Align: TEXTALIGN_ML);
1529 }
1530
1531 static CButtonContainer s_RestartButton;
1532 if(DoButton_Menu(pButtonContainer: &s_RestartButton, pText: Localize(pStr: "Restart"), Checked: 0, pRect: &RestartButton))
1533 {
1534 if(Client()->State() == IClient::STATE_ONLINE || GameClient()->Editor()->HasUnsavedData())
1535 {
1536 m_Popup = POPUP_RESTART;
1537 }
1538 else
1539 {
1540 Client()->Restart();
1541 }
1542 }
1543 }
1544}
1545
1546bool CMenus::RenderHslaScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, float DarkestLight)
1547{
1548 const unsigned PrevPackedColor = *pColor;
1549 ColorHSLA Color(*pColor, Alpha);
1550 const ColorHSLA OriginalColor = Color;
1551 const char *apLabels[] = {Localize(pStr: "Hue"), Localize(pStr: "Sat."), Localize(pStr: "Lht."), Localize(pStr: "Alpha")};
1552 const float SizePerEntry = 20.0f;
1553 const float MarginPerEntry = 5.0f;
1554 const float PreviewMargin = 2.5f;
1555 const float PreviewHeight = 40.0f + 2 * PreviewMargin;
1556 const float OffY = (SizePerEntry + MarginPerEntry) * (3 + (Alpha ? 1 : 0)) - PreviewHeight;
1557
1558 CUIRect Preview;
1559 pRect->VSplitLeft(Cut: PreviewHeight, pLeft: &Preview, pRight: pRect);
1560 Preview.HSplitTop(Cut: OffY / 2.0f, pTop: nullptr, pBottom: &Preview);
1561 Preview.HSplitTop(Cut: PreviewHeight, pTop: &Preview, pBottom: nullptr);
1562
1563 Preview.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f + PreviewMargin);
1564 Preview.Margin(Cut: PreviewMargin, pOtherRect: &Preview);
1565 Preview.Draw(Color: color_cast<ColorRGBA>(hsl: Color.UnclampLighting(Darkest: DarkestLight)), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f + PreviewMargin);
1566
1567 auto &&RenderHueRect = [&](CUIRect *pColorRect) {
1568 float CurXOff = pColorRect->x;
1569 const float SizeColor = pColorRect->w / 6;
1570
1571 // red to yellow
1572 {
1573 IGraphics::CColorVertex aColorVertices[] = {
1574 IGraphics::CColorVertex(0, 1, 0, 0, 1),
1575 IGraphics::CColorVertex(1, 1, 1, 0, 1),
1576 IGraphics::CColorVertex(2, 1, 0, 0, 1),
1577 IGraphics::CColorVertex(3, 1, 1, 0, 1)};
1578 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1579
1580 IGraphics::CFreeformItem Freeform(
1581 CurXOff, pColorRect->y,
1582 CurXOff + SizeColor, pColorRect->y,
1583 CurXOff, pColorRect->y + pColorRect->h,
1584 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1585 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1586 }
1587
1588 // yellow to green
1589 CurXOff += SizeColor;
1590 {
1591 IGraphics::CColorVertex aColorVertices[] = {
1592 IGraphics::CColorVertex(0, 1, 1, 0, 1),
1593 IGraphics::CColorVertex(1, 0, 1, 0, 1),
1594 IGraphics::CColorVertex(2, 1, 1, 0, 1),
1595 IGraphics::CColorVertex(3, 0, 1, 0, 1)};
1596 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1597
1598 IGraphics::CFreeformItem Freeform(
1599 CurXOff, pColorRect->y,
1600 CurXOff + SizeColor, pColorRect->y,
1601 CurXOff, pColorRect->y + pColorRect->h,
1602 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1603 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1604 }
1605
1606 CurXOff += SizeColor;
1607 // green to turquoise
1608 {
1609 IGraphics::CColorVertex aColorVertices[] = {
1610 IGraphics::CColorVertex(0, 0, 1, 0, 1),
1611 IGraphics::CColorVertex(1, 0, 1, 1, 1),
1612 IGraphics::CColorVertex(2, 0, 1, 0, 1),
1613 IGraphics::CColorVertex(3, 0, 1, 1, 1)};
1614 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1615
1616 IGraphics::CFreeformItem Freeform(
1617 CurXOff, pColorRect->y,
1618 CurXOff + SizeColor, pColorRect->y,
1619 CurXOff, pColorRect->y + pColorRect->h,
1620 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1621 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1622 }
1623
1624 CurXOff += SizeColor;
1625 // turquoise to blue
1626 {
1627 IGraphics::CColorVertex aColorVertices[] = {
1628 IGraphics::CColorVertex(0, 0, 1, 1, 1),
1629 IGraphics::CColorVertex(1, 0, 0, 1, 1),
1630 IGraphics::CColorVertex(2, 0, 1, 1, 1),
1631 IGraphics::CColorVertex(3, 0, 0, 1, 1)};
1632 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1633
1634 IGraphics::CFreeformItem Freeform(
1635 CurXOff, pColorRect->y,
1636 CurXOff + SizeColor, pColorRect->y,
1637 CurXOff, pColorRect->y + pColorRect->h,
1638 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1639 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1640 }
1641
1642 CurXOff += SizeColor;
1643 // blue to purple
1644 {
1645 IGraphics::CColorVertex aColorVertices[] = {
1646 IGraphics::CColorVertex(0, 0, 0, 1, 1),
1647 IGraphics::CColorVertex(1, 1, 0, 1, 1),
1648 IGraphics::CColorVertex(2, 0, 0, 1, 1),
1649 IGraphics::CColorVertex(3, 1, 0, 1, 1)};
1650 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1651
1652 IGraphics::CFreeformItem Freeform(
1653 CurXOff, pColorRect->y,
1654 CurXOff + SizeColor, pColorRect->y,
1655 CurXOff, pColorRect->y + pColorRect->h,
1656 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1657 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1658 }
1659
1660 CurXOff += SizeColor;
1661 // purple to red
1662 {
1663 IGraphics::CColorVertex aColorVertices[] = {
1664 IGraphics::CColorVertex(0, 1, 0, 1, 1),
1665 IGraphics::CColorVertex(1, 1, 0, 0, 1),
1666 IGraphics::CColorVertex(2, 1, 0, 1, 1),
1667 IGraphics::CColorVertex(3, 1, 0, 0, 1)};
1668 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1669
1670 IGraphics::CFreeformItem Freeform(
1671 CurXOff, pColorRect->y,
1672 CurXOff + SizeColor, pColorRect->y,
1673 CurXOff, pColorRect->y + pColorRect->h,
1674 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1675 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1676 }
1677 };
1678
1679 auto &&RenderSaturationRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) {
1680 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColor);
1681 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColor);
1682
1683 LeftColor.s = 0.0f;
1684 RightColor.s = 1.0f;
1685
1686 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
1687 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
1688
1689 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1690
1691 IGraphics::CFreeformItem Freeform(
1692 pColorRect->x, pColorRect->y,
1693 pColorRect->x + pColorRect->w, pColorRect->y,
1694 pColorRect->x, pColorRect->y + pColorRect->h,
1695 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1696 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1697 };
1698
1699 auto &&RenderLightingRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) {
1700 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColor);
1701 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColor);
1702
1703 LeftColor.l = DarkestLight;
1704 RightColor.l = 1.0f;
1705
1706 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
1707 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
1708
1709 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1710
1711 IGraphics::CFreeformItem Freeform(
1712 pColorRect->x, pColorRect->y,
1713 pColorRect->x + pColorRect->w, pColorRect->y,
1714 pColorRect->x, pColorRect->y + pColorRect->h,
1715 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1716 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1717 };
1718
1719 auto &&RenderAlphaRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColorFull) {
1720 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: color_cast<ColorHSLA>(rgb: CurColorFull).WithAlpha(alpha: 0.0f));
1721 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: color_cast<ColorHSLA>(rgb: CurColorFull).WithAlpha(alpha: 1.0f));
1722
1723 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1724
1725 IGraphics::CFreeformItem Freeform(
1726 pColorRect->x, pColorRect->y,
1727 pColorRect->x + pColorRect->w, pColorRect->y,
1728 pColorRect->x, pColorRect->y + pColorRect->h,
1729 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1730 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1731 };
1732
1733 for(int i = 0; i < 3 + Alpha; i++)
1734 {
1735 CUIRect Button, Label;
1736 pRect->HSplitTop(Cut: SizePerEntry, pTop: &Button, pBottom: pRect);
1737 pRect->HSplitTop(Cut: MarginPerEntry, pTop: nullptr, pBottom: pRect);
1738 Button.VSplitLeft(Cut: 140.0f, pLeft: &Label, pRight: &Button);
1739 Label.VMargin(Cut: 10.0f, pOtherRect: &Label);
1740
1741 Button.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 1.0f);
1742
1743 CUIRect Rail;
1744 Button.Margin(Cut: 2.0f, pOtherRect: &Rail);
1745
1746 char aBuf[32];
1747
1748 // Hue
1749 if(i == 0)
1750 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %.1f° (%03d)", apLabels[i], Color[i] * 360.0f, round_to_int(f: Color[i] * 255.0f));
1751 // Lht
1752 else if(i == 2)
1753 {
1754 // handle internal light clamping, see `UnclampLighting`
1755 float Lht = DarkestLight + Color[i] * (1.0f - DarkestLight);
1756 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %.1f%% (%03d)", apLabels[i], Lht * 100.0f, round_to_int(f: Color[i] * 255.0f));
1757 }
1758 // Sat and Alpha
1759 else
1760 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %.1f%% (%03d)", apLabels[i], Color[i] * 100.0f, round_to_int(f: Color[i] * 255.0f));
1761 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1762
1763 ColorRGBA HandleColor;
1764 Graphics()->TextureClear();
1765 Graphics()->TrianglesBegin();
1766 if(i == 0)
1767 {
1768 RenderHueRect(&Rail);
1769 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f));
1770 }
1771 else if(i == 1)
1772 {
1773 RenderSaturationRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f)));
1774 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, 0.5f, 1.0f));
1775 }
1776 else if(i == 2)
1777 {
1778 RenderLightingRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, 0.5f, 1.0f)));
1779 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(Darkest: DarkestLight));
1780 }
1781 else if(i == 3)
1782 {
1783 RenderAlphaRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(Darkest: DarkestLight)));
1784 HandleColor = color_cast<ColorRGBA>(hsl: Color.UnclampLighting(Darkest: DarkestLight));
1785 }
1786 Graphics()->TrianglesEnd();
1787
1788 Color[i] = Ui()->DoScrollbarH(pId: &((char *)pColor)[i], pRect: &Button, Current: Color[i], pColorInner: &HandleColor);
1789 }
1790
1791 if(OriginalColor != Color)
1792 {
1793 *pColor = Color.Pack(Alpha);
1794 }
1795 return PrevPackedColor != *pColor;
1796}
1797
1798enum
1799{
1800 APPEARANCE_TAB_HUD = 0,
1801 APPEARANCE_TAB_CHAT = 1,
1802 APPEARANCE_TAB_NAME_PLATE = 2,
1803 APPEARANCE_TAB_HOOK_COLLISION = 3,
1804 APPEARANCE_TAB_INFO_MESSAGES = 4,
1805 APPEARANCE_TAB_LASER = 5,
1806 NUMBER_OF_APPEARANCE_TABS = 6,
1807};
1808
1809void CMenus::RenderSettingsAppearance(CUIRect MainView)
1810{
1811 char aBuf[128];
1812 static int s_CurTab = 0;
1813
1814 CUIRect TabBar, LeftView, RightView, Button;
1815
1816 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
1817 const float TabWidth = TabBar.w / (float)NUMBER_OF_APPEARANCE_TABS;
1818 static CButtonContainer s_aPageTabs[NUMBER_OF_APPEARANCE_TABS] = {};
1819 const char *apTabNames[NUMBER_OF_APPEARANCE_TABS] = {
1820 Localize(pStr: "HUD"),
1821 Localize(pStr: "Chat"),
1822 Localize(pStr: "Name Plate"),
1823 Localize(pStr: "Hook Collisions"),
1824 Localize(pStr: "Info Messages"),
1825 Localize(pStr: "Laser")};
1826
1827 for(int Tab = APPEARANCE_TAB_HUD; Tab < NUMBER_OF_APPEARANCE_TABS; ++Tab)
1828 {
1829 TabBar.VSplitLeft(Cut: TabWidth, pLeft: &Button, pRight: &TabBar);
1830 const int Corners = Tab == APPEARANCE_TAB_HUD ? IGraphics::CORNER_L : (Tab == NUMBER_OF_APPEARANCE_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE);
1831 if(DoButton_MenuTab(pButtonContainer: &s_aPageTabs[Tab], pText: apTabNames[Tab], Checked: s_CurTab == Tab, pRect: &Button, Corners, pAnimator: nullptr, pDefaultColor: nullptr, pActiveColor: nullptr, pHoverColor: nullptr, EdgeRounding: 4.0f))
1832 {
1833 s_CurTab = Tab;
1834 }
1835 }
1836
1837 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1838
1839 const float LineSize = 20.0f;
1840 const float ColorPickerLineSize = 25.0f;
1841 const float HeadlineFontSize = 20.0f;
1842 const float HeadlineHeight = 30.0f;
1843 const float MarginSmall = 5.0f;
1844 const float MarginBetweenViews = 20.0f;
1845
1846 const float ColorPickerLabelSize = 13.0f;
1847 const float ColorPickerLineSpacing = 5.0f;
1848
1849 if(s_CurTab == APPEARANCE_TAB_HUD)
1850 {
1851 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
1852
1853 // ***** HUD ***** //
1854 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "HUD"), FontSize: HeadlineFontSize,
1855 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1856 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1857
1858 // Switch of the entire HUD
1859 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhud, pText: Localize(pStr: "Show ingame HUD"), pValue: &g_Config.m_ClShowhud, pRect: &LeftView, VMargin: LineSize);
1860
1861 // Switches of the various normal HUD elements
1862 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudHealthAmmo, pText: Localize(pStr: "Show health, shields and ammo"), pValue: &g_Config.m_ClShowhudHealthAmmo, pRect: &LeftView, VMargin: LineSize);
1863 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudScore, pText: Localize(pStr: "Show score"), pValue: &g_Config.m_ClShowhudScore, pRect: &LeftView, VMargin: LineSize);
1864 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowLocalTimeAlways, pText: Localize(pStr: "Show local time always"), pValue: &g_Config.m_ClShowLocalTimeAlways, pRect: &LeftView, VMargin: LineSize);
1865 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClSpecCursor, pText: Localize(pStr: "Show spectator cursor"), pValue: &g_Config.m_ClSpecCursor, pRect: &LeftView, VMargin: LineSize);
1866
1867 // Settings of the HUD element for votes
1868 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowVotesAfterVoting, pText: Localize(pStr: "Show votes window after voting"), pValue: &g_Config.m_ClShowVotesAfterVoting, pRect: &LeftView, VMargin: LineSize);
1869
1870 // ***** Scoreboard ***** //
1871 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
1872 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Scoreboard"), FontSize: HeadlineFontSize,
1873 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1874 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1875
1876 ColorRGBA GreenDefault(0.78f, 1.0f, 0.8f, 1.0f);
1877 static CButtonContainer s_AuthedColor, s_SameClanColor;
1878 DoLine_ColorPicker(pResetId: &s_AuthedColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Authed name color in scoreboard"), pColorValue: &g_Config.m_ClAuthedPlayerColor, DefaultColor: GreenDefault, CheckBoxSpacing: false);
1879 DoLine_ColorPicker(pResetId: &s_SameClanColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Same clan color in scoreboard"), pColorValue: &g_Config.m_ClSameClanColor, DefaultColor: GreenDefault, CheckBoxSpacing: false);
1880
1881 // ***** DDRace HUD ***** //
1882 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "DDRace HUD"), FontSize: HeadlineFontSize,
1883 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
1884 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
1885
1886 // Switches of various DDRace HUD elements
1887 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowIds, pText: Localize(pStr: "Show client IDs (scoreboard, chat, spectator)"), pValue: &g_Config.m_ClShowIds, pRect: &RightView, VMargin: LineSize);
1888 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDDRace, pText: Localize(pStr: "Show DDRace HUD"), pValue: &g_Config.m_ClShowhudDDRace, pRect: &RightView, VMargin: LineSize);
1889 if(g_Config.m_ClShowhudDDRace)
1890 {
1891 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudJumpsIndicator, pText: Localize(pStr: "Show jumps indicator"), pValue: &g_Config.m_ClShowhudJumpsIndicator, pRect: &RightView, VMargin: LineSize);
1892 }
1893 else
1894 {
1895 RightView.HSplitTop(Cut: LineSize, pTop: nullptr, pBottom: &RightView); // Create empty space for hidden option
1896 }
1897
1898 // Eye with a number of spectators
1899 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudSpectatorCount, pText: Localize(pStr: "Show number of spectators"), pValue: &g_Config.m_ClShowhudSpectatorCount, pRect: &RightView, VMargin: LineSize);
1900
1901 // Switch for dummy actions display
1902 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDummyActions, pText: Localize(pStr: "Show dummy actions"), pValue: &g_Config.m_ClShowhudDummyActions, pRect: &RightView, VMargin: LineSize);
1903
1904 // Player movement information display settings
1905 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerPosition, pText: Localize(pStr: "Show player position"), pValue: &g_Config.m_ClShowhudPlayerPosition, pRect: &RightView, VMargin: LineSize);
1906 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerSpeed, pText: Localize(pStr: "Show player speed"), pValue: &g_Config.m_ClShowhudPlayerSpeed, pRect: &RightView, VMargin: LineSize);
1907 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerAngle, pText: Localize(pStr: "Show player target angle"), pValue: &g_Config.m_ClShowhudPlayerAngle, pRect: &RightView, VMargin: LineSize);
1908
1909 // Freeze bar settings
1910 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowFreezeBars, pText: Localize(pStr: "Show freeze bars"), pValue: &g_Config.m_ClShowFreezeBars, pRect: &RightView, VMargin: LineSize);
1911 RightView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &RightView);
1912 if(g_Config.m_ClShowFreezeBars)
1913 {
1914 Ui()->DoScrollbarOption(pId: &g_Config.m_ClFreezeBarsAlphaInsideFreeze, pOption: &g_Config.m_ClFreezeBarsAlphaInsideFreeze, pRect: &Button, pStr: Localize(pStr: "Opacity of freeze bars inside freeze"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE, pSuffix: "%");
1915 }
1916 }
1917 else if(s_CurTab == APPEARANCE_TAB_CHAT)
1918 {
1919 CChat &Chat = GameClient()->m_Chat;
1920 CUIRect TopView, PreviewView;
1921 MainView.HSplitBottom(Cut: 220.0f, pTop: &TopView, pBottom: &PreviewView);
1922 TopView.HSplitBottom(Cut: MarginBetweenViews, pTop: &TopView, pBottom: nullptr);
1923 TopView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
1924
1925 // ***** Chat ***** //
1926 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Chat"), FontSize: HeadlineFontSize,
1927 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1928 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1929
1930 // General chat settings
1931 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1932 if(DoButton_CheckBox(pId: &g_Config.m_ClShowChat, pText: Localize(pStr: "Show chat"), Checked: g_Config.m_ClShowChat, pRect: &Button))
1933 {
1934 g_Config.m_ClShowChat = g_Config.m_ClShowChat ? 0 : 1;
1935 }
1936 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1937 if(g_Config.m_ClShowChat)
1938 {
1939 static int s_ShowChat = 0;
1940 if(DoButton_CheckBox(pId: &s_ShowChat, pText: Localize(pStr: "Always show chat"), Checked: g_Config.m_ClShowChat == 2, pRect: &Button))
1941 g_Config.m_ClShowChat = g_Config.m_ClShowChat != 2 ? 2 : 1;
1942 }
1943
1944 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClChatTeamColors, pText: Localize(pStr: "Show names in chat in team colors"), pValue: &g_Config.m_ClChatTeamColors, pRect: &LeftView, VMargin: LineSize);
1945 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowChatFriends, pText: Localize(pStr: "Show only chat messages from friends"), pValue: &g_Config.m_ClShowChatFriends, pRect: &LeftView, VMargin: LineSize);
1946 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowChatTeamMembersOnly, pText: Localize(pStr: "Show only chat messages from team members"), pValue: &g_Config.m_ClShowChatTeamMembersOnly, pRect: &LeftView, VMargin: LineSize);
1947
1948 if(DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClChatOld, pText: Localize(pStr: "Use old chat style"), pValue: &g_Config.m_ClChatOld, pRect: &LeftView, VMargin: LineSize))
1949 GameClient()->m_Chat.RebuildChat();
1950
1951 // DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClCensorChat, Localize("Censor profanity"), &g_Config.m_ClCensorChat, &LeftView, LineSize);
1952
1953 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1954 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClChatFontSize, pOption: &g_Config.m_ClChatFontSize, pRect: &Button, pStr: Localize(pStr: "Chat font size"), Min: 10, Max: 100))
1955 {
1956 Chat.EnsureCoherentWidth();
1957 Chat.RebuildChat();
1958 }
1959
1960 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1961 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClChatWidth, pOption: &g_Config.m_ClChatWidth, pRect: &Button, pStr: Localize(pStr: "Chat width"), Min: 120, Max: 400))
1962 {
1963 Chat.EnsureCoherentFontSize();
1964 Chat.RebuildChat();
1965 }
1966
1967 static CButtonContainer s_BackgroundColor;
1968 DoLine_ColorPicker(pResetId: &s_BackgroundColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Chat background color"), pColorValue: &g_Config.m_ClChatBackgroundColor, DefaultColor: color_cast<ColorRGBA>(hsl: ColorHSLA(DefaultConfig::ClChatBackgroundColor, true)), CheckBoxSpacing: false, pCheckBoxValue: nullptr, Alpha: true);
1969
1970 // ***** Messages ***** //
1971 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Messages"), FontSize: HeadlineFontSize,
1972 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
1973 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
1974
1975 // Message Colors and extra settings
1976 static CButtonContainer s_SystemMessageColor;
1977 DoLine_ColorPicker(pResetId: &s_SystemMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: Localize(pStr: "System message"), pColorValue: &g_Config.m_ClMessageSystemColor, DefaultColor: ColorRGBA(1.0f, 1.0f, 0.5f), CheckBoxSpacing: true, pCheckBoxValue: &g_Config.m_ClShowChatSystem);
1978 static CButtonContainer s_HighlightedMessageColor;
1979 DoLine_ColorPicker(pResetId: &s_HighlightedMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: Localize(pStr: "Highlighted message"), pColorValue: &g_Config.m_ClMessageHighlightColor, DefaultColor: ColorRGBA(1.0f, 0.5f, 0.5f));
1980 static CButtonContainer s_TeamMessageColor;
1981 DoLine_ColorPicker(pResetId: &s_TeamMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: Localize(pStr: "Team message"), pColorValue: &g_Config.m_ClMessageTeamColor, DefaultColor: ColorRGBA(0.65f, 1.0f, 0.65f));
1982 static CButtonContainer s_FriendMessageColor;
1983 DoLine_ColorPicker(pResetId: &s_FriendMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: Localize(pStr: "Friend message"), pColorValue: &g_Config.m_ClMessageFriendColor, DefaultColor: ColorRGBA(1.0f, 0.137f, 0.137f), CheckBoxSpacing: true, pCheckBoxValue: &g_Config.m_ClMessageFriend);
1984 static CButtonContainer s_NormalMessageColor;
1985 DoLine_ColorPicker(pResetId: &s_NormalMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: Localize(pStr: "Normal message"), pColorValue: &g_Config.m_ClMessageColor, DefaultColor: ColorRGBA(1.0f, 1.0f, 1.0f));
1986
1987 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (echo)", Localize(pStr: "Client message"));
1988 static CButtonContainer s_ClientMessageColor;
1989 DoLine_ColorPicker(pResetId: &s_ClientMessageColor, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &RightView, pText: aBuf, pColorValue: &g_Config.m_ClMessageClientColor, DefaultColor: ColorRGBA(0.5f, 0.78f, 1.0f));
1990
1991 // ***** Chat Preview ***** //
1992 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
1993 Align: TEXTALIGN_ML, pRect: &PreviewView, LineSize: HeadlineHeight);
1994 PreviewView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &PreviewView);
1995
1996 // Use the rest of the view for preview
1997 PreviewView.Draw(Color: ColorRGBA(1, 1, 1, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1998 PreviewView.Margin(Cut: MarginSmall, pOtherRect: &PreviewView);
1999
2000 ColorRGBA SystemColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor));
2001 ColorRGBA HighlightedColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageHighlightColor));
2002 ColorRGBA TeamColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageTeamColor));
2003 ColorRGBA FriendColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor));
2004 ColorRGBA NormalColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageColor));
2005 ColorRGBA ClientColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor));
2006 ColorRGBA DefaultNameColor(0.8f, 0.8f, 0.8f, 1.0f);
2007
2008 const float RealFontSize = Chat.FontSize() * 2;
2009 const float RealMsgPaddingX = (!g_Config.m_ClChatOld ? Chat.MessagePaddingX() : 0) * 2;
2010 const float RealMsgPaddingY = (!g_Config.m_ClChatOld ? Chat.MessagePaddingY() : 0) * 2;
2011 const float RealMsgPaddingTee = (!g_Config.m_ClChatOld ? Chat.MessageTeeSize() + CChat::MESSAGE_TEE_PADDING_RIGHT : 0) * 2;
2012 const float RealOffsetY = RealFontSize + RealMsgPaddingY;
2013
2014 const float X = RealMsgPaddingX / 2.0f + PreviewView.x;
2015 float Y = PreviewView.y;
2016 float LineWidth = g_Config.m_ClChatWidth * 2 - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee;
2017
2018 str_copy(dst&: aBuf, src: Client()->PlayerName());
2019
2020 const CAnimState *pIdleState = CAnimState::GetIdle();
2021 const float RealTeeSize = Chat.MessageTeeSize() * 2;
2022 const float RealTeeSizeHalved = Chat.MessageTeeSize();
2023 constexpr float TWSkinUnreliableOffset = -0.25f;
2024 const float OffsetTeeY = RealTeeSizeHalved;
2025 const float FullHeightMinusTee = RealOffsetY - RealTeeSize;
2026
2027 struct SPreviewLine
2028 {
2029 int m_ClientId;
2030 bool m_Team;
2031 char m_aName[64];
2032 char m_aText[256];
2033 bool m_Friend;
2034 bool m_Player;
2035 bool m_Client;
2036 bool m_Highlighted;
2037 int m_TimesRepeated;
2038
2039 CTeeRenderInfo m_RenderInfo;
2040 };
2041
2042 static std::vector<SPreviewLine> s_vLines;
2043
2044 enum ELineFlag
2045 {
2046 FLAG_TEAM = 1 << 0,
2047 FLAG_FRIEND = 1 << 1,
2048 FLAG_HIGHLIGHT = 1 << 2,
2049 FLAG_CLIENT = 1 << 3
2050 };
2051 enum
2052 {
2053 PREVIEW_SYS,
2054 PREVIEW_HIGHLIGHT,
2055 PREVIEW_TEAM,
2056 PREVIEW_FRIEND,
2057 PREVIEW_SPAMMER,
2058 PREVIEW_CLIENT
2059 };
2060 auto &&SetPreviewLine = [](int Index, int ClientId, const char *pName, const char *pText, int Flag, int Repeats) {
2061 SPreviewLine *pLine;
2062 if((int)s_vLines.size() <= Index)
2063 {
2064 s_vLines.emplace_back();
2065 pLine = &s_vLines.back();
2066 }
2067 else
2068 {
2069 pLine = &s_vLines[Index];
2070 }
2071 pLine->m_ClientId = ClientId;
2072 pLine->m_Team = Flag & FLAG_TEAM;
2073 pLine->m_Friend = Flag & FLAG_FRIEND;
2074 pLine->m_Player = ClientId >= 0;
2075 pLine->m_Highlighted = Flag & FLAG_HIGHLIGHT;
2076 pLine->m_Client = Flag & FLAG_CLIENT;
2077 pLine->m_TimesRepeated = Repeats;
2078 str_copy(dst&: pLine->m_aName, src: pName);
2079 str_copy(dst&: pLine->m_aText, src: pText);
2080 };
2081 auto &&SetLineSkin = [RealTeeSize](int Index, const CSkin *pSkin) {
2082 if(Index >= (int)s_vLines.size())
2083 return;
2084 s_vLines[Index].m_RenderInfo.m_Size = RealTeeSize;
2085 s_vLines[Index].m_RenderInfo.Apply(pSkin);
2086 };
2087
2088 auto &&RenderPreview = [&](int LineIndex, int x, int y, bool Render = true) {
2089 if(LineIndex >= (int)s_vLines.size())
2090 return vec2(0, 0);
2091 CTextCursor LocalCursor;
2092 LocalCursor.SetPosition(vec2(x, y));
2093 LocalCursor.m_FontSize = RealFontSize;
2094 LocalCursor.m_Flags = Render ? TEXTFLAG_RENDER : 0;
2095 LocalCursor.m_LineWidth = LineWidth;
2096 const auto &Line = s_vLines[LineIndex];
2097
2098 char aClientId[16] = "";
2099 if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2100 {
2101 GameClient()->FormatClientId(ClientId: Line.m_ClientId, aClientId, Format: EClientIdFormat::INDENT_FORCE);
2102 }
2103
2104 char aCount[12];
2105 if(Line.m_ClientId < 0)
2106 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: "[%d] ", Line.m_TimesRepeated + 1);
2107 else
2108 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: " [%d]", Line.m_TimesRepeated + 1);
2109
2110 if(Line.m_Player)
2111 {
2112 LocalCursor.m_X += RealMsgPaddingTee;
2113
2114 if(Line.m_Friend && g_Config.m_ClMessageFriend)
2115 {
2116 if(Render)
2117 TextRender()->TextColor(Color: FriendColor);
2118 TextRender()->TextEx(pCursor: &LocalCursor, pText: "♥ ", Length: -1);
2119 }
2120 }
2121
2122 ColorRGBA NameColor;
2123 if(Line.m_Team)
2124 NameColor = CalculateNameColor(TextColorHSL: color_cast<ColorHSLA>(rgb: TeamColor));
2125 else if(Line.m_Player)
2126 NameColor = DefaultNameColor;
2127 else if(Line.m_Client)
2128 NameColor = ClientColor;
2129 else
2130 NameColor = SystemColor;
2131
2132 if(Render)
2133 TextRender()->TextColor(Color: NameColor);
2134
2135 TextRender()->TextEx(pCursor: &LocalCursor, pText: aClientId);
2136 TextRender()->TextEx(pCursor: &LocalCursor, pText: Line.m_aName);
2137
2138 if(Line.m_TimesRepeated > 0)
2139 {
2140 if(Render)
2141 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.3f);
2142 TextRender()->TextEx(pCursor: &LocalCursor, pText: aCount, Length: -1);
2143 }
2144
2145 if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2146 {
2147 if(Render)
2148 TextRender()->TextColor(Color: NameColor);
2149 TextRender()->TextEx(pCursor: &LocalCursor, pText: ": ", Length: -1);
2150 }
2151
2152 CTextCursor AppendCursor = LocalCursor;
2153 AppendCursor.m_LongestLineWidth = 0.0f;
2154 if(!g_Config.m_ClChatOld)
2155 {
2156 AppendCursor.m_StartX = LocalCursor.m_X;
2157 AppendCursor.m_LineWidth -= LocalCursor.m_LongestLineWidth;
2158 }
2159
2160 if(Render)
2161 {
2162 if(Line.m_Highlighted)
2163 TextRender()->TextColor(Color: HighlightedColor);
2164 else if(Line.m_Team)
2165 TextRender()->TextColor(Color: TeamColor);
2166 else if(Line.m_Player)
2167 TextRender()->TextColor(Color: NormalColor);
2168 }
2169
2170 TextRender()->TextEx(pCursor: &AppendCursor, pText: Line.m_aText, Length: -1);
2171 if(Render)
2172 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
2173
2174 return vec2{LocalCursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth, AppendCursor.Height() + RealMsgPaddingY};
2175 };
2176
2177 // Set preview lines
2178 {
2179 char aLineBuilder[128];
2180
2181 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "'%s' entered and joined the game", aBuf);
2182 SetPreviewLine(PREVIEW_SYS, -1, "*** ", aLineBuilder, 0, 0);
2183
2184 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "Hey, how are you %s?", aBuf);
2185 SetPreviewLine(PREVIEW_HIGHLIGHT, 7, "Random Tee", aLineBuilder, FLAG_HIGHLIGHT, 0);
2186
2187 SetPreviewLine(PREVIEW_TEAM, 11, "Your Teammate", "Let's speedrun this!", FLAG_TEAM, 0);
2188 SetPreviewLine(PREVIEW_FRIEND, 8, "Friend", "Hello there", FLAG_FRIEND, 0);
2189 SetPreviewLine(PREVIEW_SPAMMER, 9, "Spammer", "Hey fools, I'm spamming here!", 0, 5);
2190 SetPreviewLine(PREVIEW_CLIENT, -1, "— ", "Echo command executed", FLAG_CLIENT, 0);
2191 }
2192
2193 SetLineSkin(1, GameClient()->m_Skins.Find(pName: "pinky"));
2194 SetLineSkin(2, GameClient()->m_Skins.Find(pName: "default"));
2195 SetLineSkin(3, GameClient()->m_Skins.Find(pName: "cammostripes"));
2196 SetLineSkin(4, GameClient()->m_Skins.Find(pName: "beast"));
2197
2198 // Backgrounds first
2199 if(!g_Config.m_ClChatOld)
2200 {
2201 Graphics()->TextureClear();
2202 Graphics()->QuadsBegin();
2203 Graphics()->SetColor(color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClChatBackgroundColor, true)));
2204
2205 float TempY = Y;
2206 const float RealBackgroundRounding = Chat.MessageRounding() * 2.0f;
2207
2208 auto &&RenderMessageBackground = [&](int LineIndex) {
2209 auto Size = RenderPreview(LineIndex, 0, 0, false);
2210 Graphics()->DrawRectExt(x: X - RealMsgPaddingX / 2.0f, y: TempY - RealMsgPaddingY / 2.0f, w: Size.x + RealMsgPaddingX * 1.5f, h: Size.y, r: RealBackgroundRounding, Corners: IGraphics::CORNER_ALL);
2211 return Size.y;
2212 };
2213
2214 if(g_Config.m_ClShowChatSystem)
2215 {
2216 TempY += RenderMessageBackground(PREVIEW_SYS);
2217 }
2218
2219 if(!g_Config.m_ClShowChatFriends)
2220 {
2221 if(!g_Config.m_ClShowChatTeamMembersOnly)
2222 TempY += RenderMessageBackground(PREVIEW_HIGHLIGHT);
2223 TempY += RenderMessageBackground(PREVIEW_TEAM);
2224 }
2225
2226 if(!g_Config.m_ClShowChatTeamMembersOnly)
2227 TempY += RenderMessageBackground(PREVIEW_FRIEND);
2228
2229 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2230 {
2231 TempY += RenderMessageBackground(PREVIEW_SPAMMER);
2232 }
2233
2234 TempY += RenderMessageBackground(PREVIEW_CLIENT);
2235
2236 Graphics()->QuadsEnd();
2237 }
2238
2239 // System
2240 if(g_Config.m_ClShowChatSystem)
2241 {
2242 Y += RenderPreview(PREVIEW_SYS, X, Y).y;
2243 }
2244
2245 if(!g_Config.m_ClShowChatFriends)
2246 {
2247 // Highlighted
2248 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2249 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &s_vLines[PREVIEW_HIGHLIGHT].m_RenderInfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0.1f), Pos: vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset));
2250 if(!g_Config.m_ClShowChatTeamMembersOnly)
2251 Y += RenderPreview(PREVIEW_HIGHLIGHT, X, Y).y;
2252
2253 // Team
2254 if(!g_Config.m_ClChatOld)
2255 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &s_vLines[PREVIEW_TEAM].m_RenderInfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0.1f), Pos: vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset));
2256 Y += RenderPreview(PREVIEW_TEAM, X, Y).y;
2257 }
2258
2259 // Friend
2260 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2261 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &s_vLines[PREVIEW_FRIEND].m_RenderInfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0.1f), Pos: vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset));
2262 if(!g_Config.m_ClShowChatTeamMembersOnly)
2263 Y += RenderPreview(PREVIEW_FRIEND, X, Y).y;
2264
2265 // Normal
2266 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2267 {
2268 if(!g_Config.m_ClChatOld)
2269 RenderTools()->RenderTee(pAnim: pIdleState, pInfo: &s_vLines[PREVIEW_SPAMMER].m_RenderInfo, Emote: EMOTE_NORMAL, Dir: vec2(1, 0.1f), Pos: vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset));
2270 Y += RenderPreview(PREVIEW_SPAMMER, X, Y).y;
2271 }
2272 // Client
2273 RenderPreview(PREVIEW_CLIENT, X, Y);
2274
2275 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
2276 }
2277 else if(s_CurTab == APPEARANCE_TAB_NAME_PLATE)
2278 {
2279 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2280
2281 // ***** Name Plate ***** //
2282 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Name Plate"), FontSize: HeadlineFontSize,
2283 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2284 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2285
2286 // General name plate settings
2287 {
2288 int Pressed = (g_Config.m_ClNamePlates ? 2 : 0) + (g_Config.m_ClNamePlatesOwn ? 1 : 0);
2289 if(DoLine_RadioMenu(View&: LeftView, pLabel: Localize(pStr: "Show name plates"),
2290 vButtonContainers&: m_vButtonContainersNamePlateShow,
2291 vLabels: {Localize(pStr: "None", pContext: "Show name plates"), Localize(pStr: "Own", pContext: "Show name plates"), Localize(pStr: "Others", pContext: "Show name plates"), Localize(pStr: "All", pContext: "Show name plates")},
2292 vValues: {0, 1, 2, 3},
2293 Value&: Pressed))
2294 {
2295 g_Config.m_ClNamePlates = Pressed & 2 ? 1 : 0;
2296 g_Config.m_ClNamePlatesOwn = Pressed & 1 ? 1 : 0;
2297 }
2298 }
2299 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2300 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesSize, pOption: &g_Config.m_ClNamePlatesSize, pRect: &Button, pStr: Localize(pStr: "Name plates size"), Min: -50, Max: 100);
2301 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2302 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesOffset, pOption: &g_Config.m_ClNamePlatesOffset, pRect: &Button, pStr: Localize(pStr: "Name plates offset"), Min: 10, Max: 50);
2303
2304 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesClan, pText: Localize(pStr: "Show clan above name plates"), pValue: &g_Config.m_ClNamePlatesClan, pRect: &LeftView, VMargin: LineSize);
2305 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2306 if(g_Config.m_ClNamePlatesClan)
2307 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesClanSize, pOption: &g_Config.m_ClNamePlatesClanSize, pRect: &Button, pStr: Localize(pStr: "Clan plates size"), Min: -50, Max: 100);
2308
2309 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesTeamcolors, pText: Localize(pStr: "Use team colors for name plates"), pValue: &g_Config.m_ClNamePlatesTeamcolors, pRect: &LeftView, VMargin: LineSize);
2310 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesFriendMark, pText: Localize(pStr: "Show friend icon in name plates"), pValue: &g_Config.m_ClNamePlatesFriendMark, pRect: &LeftView, VMargin: LineSize);
2311
2312 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesIds, pText: Localize(pStr: "Show client IDs in name plates"), pValue: &g_Config.m_ClNamePlatesIds, pRect: &LeftView, VMargin: LineSize);
2313 if(g_Config.m_ClNamePlatesIds > 0)
2314 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesIdsSeparateLine, pText: Localize(pStr: "Show client IDs on a separate line"), pValue: &g_Config.m_ClNamePlatesIdsSeparateLine, pRect: &LeftView, VMargin: LineSize);
2315 else
2316 LeftView.HSplitTop(Cut: LineSize, pTop: nullptr, pBottom: &LeftView);
2317 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2318 if(g_Config.m_ClNamePlatesIds > 0 && g_Config.m_ClNamePlatesIdsSeparateLine > 0)
2319 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesIdsSize, pOption: &g_Config.m_ClNamePlatesIdsSize, pRect: &Button, pStr: Localize(pStr: "Client IDs size"), Min: -50, Max: 100);
2320
2321 // ***** Hook Strength ***** //
2322 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
2323 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Hook Strength"), FontSize: HeadlineFontSize,
2324 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2325 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2326
2327 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2328 if(DoButton_CheckBox(pId: &g_Config.m_ClNamePlatesStrong, pText: Localize(pStr: "Show hook strength icon indicator"), Checked: g_Config.m_ClNamePlatesStrong, pRect: &Button))
2329 {
2330 g_Config.m_ClNamePlatesStrong = g_Config.m_ClNamePlatesStrong ? 0 : 1;
2331 }
2332 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2333 if(g_Config.m_ClNamePlatesStrong)
2334 {
2335 static int s_NamePlatesStrong = 0;
2336 if(DoButton_CheckBox(pId: &s_NamePlatesStrong, pText: Localize(pStr: "Show hook strength number indicator"), Checked: g_Config.m_ClNamePlatesStrong == 2, pRect: &Button))
2337 g_Config.m_ClNamePlatesStrong = g_Config.m_ClNamePlatesStrong != 2 ? 2 : 1;
2338 }
2339
2340 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2341 if(g_Config.m_ClNamePlatesStrong)
2342 {
2343 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesStrongSize, pOption: &g_Config.m_ClNamePlatesStrongSize, pRect: &Button, pStr: Localize(pStr: "Size of hook strength icon and number indicator"), Min: -50, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
2344 }
2345
2346 // ***** Key Presses ***** //
2347 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
2348 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Key Presses"), FontSize: HeadlineFontSize,
2349 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2350 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2351
2352 DoLine_RadioMenu(View&: LeftView, pLabel: Localize(pStr: "Show players' key presses"),
2353 vButtonContainers&: m_vButtonContainersNamePlateKeyPresses,
2354 vLabels: {Localize(pStr: "None", pContext: "Show players' key presses"), Localize(pStr: "Own", pContext: "Show players' key presses"), Localize(pStr: "Others", pContext: "Show players' key presses"), Localize(pStr: "All", pContext: "Show players' key presses")},
2355 vValues: {0, 3, 1, 2},
2356 Value&: g_Config.m_ClShowDirection);
2357
2358 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2359 if(g_Config.m_ClShowDirection > 0)
2360 Ui()->DoScrollbarOption(pId: &g_Config.m_ClDirectionSize, pOption: &g_Config.m_ClDirectionSize, pRect: &Button, pStr: Localize(pStr: "Size of key press icons"), Min: -50, Max: 100);
2361
2362 // ***** Name Plate Preview ***** //
2363 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2364 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2365 RightView.HSplitTop(Cut: 2.0f * MarginSmall, pTop: nullptr, pBottom: &RightView);
2366
2367 // ***** Name Plate Dummy Preview ***** //
2368 RightView.HSplitBottom(Cut: LineSize, pTop: &RightView, pBottom: &Button);
2369 if(DoButton_CheckBox(pId: &m_DummyNamePlatePreview, pText: g_Config.m_ClDummy ? Localize(pStr: "Preview player's name plate") : Localize(pStr: "Preview dummy's name plate"), Checked: m_DummyNamePlatePreview, pRect: &Button))
2370 m_DummyNamePlatePreview = !m_DummyNamePlatePreview;
2371
2372 int Dummy = g_Config.m_ClDummy != (m_DummyNamePlatePreview ? 1 : 0);
2373
2374 const vec2 Position = RightView.Center();
2375
2376 GameClient()->m_NamePlates.RenderNamePlatePreview(Position, Dummy);
2377 }
2378 else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION)
2379 {
2380 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2381
2382 // ***** Hookline ***** //
2383 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Hook collision line"), FontSize: HeadlineFontSize,
2384 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2385 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2386
2387 // General hookline settings
2388 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2389 if(DoButton_CheckBox(pId: &g_Config.m_ClShowHookCollOwn, pText: Localize(pStr: "Show own player's hook collision line"), Checked: g_Config.m_ClShowHookCollOwn, pRect: &Button))
2390 {
2391 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn ? 0 : 1;
2392 }
2393 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2394 if(g_Config.m_ClShowHookCollOwn)
2395 {
2396 static int s_ShowHookCollOwn = 0;
2397 if(DoButton_CheckBox(pId: &s_ShowHookCollOwn, pText: Localize(pStr: "Always show own player's hook collision line"), Checked: g_Config.m_ClShowHookCollOwn == 2, pRect: &Button))
2398 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn != 2 ? 2 : 1;
2399 }
2400
2401 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2402 if(DoButton_CheckBox(pId: &g_Config.m_ClShowHookCollOther, pText: Localize(pStr: "Show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther, pRect: &Button))
2403 {
2404 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther >= 1 ? 0 : 1;
2405 }
2406 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2407 if(g_Config.m_ClShowHookCollOther)
2408 {
2409 static int s_ShowHookCollOther = 0;
2410 if(DoButton_CheckBox(pId: &s_ShowHookCollOther, pText: Localize(pStr: "Always show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther == 2, pRect: &Button))
2411 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther != 2 ? 2 : 1;
2412 }
2413
2414 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2415 Ui()->DoScrollbarOption(pId: &g_Config.m_ClHookCollSize, pOption: &g_Config.m_ClHookCollSize, pRect: &Button, pStr: Localize(pStr: "Width of your own hook collision line"), Min: 0, Max: 20, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
2416
2417 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2418 Ui()->DoScrollbarOption(pId: &g_Config.m_ClHookCollSizeOther, pOption: &g_Config.m_ClHookCollSizeOther, pRect: &Button, pStr: Localize(pStr: "Width of others' hook collision line"), Min: 0, Max: 20, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
2419
2420 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2421 Ui()->DoScrollbarOption(pId: &g_Config.m_ClHookCollAlpha, pOption: &g_Config.m_ClHookCollAlpha, pRect: &Button, pStr: Localize(pStr: "Hook collision line opacity"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE, pSuffix: "%");
2422
2423 static CButtonContainer s_HookCollNoCollResetId, s_HookCollHookableCollResetId, s_HookCollTeeCollResetId, s_HookCollTipColorResetId;
2424 static int s_HookCollToolTip;
2425
2426 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Colors of the hook collision line:"), FontSize: 13.0f,
2427 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2428
2429 Ui()->DoButtonLogic(pId: &s_HookCollToolTip, Checked: 0, pRect: &LeftView, Flags: BUTTONFLAG_NONE); // Just for the tooltip, result ignored
2430 GameClient()->m_Tooltips.DoToolTip(pId: &s_HookCollToolTip, pNearRect: &LeftView, pText: Localize(pStr: "Your movements are not taken into account when calculating the line colors"));
2431 DoLine_ColorPicker(pResetId: &s_HookCollNoCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "When nothing is hookable", pContext: "Hook collision line color"), pColorValue: &g_Config.m_ClHookCollColorNoColl, DefaultColor: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), CheckBoxSpacing: false);
2432 DoLine_ColorPicker(pResetId: &s_HookCollHookableCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "When something is hookable", pContext: "Hook collision line color"), pColorValue: &g_Config.m_ClHookCollColorHookableColl, DefaultColor: ColorRGBA(130.0f / 255.0f, 232.0f / 255.0f, 160.0f / 255.0f, 1.0f), CheckBoxSpacing: false);
2433 DoLine_ColorPicker(pResetId: &s_HookCollTeeCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "When a Tee is hookable", pContext: "Hook collision line color"), pColorValue: &g_Config.m_ClHookCollColorTeeColl, DefaultColor: ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f), CheckBoxSpacing: false);
2434 DoLine_ColorPicker(pResetId: &s_HookCollTipColorResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Hook collision line tip", pContext: "Hook collision line color"), pColorValue: &g_Config.m_ClHookCollTipColor, DefaultColor: ColorRGBA(1.0f, 1.0f, 0.0f, 0.5f), CheckBoxSpacing: false, pCheckBoxValue: nullptr, Alpha: true);
2435
2436 // ***** Hook collisions preview ***** //
2437 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2438 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2439 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2440
2441 auto DoHookCollision = [this](const vec2 &Pos, const float &Length, const int &Size, const ColorRGBA &Color, const ColorRGBA &TipColor, const bool &Invert) {
2442 ColorRGBA ColorModified = Color;
2443 ColorRGBA TipColorModified = TipColor;
2444 if(Invert)
2445 ColorModified = color_invert(col: ColorModified);
2446 ColorModified = ColorModified.WithAlpha(alpha: (float)g_Config.m_ClHookCollAlpha / 100);
2447 TipColorModified = TipColor.WithMultipliedAlpha(alpha: (float)g_Config.m_ClHookCollAlpha / 100);
2448 Graphics()->TextureClear();
2449 if(Size > 0)
2450 {
2451 Graphics()->QuadsBegin();
2452 Graphics()->SetColor(ColorModified);
2453 float LineWidth = 0.5f + (float)(Size - 1) * 0.25f;
2454 IGraphics::CQuadItem QuadItem(Pos.x, Pos.y - LineWidth, Length, LineWidth * 2.f);
2455 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
2456 if(TipColor.a > 0.0f)
2457 {
2458 Graphics()->SetColor(TipColorModified);
2459 IGraphics::CQuadItem TipQuadItem(Pos.x + Length, Pos.y - LineWidth, 15.f, LineWidth * 2.f);
2460 Graphics()->QuadsDrawTL(pArray: &TipQuadItem, Num: 1);
2461 }
2462 Graphics()->QuadsEnd();
2463 }
2464 else
2465 {
2466 Graphics()->LinesBegin();
2467 Graphics()->SetColor(ColorModified);
2468 IGraphics::CLineItem LineItem(Pos.x, Pos.y, Pos.x + Length, Pos.y);
2469 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
2470 if(TipColor.a > 0.0f)
2471 {
2472 Graphics()->SetColor(TipColorModified);
2473 IGraphics::CLineItem TipLineItem(Pos.x + Length, Pos.y, Pos.x + Length + 15.f, Pos.y);
2474 Graphics()->LinesDraw(pArray: &TipLineItem, Num: 1);
2475 }
2476 Graphics()->LinesEnd();
2477 }
2478 };
2479
2480 CTeeRenderInfo OwnSkinInfo;
2481 OwnSkinInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClPlayerSkin));
2482 OwnSkinInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClPlayerUseCustomColor, ColorBody: g_Config.m_ClPlayerColorBody, ColorFeet: g_Config.m_ClPlayerColorFeet);
2483 OwnSkinInfo.m_Size = 50.0f;
2484
2485 CTeeRenderInfo DummySkinInfo;
2486 DummySkinInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClDummySkin));
2487 DummySkinInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClDummyUseCustomColor, ColorBody: g_Config.m_ClDummyColorBody, ColorFeet: g_Config.m_ClDummyColorFeet);
2488 DummySkinInfo.m_Size = 50.0f;
2489
2490 vec2 TeeRenderPos, DummyRenderPos;
2491
2492 const float LineLength = 150.f;
2493 const float LeftMargin = 30.f;
2494
2495 const int TileScale = 32.0f;
2496
2497 // Toggled via checkbox later, inverts some previews
2498 static bool s_HookCollPressed = false;
2499
2500 CUIRect PreviewColl;
2501
2502 // ***** Unhookable Tile Preview *****
2503 CUIRect PreviewNoColl;
2504 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewNoColl, pBottom: &RightView);
2505 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2506 TeeRenderPos = vec2(PreviewNoColl.x + LeftMargin, PreviewNoColl.y + PreviewNoColl.h / 2.0f);
2507 DoHookCollision(TeeRenderPos, PreviewNoColl.w - LineLength, g_Config.m_ClHookCollSize, color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorNoColl)), ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f), s_HookCollPressed);
2508 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2509
2510 CUIRect NoHookTileRect;
2511 PreviewNoColl.VSplitRight(Cut: LineLength, pLeft: &PreviewNoColl, pRight: &NoHookTileRect);
2512 NoHookTileRect.VSplitLeft(Cut: 50.0f, pLeft: &NoHookTileRect, pRight: nullptr);
2513 NoHookTileRect.Margin(Cut: 10.0f, pOtherRect: &NoHookTileRect);
2514
2515 // Render unhookable tile
2516 Graphics()->TextureClear();
2517 Graphics()->TextureSet(Texture: GameClient()->m_MapImages.GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
2518 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2519 RenderMap()->RenderTile(x: NoHookTileRect.x, y: NoHookTileRect.y, Index: TILE_NOHOOK, Scale: TileScale, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2520
2521 // ***** Hookable Tile Preview *****
2522 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2523 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2524 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2525 DoHookCollision(TeeRenderPos, PreviewColl.w - LineLength, g_Config.m_ClHookCollSize, color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorHookableColl)), ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f), s_HookCollPressed);
2526 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2527
2528 CUIRect HookTileRect;
2529 PreviewColl.VSplitRight(Cut: LineLength, pLeft: &PreviewColl, pRight: &HookTileRect);
2530 HookTileRect.VSplitLeft(Cut: 50.0f, pLeft: &HookTileRect, pRight: nullptr);
2531 HookTileRect.Margin(Cut: 10.0f, pOtherRect: &HookTileRect);
2532
2533 // Render hookable tile
2534 Graphics()->TextureClear();
2535 Graphics()->TextureSet(Texture: GameClient()->m_MapImages.GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
2536 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2537 RenderMap()->RenderTile(x: HookTileRect.x, y: HookTileRect.y, Index: TILE_SOLID, Scale: TileScale, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2538
2539 // ***** Hook Dummy Preview *****
2540 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2541 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2542 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2543 DummyRenderPos = vec2(PreviewColl.x + PreviewColl.w - LineLength - 5.f + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2544 DoHookCollision(TeeRenderPos, PreviewColl.w - LineLength - 15.f, g_Config.m_ClHookCollSize, color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorTeeColl)), ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f), s_HookCollPressed);
2545 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &DummySkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: DummyRenderPos);
2546 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2547
2548 // ***** Hook Dummy Reverse Preview *****
2549 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2550 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2551 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2552 DummyRenderPos = vec2(PreviewColl.x + PreviewColl.w - LineLength - 5.f + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2553 DoHookCollision(TeeRenderPos, PreviewColl.w - LineLength - 15.f, g_Config.m_ClHookCollSizeOther, color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorTeeColl)), ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f), false);
2554 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: DummyRenderPos);
2555 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &DummySkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2556
2557 // ***** Hook Line Tip Preview *****
2558 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2559 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2560 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2561 DoHookCollision(TeeRenderPos, PreviewColl.w - LineLength - 15.f, g_Config.m_ClHookCollSize, color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollColorNoColl)), color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClHookCollTipColor, true)), s_HookCollPressed);
2562 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2563
2564 // ***** Preview +hookcoll pressed toggle *****
2565 RightView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &RightView);
2566 if(DoButton_CheckBox(pId: &s_HookCollPressed, pText: Localize(pStr: "Preview 'Hook collisions' being pressed"), Checked: s_HookCollPressed, pRect: &Button))
2567 s_HookCollPressed = !s_HookCollPressed;
2568 }
2569 else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES)
2570 {
2571 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2572
2573 // ***** Info Messages ***** //
2574 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Info Messages"), FontSize: HeadlineFontSize,
2575 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2576 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2577
2578 // General info messages settings
2579 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2580 if(DoButton_CheckBox(pId: &g_Config.m_ClShowKillMessages, pText: Localize(pStr: "Show kill messages"), Checked: g_Config.m_ClShowKillMessages, pRect: &Button))
2581 {
2582 g_Config.m_ClShowKillMessages ^= 1;
2583 }
2584
2585 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2586 if(DoButton_CheckBox(pId: &g_Config.m_ClShowFinishMessages, pText: Localize(pStr: "Show finish messages"), Checked: g_Config.m_ClShowFinishMessages, pRect: &Button))
2587 {
2588 g_Config.m_ClShowFinishMessages ^= 1;
2589 }
2590
2591 static CButtonContainer s_KillMessageNormalColorId, s_KillMessageHighlightColorId;
2592 DoLine_ColorPicker(pResetId: &s_KillMessageNormalColorId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Normal Color"), pColorValue: &g_Config.m_ClKillMessageNormalColor, DefaultColor: ColorRGBA(1.0f, 1.0f, 1.0f), CheckBoxSpacing: false);
2593 DoLine_ColorPicker(pResetId: &s_KillMessageHighlightColorId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Highlight Color"), pColorValue: &g_Config.m_ClKillMessageHighlightColor, DefaultColor: ColorRGBA(1.0f, 1.0f, 1.0f), CheckBoxSpacing: false);
2594 }
2595 else if(s_CurTab == APPEARANCE_TAB_LASER)
2596 {
2597 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2598
2599 // ***** Weapons ***** //
2600 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Weapons"), FontSize: HeadlineFontSize,
2601 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2602 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2603
2604 // General weapon laser settings
2605 static CButtonContainer s_LaserRifleOutResetId, s_LaserRifleInResetId, s_LaserShotgunOutResetId, s_LaserShotgunInResetId;
2606
2607 ColorHSLA LaserRifleOutlineColor = DoLine_ColorPicker(pResetId: &s_LaserRifleOutResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Rifle Laser Outline Color"), pColorValue: &g_Config.m_ClLaserRifleOutlineColor, DefaultColor: ColorRGBA(0.074402f, 0.074402f, 0.247166f, 1.0f), CheckBoxSpacing: false);
2608 ColorHSLA LaserRifleInnerColor = DoLine_ColorPicker(pResetId: &s_LaserRifleInResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Rifle Laser Inner Color"), pColorValue: &g_Config.m_ClLaserRifleInnerColor, DefaultColor: ColorRGBA(0.498039f, 0.498039f, 1.0f, 1.0f), CheckBoxSpacing: false);
2609 ColorHSLA LaserShotgunOutlineColor = DoLine_ColorPicker(pResetId: &s_LaserShotgunOutResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Shotgun Laser Outline Color"), pColorValue: &g_Config.m_ClLaserShotgunOutlineColor, DefaultColor: ColorRGBA(0.125490f, 0.098039f, 0.043137f, 1.0f), CheckBoxSpacing: false);
2610 ColorHSLA LaserShotgunInnerColor = DoLine_ColorPicker(pResetId: &s_LaserShotgunInResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Shotgun Laser Inner Color"), pColorValue: &g_Config.m_ClLaserShotgunInnerColor, DefaultColor: ColorRGBA(0.570588f, 0.417647f, 0.252941f, 1.0f), CheckBoxSpacing: false);
2611
2612 // ***** Entities ***** //
2613 LeftView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &LeftView);
2614 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Entities"), FontSize: HeadlineFontSize,
2615 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2616 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2617
2618 // General entity laser settings
2619 static CButtonContainer s_LaserDoorOutResetId, s_LaserDoorInResetId, s_LaserFreezeOutResetId, s_LaserFreezeInResetId, s_LaserDraggerOutResetId, s_LaserDraggerInResetId;
2620
2621 ColorHSLA LaserDoorOutlineColor = DoLine_ColorPicker(pResetId: &s_LaserDoorOutResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Door Laser Outline Color"), pColorValue: &g_Config.m_ClLaserDoorOutlineColor, DefaultColor: ColorRGBA(0.0f, 0.131372f, 0.096078f, 1.0f), CheckBoxSpacing: false);
2622 ColorHSLA LaserDoorInnerColor = DoLine_ColorPicker(pResetId: &s_LaserDoorInResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Door Laser Inner Color"), pColorValue: &g_Config.m_ClLaserDoorInnerColor, DefaultColor: ColorRGBA(0.262745f, 0.760784f, 0.639215f, 1.0f), CheckBoxSpacing: false);
2623 ColorHSLA LaserFreezeOutlineColor = DoLine_ColorPicker(pResetId: &s_LaserFreezeOutResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Freeze Laser Outline Color"), pColorValue: &g_Config.m_ClLaserFreezeOutlineColor, DefaultColor: ColorRGBA(0.131372f, 0.123529f, 0.182352f, 1.0f), CheckBoxSpacing: false);
2624 ColorHSLA LaserFreezeInnerColor = DoLine_ColorPicker(pResetId: &s_LaserFreezeInResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Freeze Laser Inner Color"), pColorValue: &g_Config.m_ClLaserFreezeInnerColor, DefaultColor: ColorRGBA(0.482352f, 0.443137f, 0.564705f, 1.0f), CheckBoxSpacing: false);
2625 ColorHSLA LaserDraggerOutlineColor = DoLine_ColorPicker(pResetId: &s_LaserDraggerOutResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Dragger Outline Color"), pColorValue: &g_Config.m_ClLaserDraggerOutlineColor, DefaultColor: ColorRGBA(0.1640625f, 0.015625f, 0.015625f, 1.0f), CheckBoxSpacing: false);
2626 ColorHSLA LaserDraggerInnerColor = DoLine_ColorPicker(pResetId: &s_LaserDraggerInResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Dragger Inner Color"), pColorValue: &g_Config.m_ClLaserDraggerInnerColor, DefaultColor: ColorRGBA(.8666666f, .3725490f, .3725490f, 1.0f), CheckBoxSpacing: false);
2627
2628 static CButtonContainer s_AllToRifleResetId, s_AllToDefaultResetId;
2629
2630 LeftView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
2631 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2632 if(DoButton_Menu(pButtonContainer: &s_AllToRifleResetId, pText: Localize(pStr: "Set all to Rifle"), Checked: 0, pRect: &Button))
2633 {
2634 g_Config.m_ClLaserShotgunOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2635 g_Config.m_ClLaserShotgunInnerColor = g_Config.m_ClLaserRifleInnerColor;
2636 g_Config.m_ClLaserDoorOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2637 g_Config.m_ClLaserDoorInnerColor = g_Config.m_ClLaserRifleInnerColor;
2638 g_Config.m_ClLaserFreezeOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2639 g_Config.m_ClLaserFreezeInnerColor = g_Config.m_ClLaserRifleInnerColor;
2640 g_Config.m_ClLaserDraggerOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2641 g_Config.m_ClLaserDraggerInnerColor = g_Config.m_ClLaserRifleInnerColor;
2642 }
2643
2644 // values taken from the CL commands
2645 LeftView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
2646 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2647 if(DoButton_Menu(pButtonContainer: &s_AllToDefaultResetId, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &Button))
2648 {
2649 g_Config.m_ClLaserRifleOutlineColor = 11176233;
2650 g_Config.m_ClLaserRifleInnerColor = 11206591;
2651 g_Config.m_ClLaserShotgunOutlineColor = 1866773;
2652 g_Config.m_ClLaserShotgunInnerColor = 1467241;
2653 g_Config.m_ClLaserDoorOutlineColor = 7667473;
2654 g_Config.m_ClLaserDoorInnerColor = 7701379;
2655 g_Config.m_ClLaserFreezeOutlineColor = 11613223;
2656 g_Config.m_ClLaserFreezeInnerColor = 12001153;
2657 g_Config.m_ClLaserDraggerOutlineColor = 57618;
2658 g_Config.m_ClLaserDraggerInnerColor = 42398;
2659 }
2660
2661 // ***** Laser Preview ***** //
2662 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2663 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2664 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
2665
2666 const float LaserPreviewHeight = 60.0f;
2667 CUIRect LaserPreview;
2668 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2669 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2670 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserRifleOutlineColor, InnerColor: LaserRifleInnerColor, LaserType: LASERTYPE_RIFLE);
2671
2672 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2673 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2674 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserShotgunOutlineColor, InnerColor: LaserShotgunInnerColor, LaserType: LASERTYPE_SHOTGUN);
2675
2676 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2677 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2678 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserDoorOutlineColor, InnerColor: LaserDoorInnerColor, LaserType: LASERTYPE_DOOR);
2679
2680 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2681 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2682 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserFreezeOutlineColor, InnerColor: LaserFreezeInnerColor, LaserType: LASERTYPE_FREEZE);
2683
2684 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2685 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2686 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserDraggerOutlineColor, InnerColor: LaserDraggerInnerColor, LaserType: LASERTYPE_DRAGGER);
2687 }
2688}
2689
2690void CMenus::RenderSettingsDDNet(CUIRect MainView)
2691{
2692 CUIRect Button, Left, Right, LeftLeft, Label;
2693
2694#if defined(CONF_AUTOUPDATE)
2695 CUIRect UpdaterRect;
2696 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &UpdaterRect);
2697 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
2698#endif
2699
2700 // demo
2701 CUIRect Demo;
2702 MainView.HSplitTop(Cut: 110.0f, pTop: &Demo, pBottom: &MainView);
2703 Demo.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Demo);
2704 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Demo"), Size: 20.0f, Align: TEXTALIGN_ML);
2705 Label.VSplitMid(pLeft: nullptr, pRight: &Label, Spacing: 20.0f);
2706 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Ghost"), Size: 20.0f, Align: TEXTALIGN_ML);
2707
2708 Demo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Demo);
2709 Demo.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
2710
2711 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2712 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoRaceRecord, pText: Localize(pStr: "Save the best demo of each race"), Checked: g_Config.m_ClAutoRaceRecord, pRect: &Button))
2713 {
2714 g_Config.m_ClAutoRaceRecord ^= 1;
2715 }
2716
2717 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2718 if(DoButton_CheckBox(pId: &g_Config.m_ClReplays, pText: Localize(pStr: "Enable replays"), Checked: g_Config.m_ClReplays, pRect: &Button))
2719 {
2720 g_Config.m_ClReplays ^= 1;
2721 if(Client()->State() == IClient::STATE_ONLINE)
2722 {
2723 Client()->DemoRecorder_UpdateReplayRecorder();
2724 }
2725 }
2726
2727 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2728 if(g_Config.m_ClReplays)
2729 Ui()->DoScrollbarOption(pId: &g_Config.m_ClReplayLength, pOption: &g_Config.m_ClReplayLength, pRect: &Button, pStr: Localize(pStr: "Default length"), Min: 10, Max: 600, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
2730
2731 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2732 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhost, pText: Localize(pStr: "Enable ghost"), Checked: g_Config.m_ClRaceGhost, pRect: &Button))
2733 {
2734 g_Config.m_ClRaceGhost ^= 1;
2735 }
2736 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClRaceGhost, pNearRect: &Button, pText: Localize(pStr: "When you cross the start line, show a ghost tee replicating the movements of your best time"));
2737
2738 if(g_Config.m_ClRaceGhost)
2739 {
2740 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2741 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2742 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceShowGhost, pText: Localize(pStr: "Show ghost"), Checked: g_Config.m_ClRaceShowGhost, pRect: &LeftLeft))
2743 {
2744 g_Config.m_ClRaceShowGhost ^= 1;
2745 }
2746 Ui()->DoScrollbarOption(pId: &g_Config.m_ClRaceGhostAlpha, pOption: &g_Config.m_ClRaceGhostAlpha, pRect: &Button, pStr: Localize(pStr: "Opacity"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: 0u, pSuffix: "%");
2747
2748 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2749 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceSaveGhost, pText: Localize(pStr: "Save ghost"), Checked: g_Config.m_ClRaceSaveGhost, pRect: &Button))
2750 {
2751 g_Config.m_ClRaceSaveGhost ^= 1;
2752 }
2753
2754 if(g_Config.m_ClRaceSaveGhost)
2755 {
2756 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2757 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhostSaveBest, pText: Localize(pStr: "Only save improvements"), Checked: g_Config.m_ClRaceGhostSaveBest, pRect: &Button))
2758 {
2759 g_Config.m_ClRaceGhostSaveBest ^= 1;
2760 }
2761 }
2762 }
2763
2764 // gameplay
2765 CUIRect Gameplay;
2766 MainView.HSplitTop(Cut: 170.0f, pTop: &Gameplay, pBottom: &MainView);
2767 Gameplay.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Gameplay);
2768 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Gameplay"), Size: 20.0f, Align: TEXTALIGN_ML);
2769 Gameplay.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Gameplay);
2770 Gameplay.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
2771
2772 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2773 Ui()->DoScrollbarOption(pId: &g_Config.m_ClOverlayEntities, pOption: &g_Config.m_ClOverlayEntities, pRect: &Button, pStr: Localize(pStr: "Overlay entities"), Min: 0, Max: 100);
2774
2775 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2776 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2777
2778 if(DoButton_CheckBox(pId: &g_Config.m_ClTextEntities, pText: Localize(pStr: "Show text entities"), Checked: g_Config.m_ClTextEntities, pRect: &LeftLeft))
2779 g_Config.m_ClTextEntities ^= 1;
2780
2781 if(g_Config.m_ClTextEntities)
2782 {
2783 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClTextEntitiesSize, pOption: &g_Config.m_ClTextEntitiesSize, pRect: &Button, pStr: Localize(pStr: "Size"), Min: 20, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_DELAYUPDATE))
2784 GameClient()->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
2785 }
2786
2787 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2788 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2789
2790 if(DoButton_CheckBox(pId: &g_Config.m_ClShowOthers, pText: Localize(pStr: "Show others"), Checked: g_Config.m_ClShowOthers == SHOW_OTHERS_ON, pRect: &LeftLeft))
2791 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF;
2792
2793 Ui()->DoScrollbarOption(pId: &g_Config.m_ClShowOthersAlpha, pOption: &g_Config.m_ClShowOthersAlpha, pRect: &Button, pStr: Localize(pStr: "Opacity"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: 0u, pSuffix: "%");
2794
2795 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowOthersAlpha, pNearRect: &Button, pText: Localize(pStr: "Adjust the opacity of entities belonging to other teams, such as tees and name plates"));
2796
2797 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2798 static int s_ShowOwnTeamId = 0;
2799 if(DoButton_CheckBox(pId: &s_ShowOwnTeamId, pText: Localize(pStr: "Show others (own team only)"), Checked: g_Config.m_ClShowOthers == SHOW_OTHERS_ONLY_TEAM, pRect: &Button))
2800 {
2801 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ONLY_TEAM ? SHOW_OTHERS_ONLY_TEAM : SHOW_OTHERS_OFF;
2802 }
2803
2804 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2805 if(DoButton_CheckBox(pId: &g_Config.m_ClShowQuads, pText: Localize(pStr: "Show background quads"), Checked: g_Config.m_ClShowQuads, pRect: &Button))
2806 {
2807 g_Config.m_ClShowQuads ^= 1;
2808 }
2809 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowQuads, pNearRect: &Button, pText: Localize(pStr: "Quads are used for background decoration"));
2810
2811 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2812 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClDefaultZoom, pOption: &g_Config.m_ClDefaultZoom, pRect: &Button, pStr: Localize(pStr: "Default zoom"), Min: 0, Max: 20))
2813 GameClient()->m_Camera.SetZoom(Target: CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: true);
2814
2815 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2816 Ui()->DoScrollbarOption(pId: &g_Config.m_ClPredictionMargin, pOption: &g_Config.m_ClPredictionMargin, pRect: &Button, pStr: Localize(pStr: "Prediction margin"), Min: 1, Max: 300);
2817
2818 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2819 if(DoButton_CheckBox(pId: &g_Config.m_ClPredictEvents, pText: Localize(pStr: "Predict events (experimental)"), Checked: g_Config.m_ClPredictEvents, pRect: &Button))
2820 {
2821 g_Config.m_ClPredictEvents ^= 1;
2822 }
2823
2824 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2825 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPing, pText: Localize(pStr: "AntiPing"), Checked: g_Config.m_ClAntiPing, pRect: &Button))
2826 {
2827 g_Config.m_ClAntiPing ^= 1;
2828 }
2829 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClAntiPing, pNearRect: &Button, pText: Localize(pStr: "Tries to predict other entities to give a feel of low latency"));
2830
2831 if(g_Config.m_ClAntiPing)
2832 {
2833 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2834 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingPlayers, pText: Localize(pStr: "AntiPing: predict other players"), Checked: g_Config.m_ClAntiPingPlayers, pRect: &Button))
2835 {
2836 g_Config.m_ClAntiPingPlayers ^= 1;
2837 }
2838
2839 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2840 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingWeapons, pText: Localize(pStr: "AntiPing: predict weapons"), Checked: g_Config.m_ClAntiPingWeapons, pRect: &Button))
2841 {
2842 g_Config.m_ClAntiPingWeapons ^= 1;
2843 }
2844
2845 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2846 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingGrenade, pText: Localize(pStr: "AntiPing: predict grenade paths"), Checked: g_Config.m_ClAntiPingGrenade, pRect: &Button))
2847 {
2848 g_Config.m_ClAntiPingGrenade ^= 1;
2849 }
2850 }
2851
2852 CUIRect Background, Miscellaneous;
2853 MainView.VSplitMid(pLeft: &Background, pRight: &Miscellaneous, Spacing: 20.0f);
2854
2855 // background
2856 Background.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Background);
2857 Background.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Background);
2858 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Background"), Size: 20.0f, Align: TEXTALIGN_ML);
2859
2860 ColorRGBA GreyDefault(0.5f, 0.5f, 0.5f, 1);
2861
2862 static CButtonContainer s_ResetId1;
2863 DoLine_ColorPicker(pResetId: &s_ResetId1, LineSize: 25.0f, LabelSize: 13.0f, BottomMargin: 5.0f, pMainRect: &Background, pText: Localize(pStr: "Regular background color"), pColorValue: &g_Config.m_ClBackgroundColor, DefaultColor: GreyDefault, CheckBoxSpacing: false);
2864
2865 static CButtonContainer s_ResetId2;
2866 DoLine_ColorPicker(pResetId: &s_ResetId2, LineSize: 25.0f, LabelSize: 13.0f, BottomMargin: 5.0f, pMainRect: &Background, pText: Localize(pStr: "Entities background color"), pColorValue: &g_Config.m_ClBackgroundEntitiesColor, DefaultColor: GreyDefault, CheckBoxSpacing: false);
2867
2868 CUIRect EditBox, ReloadButton;
2869 Background.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Background);
2870 Background.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Background);
2871 Label.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &EditBox);
2872 EditBox.VSplitRight(Cut: 60.0f, pLeft: &EditBox, pRight: &Button);
2873 Button.VSplitMid(pLeft: &ReloadButton, pRight: &Button, Spacing: 5.0f);
2874 EditBox.VSplitRight(Cut: 5.0f, pLeft: &EditBox, pRight: nullptr);
2875
2876 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Map"), Size: 14.0f, Align: TEXTALIGN_ML);
2877
2878 static CLineInput s_BackgroundEntitiesInput(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities));
2879 Ui()->DoEditBox(pLineInput: &s_BackgroundEntitiesInput, pRect: &EditBox, FontSize: 14.0f);
2880
2881 static CButtonContainer s_BackgroundEntitiesMapPicker, s_BackgroundEntitiesReload;
2882
2883 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesReload, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &ReloadButton, Flags: BUTTONFLAG_LEFT))
2884 {
2885 GameClient()->m_Background.LoadBackground();
2886 }
2887
2888 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesMapPicker, pText: FontIcon::FOLDER, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
2889 {
2890 static SPopupMenuId s_PopupMapPickerId;
2891 static CPopupMapPickerContext s_PopupMapPickerContext;
2892 s_PopupMapPickerContext.m_pMenus = this;
2893 s_PopupMapPickerContext.MapListPopulate();
2894 Ui()->DoPopupMenu(pId: &s_PopupMapPickerId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 300.0f, Height: 250.0f, pContext: &s_PopupMapPickerContext, pfnFunc: PopupMapPicker);
2895 }
2896
2897 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
2898 const bool UseCurrentMap = str_comp(a: g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0;
2899 static int s_UseCurrentMapId = 0;
2900 if(DoButton_CheckBox(pId: &s_UseCurrentMapId, pText: Localize(pStr: "Use current map as background"), Checked: UseCurrentMap, pRect: &Button))
2901 {
2902 if(UseCurrentMap)
2903 g_Config.m_ClBackgroundEntities[0] = '\0';
2904 else
2905 str_copy(dst&: g_Config.m_ClBackgroundEntities, CURRENT_MAP);
2906 GameClient()->m_Background.LoadBackground();
2907 }
2908
2909 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
2910 if(DoButton_CheckBox(pId: &g_Config.m_ClBackgroundShowTilesLayers, pText: Localize(pStr: "Show tiles layers from BG map"), Checked: g_Config.m_ClBackgroundShowTilesLayers, pRect: &Button))
2911 g_Config.m_ClBackgroundShowTilesLayers ^= 1;
2912
2913 // miscellaneous
2914 Miscellaneous.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Miscellaneous);
2915 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
2916
2917 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Miscellaneous"), Size: 20.0f, Align: TEXTALIGN_ML);
2918
2919 static CButtonContainer s_ButtonTimeout;
2920 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
2921 if(DoButton_Menu(pButtonContainer: &s_ButtonTimeout, pText: Localize(pStr: "New random timeout code"), Checked: 0, pRect: &Button))
2922 {
2923 Client()->GenerateTimeoutSeed();
2924 }
2925
2926 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
2927 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Miscellaneous);
2928 Miscellaneous.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Miscellaneous);
2929 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Run on join"), Size: 14.0f, Align: TEXTALIGN_ML);
2930 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
2931 static CLineInput s_RunOnJoinInput(g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin));
2932 s_RunOnJoinInput.SetEmptyText(Localize(pStr: "Chat command (e.g. showall 1)"));
2933 Ui()->DoEditBox(pLineInput: &s_RunOnJoinInput, pRect: &Button, FontSize: 14.0f);
2934
2935#if defined(CONF_FAMILY_WINDOWS)
2936 static CButtonContainer s_ButtonUnregisterShell;
2937 Miscellaneous.HSplitTop(10.0f, nullptr, &Miscellaneous);
2938 Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous);
2939 if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button))
2940 {
2941 Client()->ShellUnregister();
2942 }
2943#endif
2944
2945 // Updater
2946#if defined(CONF_AUTOUPDATE)
2947 {
2948 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
2949 IUpdater::EUpdaterState State = Updater()->GetCurrentState();
2950
2951 // Update Button
2952 char aBuf[256];
2953 if(NeedUpdate && State <= IUpdater::CLEAN)
2954 {
2955 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is available:"), Client()->LatestVersion());
2956 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
2957 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
2958 static CButtonContainer s_ButtonUpdate;
2959 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &Button))
2960 {
2961 Updater()->InitiateUpdate();
2962 }
2963 }
2964 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
2965 str_copy(dst&: aBuf, src: Localize(pStr: "Updating…"));
2966 else if(State == IUpdater::NEED_RESTART)
2967 {
2968 str_copy(dst&: aBuf, src: Localize(pStr: "DDNet Client updated!"));
2969 m_NeedRestartUpdate = true;
2970 }
2971 else
2972 {
2973 str_copy(dst&: aBuf, src: Localize(pStr: "No updates available"));
2974 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
2975 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
2976 static CButtonContainer s_ButtonUpdate;
2977 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Check now"), Checked: 0, pRect: &Button))
2978 {
2979 Client()->RequestDDNetInfo();
2980 }
2981 }
2982 Ui()->DoLabel(pRect: &UpdaterRect, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
2983 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2984 }
2985#endif
2986}
2987
2988CUi::EPopupMenuFunctionResult CMenus::PopupMapPicker(void *pContext, CUIRect View, bool Active)
2989{
2990 CPopupMapPickerContext *pPopupContext = static_cast<CPopupMapPickerContext *>(pContext);
2991 CMenus *pMenus = pPopupContext->m_pMenus;
2992
2993 static CListBox s_ListBox;
2994 s_ListBox.SetActive(Active);
2995 s_ListBox.DoStart(RowHeight: 20.0f, NumItems: pPopupContext->m_vMaps.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false);
2996
2997 int MapIndex = 0;
2998 for(auto &Map : pPopupContext->m_vMaps)
2999 {
3000 MapIndex++;
3001 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Map, Selected: MapIndex == pPopupContext->m_Selection);
3002 if(!Item.m_Visible)
3003 continue;
3004
3005 CUIRect Label, Icon;
3006 Item.m_Rect.VSplitLeft(Cut: 20.0f, pLeft: &Icon, pRight: &Label);
3007
3008 char aLabelText[IO_MAX_PATH_LENGTH];
3009 str_copy(dst&: aLabelText, src: Map.m_aFilename);
3010 if(Map.m_IsDirectory)
3011 str_append(dst&: aLabelText, src: "/");
3012
3013 const char *pIconType;
3014 if(!Map.m_IsDirectory)
3015 {
3016 pIconType = FontIcon::MAP;
3017 }
3018 else
3019 {
3020 if(!str_comp(a: Map.m_aFilename, b: ".."))
3021 pIconType = FontIcon::FOLDER_TREE;
3022 else
3023 pIconType = FontIcon::FOLDER;
3024 }
3025
3026 pMenus->TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
3027 pMenus->TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
3028 pMenus->Ui()->DoLabel(pRect: &Icon, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
3029 pMenus->TextRender()->SetRenderFlags(0);
3030 pMenus->TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
3031
3032 pMenus->Ui()->DoLabel(pRect: &Label, pText: aLabelText, Size: 10.0f, Align: TEXTALIGN_ML);
3033 }
3034
3035 const int NewSelected = s_ListBox.DoEnd();
3036 pPopupContext->m_Selection = NewSelected >= 0 ? NewSelected : -1;
3037 if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated())
3038 {
3039 const CMapListItem &SelectedItem = pPopupContext->m_vMaps[pPopupContext->m_Selection];
3040
3041 if(SelectedItem.m_IsDirectory)
3042 {
3043 if(!str_comp(a: SelectedItem.m_aFilename, b: ".."))
3044 {
3045 fs_parent_dir(path: pPopupContext->m_aCurrentMapFolder);
3046 }
3047 else
3048 {
3049 str_append(dst&: pPopupContext->m_aCurrentMapFolder, src: "/");
3050 str_append(dst&: pPopupContext->m_aCurrentMapFolder, src: SelectedItem.m_aFilename);
3051 }
3052 pPopupContext->MapListPopulate();
3053 }
3054 else
3055 {
3056 str_format(buffer: g_Config.m_ClBackgroundEntities, buffer_size: sizeof(g_Config.m_ClBackgroundEntities), format: "%s/%s", pPopupContext->m_aCurrentMapFolder, SelectedItem.m_aFilename);
3057 pMenus->GameClient()->m_Background.LoadBackground();
3058 return CUi::POPUP_CLOSE_CURRENT;
3059 }
3060 }
3061
3062 return CUi::POPUP_KEEP_OPEN;
3063}
3064
3065void CMenus::CPopupMapPickerContext::MapListPopulate()
3066{
3067 m_vMaps.clear();
3068 char aTemp[IO_MAX_PATH_LENGTH];
3069 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "maps/%s", m_aCurrentMapFolder);
3070 m_pMenus->Storage()->ListDirectoryInfo(Type: IStorage::TYPE_ALL, pPath: aTemp, pfnCallback: MapListFetchCallback, pUser: this);
3071 std::stable_sort(first: m_vMaps.begin(), last: m_vMaps.end(), comp: CompareFilenameAscending);
3072 m_Selection = -1;
3073}
3074
3075int CMenus::CPopupMapPickerContext::MapListFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
3076{
3077 CPopupMapPickerContext *pRealUser = (CPopupMapPickerContext *)pUser;
3078 if((!IsDir && !str_endswith(str: pInfo->m_pName, suffix: ".map")) || !str_comp(a: pInfo->m_pName, b: ".") || (!str_comp(a: pInfo->m_pName, b: "..") && (!str_comp(a: pRealUser->m_aCurrentMapFolder, b: ""))))
3079 return 0;
3080
3081 CMapListItem Item;
3082 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
3083 Item.m_IsDirectory = IsDir;
3084
3085 pRealUser->m_vMaps.emplace_back(args&: Item);
3086
3087 return 0;
3088}
3089