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 bit (%d:%d)", Localize(pStr: "Current"), (int)(g_Config.m_GfxScreenWidth * Graphics()->ScreenHiDPIScale()), (int)(g_Config.m_GfxScreenHeight * Graphics()->ScreenHiDPIScale()), g_Config.m_GfxScreenRefreshRate, g_Config.m_GfxColorDepth, g_Config.m_GfxScreenWidth / G, g_Config.m_GfxScreenHeight / G);
893 Ui()->DoLabel(pRect: &ModeLabel, pText: aBuf, Size: sc_FontSizeResListHeader, Align: TEXTALIGN_MC);
894 }
895
896 int OldSelected = -1;
897 s_ListBox.SetActive(!Ui()->IsPopupOpen());
898 s_ListBox.DoStart(RowHeight: sc_RowHeightResList, NumItems: s_NumNodes, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: OldSelected, pRect: &ModeList);
899
900 for(int i = 0; i < s_NumNodes; ++i)
901 {
902 const int Depth = s_aModes[i].m_Red + s_aModes[i].m_Green + s_aModes[i].m_Blue > 16 ? 24 : 16;
903 if(g_Config.m_GfxColorDepth == Depth &&
904 g_Config.m_GfxScreenWidth == s_aModes[i].m_WindowWidth &&
905 g_Config.m_GfxScreenHeight == s_aModes[i].m_WindowHeight &&
906 g_Config.m_GfxScreenRefreshRate == s_aModes[i].m_RefreshRate)
907 {
908 OldSelected = i;
909 }
910
911 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_aModes[i], Selected: OldSelected == i);
912 if(!Item.m_Visible)
913 continue;
914
915 int G = std::gcd(m: s_aModes[i].m_WindowWidth, n: s_aModes[i].m_WindowHeight);
916 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " %dx%d @%dhz %d bit (%d:%d)", s_aModes[i].m_CanvasWidth, s_aModes[i].m_CanvasHeight, s_aModes[i].m_RefreshRate, Depth, s_aModes[i].m_WindowWidth / G, s_aModes[i].m_WindowHeight / G);
917 Ui()->DoLabel(pRect: &Item.m_Rect, pText: aBuf, Size: sc_FontSizeResList, Align: TEXTALIGN_ML);
918 }
919
920 const int NewSelected = s_ListBox.DoEnd();
921 if(OldSelected != NewSelected)
922 {
923 const int Depth = s_aModes[NewSelected].m_Red + s_aModes[NewSelected].m_Green + s_aModes[NewSelected].m_Blue > 16 ? 24 : 16;
924 g_Config.m_GfxColorDepth = Depth;
925 g_Config.m_GfxScreenWidth = s_aModes[NewSelected].m_WindowWidth;
926 g_Config.m_GfxScreenHeight = s_aModes[NewSelected].m_WindowHeight;
927 g_Config.m_GfxScreenRefreshRate = s_aModes[NewSelected].m_RefreshRate;
928 Graphics()->ResizeToScreen();
929 }
930
931 // switches
932 CUIRect WindowModeDropDown;
933 MainView.HSplitTop(Cut: 20.0f, pTop: &WindowModeDropDown, pBottom: &MainView);
934
935 const char *apWindowModes[] = {Localize(pStr: "Windowed"), Localize(pStr: "Windowed borderless"), Localize(pStr: "Windowed fullscreen"), Localize(pStr: "Desktop fullscreen"), Localize(pStr: "Fullscreen")};
936 static const int s_NumWindowMode = std::size(apWindowModes);
937
938 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));
939
940 static CUi::SDropDownState s_WindowModeDropDownState;
941 static CScrollRegion s_WindowModeDropDownScrollRegion;
942 s_WindowModeDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_WindowModeDropDownScrollRegion;
943 const int NewWindowMode = Ui()->DoDropDown(pRect: &WindowModeDropDown, CurSelection: OldWindowMode, pStrs: apWindowModes, Num: s_NumWindowMode, State&: s_WindowModeDropDownState);
944 if(OldWindowMode != NewWindowMode)
945 {
946 if(NewWindowMode == 0)
947 Graphics()->SetWindowParams(FullscreenMode: 0, IsBorderless: false);
948 else if(NewWindowMode == 1)
949 Graphics()->SetWindowParams(FullscreenMode: 0, IsBorderless: true);
950 else if(NewWindowMode == 2)
951 Graphics()->SetWindowParams(FullscreenMode: 3, IsBorderless: false);
952 else if(NewWindowMode == 3)
953 Graphics()->SetWindowParams(FullscreenMode: 2, IsBorderless: false);
954 else if(NewWindowMode == 4)
955 Graphics()->SetWindowParams(FullscreenMode: 1, IsBorderless: false);
956 }
957
958 if(Graphics()->GetNumScreens() > 1)
959 {
960 CUIRect ScreenDropDown;
961 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
962 MainView.HSplitTop(Cut: 20.0f, pTop: &ScreenDropDown, pBottom: &MainView);
963
964 const int NumScreens = Graphics()->GetNumScreens();
965 static std::vector<std::string> s_vScreenNames;
966 static std::vector<const char *> s_vpScreenNames;
967 s_vScreenNames.resize(sz: NumScreens);
968 s_vpScreenNames.resize(sz: NumScreens);
969
970 for(int i = 0; i < NumScreens; ++i)
971 {
972 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d: %s", Localize(pStr: "Screen"), i, Graphics()->GetScreenName(Screen: i));
973 s_vScreenNames[i] = aBuf;
974 s_vpScreenNames[i] = s_vScreenNames[i].c_str();
975 }
976
977 static CUi::SDropDownState s_ScreenDropDownState;
978 static CScrollRegion s_ScreenDropDownScrollRegion;
979 s_ScreenDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ScreenDropDownScrollRegion;
980 const int NewScreen = Ui()->DoDropDown(pRect: &ScreenDropDown, CurSelection: g_Config.m_GfxScreen, pStrs: s_vpScreenNames.data(), Num: s_vpScreenNames.size(), State&: s_ScreenDropDownState);
981 if(NewScreen != g_Config.m_GfxScreen)
982 Graphics()->SwitchWindowScreen(Index: NewScreen, MoveToCenter: true);
983 }
984
985 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
986 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
987 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "V-Sync"), Localize(pStr: "may cause delay"));
988 if(DoButton_CheckBox(pId: &g_Config.m_GfxVsync, pText: aBuf, Checked: g_Config.m_GfxVsync, pRect: &Button))
989 {
990 Graphics()->SetVSync(!g_Config.m_GfxVsync);
991 }
992
993 bool MultiSamplingChanged = false;
994 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
995 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "FSAA samples"), Localize(pStr: "may cause delay"));
996 int GfxFsaaSamplesMouseButton = DoButton_CheckBox_Number(pId: &g_Config.m_GfxFsaaSamples, pText: aBuf, Checked: g_Config.m_GfxFsaaSamples, pRect: &Button);
997 int CurFSAA = g_Config.m_GfxFsaaSamples == 0 ? 1 : g_Config.m_GfxFsaaSamples;
998 if(GfxFsaaSamplesMouseButton == 1) // inc
999 {
1000 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) + 1);
1001 if(g_Config.m_GfxFsaaSamples > 64)
1002 g_Config.m_GfxFsaaSamples = 0;
1003 MultiSamplingChanged = true;
1004 }
1005 else if(GfxFsaaSamplesMouseButton == 2) // dec
1006 {
1007 if(CurFSAA == 1)
1008 g_Config.m_GfxFsaaSamples = 64;
1009 else if(CurFSAA == 2)
1010 g_Config.m_GfxFsaaSamples = 0;
1011 else
1012 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) - 1);
1013 MultiSamplingChanged = true;
1014 }
1015
1016 uint32_t MultiSamplingCountBackend = 0;
1017 if(MultiSamplingChanged)
1018 {
1019 if(Graphics()->SetMultiSampling(ReqMultiSamplingCount: g_Config.m_GfxFsaaSamples, MultiSamplingCountBackend))
1020 {
1021 // try again with 0 if mouse click was increasing multi sampling
1022 // else just accept the current value as is
1023 if((uint32_t)g_Config.m_GfxFsaaSamples > MultiSamplingCountBackend && GfxFsaaSamplesMouseButton == 1)
1024 Graphics()->SetMultiSampling(ReqMultiSamplingCount: 0, MultiSamplingCountBackend);
1025 g_Config.m_GfxFsaaSamples = (int)MultiSamplingCountBackend;
1026 }
1027 else
1028 {
1029 CheckSettings = true;
1030 }
1031 }
1032
1033 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1034 if(DoButton_CheckBox(pId: &g_Config.m_GfxHighDetail, pText: Localize(pStr: "High Detail"), Checked: g_Config.m_GfxHighDetail, pRect: &Button))
1035 g_Config.m_GfxHighDetail ^= 1;
1036 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_GfxHighDetail, pNearRect: &Button, pText: Localize(pStr: "Allows maps to render with more detail"));
1037
1038 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1039 if(DoButton_CheckBox(pId: &g_Config.m_ClShowfps, pText: Localize(pStr: "Show FPS"), Checked: g_Config.m_ClShowfps, pRect: &Button))
1040 g_Config.m_ClShowfps ^= 1;
1041 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowfps, pNearRect: &Button, pText: Localize(pStr: "Renders your frame rate in the top right"));
1042
1043 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1044 str_copy(dst&: aBuf, src: " ");
1045 str_append(dst&: aBuf, src: Localize(pStr: "Hz", pContext: "Hertz"));
1046 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);
1047
1048 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1049 static CButtonContainer s_UiColorResetId;
1050 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);
1051
1052 // Backend list
1053 struct SMenuBackendInfo
1054 {
1055 int m_Major = 0;
1056 int m_Minor = 0;
1057 int m_Patch = 0;
1058 const char *m_pBackendName = "";
1059 bool m_Found = false;
1060 };
1061 std::array<std::array<SMenuBackendInfo, EGraphicsDriverAgeType::GRAPHICS_DRIVER_AGE_TYPE_COUNT>, EBackendType::BACKEND_TYPE_COUNT> aaSupportedBackends{};
1062 uint32_t FoundBackendCount = 0;
1063 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1064 {
1065 if(EBackendType(i) == BACKEND_TYPE_AUTO)
1066 continue;
1067 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1068 {
1069 auto &Info = aaSupportedBackends[i][n];
1070 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)))
1071 {
1072 // don't count blocked opengl drivers
1073 if(EBackendType(i) != BACKEND_TYPE_OPENGL || EGraphicsDriverAgeType(n) == GRAPHICS_DRIVER_AGE_TYPE_LEGACY || g_Config.m_GfxDriverIsBlocked == 0)
1074 {
1075 Info.m_Found = true;
1076 ++FoundBackendCount;
1077 }
1078 }
1079 }
1080 }
1081
1082 if(FoundBackendCount > 1)
1083 {
1084 CUIRect Text, BackendDropDown;
1085 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1086 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1087 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1088 MainView.HSplitTop(Cut: 20.0f, pTop: &BackendDropDown, pBottom: &MainView);
1089 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Renderer"), Size: 16.0f, Align: TEXTALIGN_MC);
1090
1091 static std::vector<std::string> s_vBackendIdNames;
1092 static std::vector<const char *> s_vpBackendIdNamesCStr;
1093 static std::vector<SMenuBackendInfo> s_vBackendInfos;
1094
1095 size_t BackendCount = FoundBackendCount + 1;
1096 s_vBackendIdNames.resize(sz: BackendCount);
1097 s_vpBackendIdNamesCStr.resize(sz: BackendCount);
1098 s_vBackendInfos.resize(sz: BackendCount);
1099
1100 char aTmpBackendName[256];
1101
1102 auto IsInfoDefault = [](const SMenuBackendInfo &CheckInfo) {
1103 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;
1104 };
1105
1106 int OldSelectedBackend = -1;
1107 uint32_t CurCounter = 0;
1108 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1109 {
1110 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1111 {
1112 auto &Info = aaSupportedBackends[i][n];
1113 if(Info.m_Found)
1114 {
1115 bool IsDefault = IsInfoDefault(Info);
1116 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") : "");
1117 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1118 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1119 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)
1120 {
1121 OldSelectedBackend = CurCounter;
1122 }
1123
1124 s_vBackendInfos[CurCounter] = Info;
1125 ++CurCounter;
1126 }
1127 }
1128 }
1129
1130 if(OldSelectedBackend != -1)
1131 {
1132 // no custom selected
1133 BackendCount -= 1;
1134 }
1135 else
1136 {
1137 // custom selected one
1138 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);
1139 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1140 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1141 OldSelectedBackend = CurCounter;
1142
1143 s_vBackendInfos[CurCounter].m_pBackendName = "custom";
1144 s_vBackendInfos[CurCounter].m_Major = g_Config.m_GfxGLMajor;
1145 s_vBackendInfos[CurCounter].m_Minor = g_Config.m_GfxGLMinor;
1146 s_vBackendInfos[CurCounter].m_Patch = g_Config.m_GfxGLPatch;
1147 }
1148
1149 static int s_OldSelectedBackend = -1;
1150 if(s_OldSelectedBackend == -1)
1151 s_OldSelectedBackend = OldSelectedBackend;
1152
1153 static CUi::SDropDownState s_BackendDropDownState;
1154 static CScrollRegion s_BackendDropDownScrollRegion;
1155 s_BackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_BackendDropDownScrollRegion;
1156 const int NewBackend = Ui()->DoDropDown(pRect: &BackendDropDown, CurSelection: OldSelectedBackend, pStrs: s_vpBackendIdNamesCStr.data(), Num: BackendCount, State&: s_BackendDropDownState);
1157 if(OldSelectedBackend != NewBackend)
1158 {
1159 str_copy(dst&: g_Config.m_GfxBackend, src: s_vBackendInfos[NewBackend].m_pBackendName);
1160 g_Config.m_GfxGLMajor = s_vBackendInfos[NewBackend].m_Major;
1161 g_Config.m_GfxGLMinor = s_vBackendInfos[NewBackend].m_Minor;
1162 g_Config.m_GfxGLPatch = s_vBackendInfos[NewBackend].m_Patch;
1163
1164 CheckSettings = true;
1165 s_GfxBackendChanged = s_OldSelectedBackend != NewBackend;
1166 }
1167 }
1168
1169 // GPU list
1170 const auto &GpuList = Graphics()->GetGpus();
1171 if(GpuList.m_vGpus.size() > 1)
1172 {
1173 CUIRect Text, GpuDropDown;
1174 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1175 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1176 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1177 MainView.HSplitTop(Cut: 20.0f, pTop: &GpuDropDown, pBottom: &MainView);
1178 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Graphics card"), Size: 16.0f, Align: TEXTALIGN_MC);
1179
1180 static std::vector<const char *> s_vpGpuIdNames;
1181
1182 size_t GpuCount = GpuList.m_vGpus.size() + 1;
1183 s_vpGpuIdNames.resize(sz: GpuCount);
1184
1185 char aCurDeviceName[256 + 4];
1186
1187 int OldSelectedGpu = -1;
1188 for(size_t i = 0; i < GpuCount; ++i)
1189 {
1190 if(i == 0)
1191 {
1192 str_format(buffer: aCurDeviceName, buffer_size: sizeof(aCurDeviceName), format: "%s (%s)", Localize(pStr: "auto"), GpuList.m_AutoGpu.m_aName);
1193 s_vpGpuIdNames[i] = aCurDeviceName;
1194 if(str_comp(a: "auto", b: g_Config.m_GfxGpuName) == 0)
1195 {
1196 OldSelectedGpu = 0;
1197 }
1198 }
1199 else
1200 {
1201 s_vpGpuIdNames[i] = GpuList.m_vGpus[i - 1].m_aName;
1202 if(str_comp(a: GpuList.m_vGpus[i - 1].m_aName, b: g_Config.m_GfxGpuName) == 0)
1203 {
1204 OldSelectedGpu = i;
1205 }
1206 }
1207 }
1208
1209 static int s_OldSelectedGpu = -1;
1210 if(s_OldSelectedGpu == -1)
1211 s_OldSelectedGpu = OldSelectedGpu;
1212
1213 static CUi::SDropDownState s_GpuDropDownState;
1214 static CScrollRegion s_GpuDropDownScrollRegion;
1215 s_GpuDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_GpuDropDownScrollRegion;
1216 const int NewGpu = Ui()->DoDropDown(pRect: &GpuDropDown, CurSelection: OldSelectedGpu, pStrs: s_vpGpuIdNames.data(), Num: GpuCount, State&: s_GpuDropDownState);
1217 if(OldSelectedGpu != NewGpu)
1218 {
1219 if(NewGpu == 0)
1220 str_copy(dst&: g_Config.m_GfxGpuName, src: "auto");
1221 else
1222 str_copy(dst&: g_Config.m_GfxGpuName, src: GpuList.m_vGpus[NewGpu - 1].m_aName);
1223 CheckSettings = true;
1224 s_GfxGpuChanged = NewGpu != s_OldSelectedGpu;
1225 }
1226 }
1227
1228 // check if the new settings require a restart
1229 if(CheckSettings)
1230 {
1231 m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples &&
1232 !s_GfxBackendChanged &&
1233 !s_GfxGpuChanged);
1234 }
1235}
1236
1237void CMenus::RenderSettingsSound(CUIRect MainView)
1238{
1239 CUIRect Button;
1240 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1241 if(DoButton_CheckBox(pId: &g_Config.m_SndEnable, pText: Localize(pStr: "Use sounds"), Checked: g_Config.m_SndEnable, pRect: &Button))
1242 {
1243 g_Config.m_SndEnable ^= 1;
1244 UpdateMusicState();
1245 }
1246
1247 m_NeedRestartSound = g_Config.m_SndEnable && !Sound()->IsSoundEnabled();
1248
1249 if(!g_Config.m_SndEnable)
1250 return;
1251
1252 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1253 if(DoButton_CheckBox(pId: &g_Config.m_SndMusic, pText: Localize(pStr: "Play background music"), Checked: g_Config.m_SndMusic, pRect: &Button))
1254 {
1255 g_Config.m_SndMusic ^= 1;
1256 UpdateMusicState();
1257 }
1258
1259 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1260 if(DoButton_CheckBox(pId: &g_Config.m_SndNonactiveMute, pText: Localize(pStr: "Mute when not active"), Checked: g_Config.m_SndNonactiveMute, pRect: &Button))
1261 g_Config.m_SndNonactiveMute ^= 1;
1262
1263 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1264 if(DoButton_CheckBox(pId: &g_Config.m_SndGame, pText: Localize(pStr: "Enable game sounds"), Checked: g_Config.m_SndGame, pRect: &Button))
1265 g_Config.m_SndGame ^= 1;
1266
1267 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1268 if(DoButton_CheckBox(pId: &g_Config.m_SndGun, pText: Localize(pStr: "Enable gun sound"), Checked: g_Config.m_SndGun, pRect: &Button))
1269 g_Config.m_SndGun ^= 1;
1270
1271 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1272 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))
1273 g_Config.m_SndLongPain ^= 1;
1274
1275 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1276 if(DoButton_CheckBox(pId: &g_Config.m_SndServerMessage, pText: Localize(pStr: "Enable server message sound"), Checked: g_Config.m_SndServerMessage, pRect: &Button))
1277 g_Config.m_SndServerMessage ^= 1;
1278
1279 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1280 if(DoButton_CheckBox(pId: &g_Config.m_SndChat, pText: Localize(pStr: "Enable regular chat sound"), Checked: g_Config.m_SndChat, pRect: &Button))
1281 g_Config.m_SndChat ^= 1;
1282
1283 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1284 if(DoButton_CheckBox(pId: &g_Config.m_SndTeamChat, pText: Localize(pStr: "Enable team chat sound"), Checked: g_Config.m_SndTeamChat, pRect: &Button))
1285 g_Config.m_SndTeamChat ^= 1;
1286
1287 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1288 if(DoButton_CheckBox(pId: &g_Config.m_SndHighlight, pText: Localize(pStr: "Enable highlighted chat sound"), Checked: g_Config.m_SndHighlight, pRect: &Button))
1289 g_Config.m_SndHighlight ^= 1;
1290
1291 // volume slider
1292 {
1293 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1294 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1295 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: "%");
1296 }
1297
1298 // volume slider game sounds
1299 {
1300 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1301 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1302 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: "%");
1303 }
1304
1305 // volume slider gui sounds
1306 {
1307 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1308 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1309 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: "%");
1310 }
1311
1312 // volume slider map sounds
1313 {
1314 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1315 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1316 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: "%");
1317 }
1318
1319 // volume slider background music
1320 {
1321 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1322 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1323 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: "%");
1324 }
1325}
1326
1327void CMenus::RenderLanguageSettings(CUIRect MainView)
1328{
1329 const float CreditsFontSize = 14.0f;
1330 const float CreditsMargin = 10.0f;
1331
1332 CUIRect List, CreditsScroll;
1333 MainView.HSplitBottom(Cut: 4.0f * CreditsFontSize + 2.0f * CreditsMargin, pTop: &List, pBottom: &CreditsScroll);
1334 List.HSplitBottom(Cut: 5.0f, pTop: &List, pBottom: nullptr);
1335
1336 RenderLanguageSelection(MainView: List);
1337
1338 CreditsScroll.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1339
1340 static CScrollRegion s_CreditsScrollRegion;
1341 CScrollRegionParams ScrollParams;
1342 ScrollParams.m_ScrollUnit = CreditsFontSize;
1343 s_CreditsScrollRegion.Begin(pClipRect: &CreditsScroll, pParams: &ScrollParams);
1344
1345 CTextCursor Cursor;
1346 Cursor.m_FontSize = CreditsFontSize;
1347 Cursor.m_LineWidth = CreditsScroll.w - 2.0f * CreditsMargin;
1348
1349 const unsigned OldRenderFlags = TextRender()->GetRenderFlags();
1350 TextRender()->SetRenderFlags(OldRenderFlags | TEXT_RENDER_FLAG_ONE_TIME_USE);
1351 STextContainerIndex CreditsTextContainer;
1352 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"));
1353 TextRender()->SetRenderFlags(OldRenderFlags);
1354 if(CreditsTextContainer.Valid())
1355 {
1356 CUIRect CreditsLabel;
1357 CreditsScroll.HSplitTop(Cut: TextRender()->GetBoundingBoxTextContainer(TextContainerIndex: CreditsTextContainer).m_H + 2.0f * CreditsMargin, pTop: &CreditsLabel, pBottom: &CreditsScroll);
1358 s_CreditsScrollRegion.AddRect(Rect: CreditsLabel);
1359 CreditsLabel.Margin(Cut: CreditsMargin, pOtherRect: &CreditsLabel);
1360 TextRender()->RenderTextContainer(TextContainerIndex: CreditsTextContainer, TextColor: TextRender()->DefaultTextColor(), TextOutlineColor: TextRender()->DefaultTextOutlineColor(), X: CreditsLabel.x, Y: CreditsLabel.y);
1361 TextRender()->DeleteTextContainer(TextContainerIndex&: CreditsTextContainer);
1362 }
1363
1364 s_CreditsScrollRegion.End();
1365}
1366
1367bool CMenus::RenderLanguageSelection(CUIRect MainView)
1368{
1369 static int s_SelectedLanguage = -2; // -2 = unloaded, -1 = unset
1370 static CListBox s_ListBox;
1371
1372 if(s_SelectedLanguage == -2)
1373 {
1374 s_SelectedLanguage = -1;
1375 for(size_t i = 0; i < g_Localization.Languages().size(); i++)
1376 {
1377 if(str_comp(a: g_Localization.Languages()[i].m_Filename.c_str(), b: g_Config.m_ClLanguagefile) == 0)
1378 {
1379 s_SelectedLanguage = i;
1380 s_ListBox.ScrollToSelected();
1381 break;
1382 }
1383 }
1384 }
1385
1386 const int OldSelected = s_SelectedLanguage;
1387
1388 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: g_Localization.Languages().size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_SelectedLanguage, pRect: &MainView);
1389
1390 for(const auto &Language : g_Localization.Languages())
1391 {
1392 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()));
1393 if(!Item.m_Visible)
1394 continue;
1395
1396 CUIRect FlagRect, Label;
1397 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h * 2.0f, pLeft: &FlagRect, pRight: &Label);
1398 FlagRect.VMargin(Cut: 6.0f, pOtherRect: &FlagRect);
1399 FlagRect.HMargin(Cut: 3.0f, pOtherRect: &FlagRect);
1400 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);
1401
1402 Ui()->DoLabel(pRect: &Label, pText: Language.m_Name.c_str(), Size: 16.0f, Align: TEXTALIGN_ML);
1403 }
1404
1405 s_SelectedLanguage = s_ListBox.DoEnd();
1406
1407 if(OldSelected != s_SelectedLanguage)
1408 {
1409 str_copy(dst&: g_Config.m_ClLanguagefile, src: g_Localization.Languages()[s_SelectedLanguage].m_Filename.c_str());
1410 GameClient()->OnLanguageChange();
1411 }
1412
1413 return s_ListBox.WasItemActivated();
1414}
1415
1416void CMenus::RenderSettings(CUIRect MainView)
1417{
1418 // render background
1419 CUIRect Button, TabBar, RestartBar;
1420 MainView.VSplitRight(Cut: 120.0f, pLeft: &MainView, pRight: &TabBar);
1421 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1422 MainView.Margin(Cut: 20.0f, pOtherRect: &MainView);
1423
1424 const bool NeedRestart = m_NeedRestartGraphics || m_NeedRestartSound || m_NeedRestartUpdate;
1425 if(NeedRestart)
1426 {
1427 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &RestartBar);
1428 MainView.HSplitBottom(Cut: 10.0f, pTop: &MainView, pBottom: nullptr);
1429 }
1430
1431 TabBar.HSplitTop(Cut: 50.0f, pTop: &Button, pBottom: &TabBar);
1432 Button.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_BR, Rounding: 10.0f);
1433
1434 const char *apTabs[SETTINGS_LENGTH] = {
1435 Localize(pStr: "Language"),
1436 Localize(pStr: "General"),
1437 Localize(pStr: "Player"),
1438 Client()->IsSixup() ? "Tee 0.7" : Localize(pStr: "Tee"),
1439 Localize(pStr: "Appearance"),
1440 Localize(pStr: "Controls"),
1441 Localize(pStr: "Graphics"),
1442 Localize(pStr: "Sound"),
1443 Localize(pStr: "DDNet"),
1444 Localize(pStr: "Assets")};
1445 static CButtonContainer s_aTabButtons[SETTINGS_LENGTH];
1446
1447 for(int i = 0; i < SETTINGS_LENGTH; i++)
1448 {
1449 TabBar.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &TabBar);
1450 TabBar.HSplitTop(Cut: 26.0f, pTop: &Button, pBottom: &TabBar);
1451 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]))
1452 g_Config.m_UiSettingsPage = i;
1453 }
1454
1455 if(g_Config.m_UiSettingsPage == SETTINGS_LANGUAGE)
1456 {
1457 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_LANGUAGE);
1458 RenderLanguageSettings(MainView);
1459 }
1460 else if(g_Config.m_UiSettingsPage == SETTINGS_GENERAL)
1461 {
1462 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GENERAL);
1463 RenderSettingsGeneral(MainView);
1464 }
1465 else if(g_Config.m_UiSettingsPage == SETTINGS_PLAYER)
1466 {
1467 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_PLAYER);
1468 RenderSettingsPlayer(MainView);
1469 }
1470 else if(g_Config.m_UiSettingsPage == SETTINGS_TEE)
1471 {
1472 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_TEE);
1473 if(Client()->IsSixup())
1474 RenderSettingsTee7(MainView);
1475 else
1476 RenderSettingsTee(MainView);
1477 }
1478 else if(g_Config.m_UiSettingsPage == SETTINGS_APPEARANCE)
1479 {
1480 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_APPEARANCE);
1481 RenderSettingsAppearance(MainView);
1482 }
1483 else if(g_Config.m_UiSettingsPage == SETTINGS_CONTROLS)
1484 {
1485 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_CONTROLS);
1486 m_MenusSettingsControls.Render(MainView);
1487 }
1488 else if(g_Config.m_UiSettingsPage == SETTINGS_GRAPHICS)
1489 {
1490 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GRAPHICS);
1491 RenderSettingsGraphics(MainView);
1492 }
1493 else if(g_Config.m_UiSettingsPage == SETTINGS_SOUND)
1494 {
1495 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_SOUND);
1496 RenderSettingsSound(MainView);
1497 }
1498 else if(g_Config.m_UiSettingsPage == SETTINGS_DDNET)
1499 {
1500 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_DDNET);
1501 RenderSettingsDDNet(MainView);
1502 }
1503 else if(g_Config.m_UiSettingsPage == SETTINGS_ASSETS)
1504 {
1505 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_ASSETS);
1506 RenderSettingsCustom(MainView);
1507 }
1508 else
1509 {
1510 dbg_assert_failed("ui_settings_page invalid");
1511 }
1512
1513 if(NeedRestart)
1514 {
1515 CUIRect RestartWarning, RestartButton;
1516 RestartBar.VSplitRight(Cut: 125.0f, pLeft: &RestartWarning, pRight: &RestartButton);
1517 RestartWarning.VSplitRight(Cut: 10.0f, pLeft: &RestartWarning, pRight: nullptr);
1518 if(m_NeedRestartUpdate)
1519 {
1520 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
1521 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "DDNet Client needs to be restarted to complete update!"), Size: 14.0f, Align: TEXTALIGN_ML);
1522 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
1523 }
1524 else
1525 {
1526 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "You must restart the game for all settings to take effect."), Size: 14.0f, Align: TEXTALIGN_ML);
1527 }
1528
1529 static CButtonContainer s_RestartButton;
1530 if(DoButton_Menu(pButtonContainer: &s_RestartButton, pText: Localize(pStr: "Restart"), Checked: 0, pRect: &RestartButton))
1531 {
1532 if(Client()->State() == IClient::STATE_ONLINE || GameClient()->Editor()->HasUnsavedData())
1533 {
1534 m_Popup = POPUP_RESTART;
1535 }
1536 else
1537 {
1538 Client()->Restart();
1539 }
1540 }
1541 }
1542}
1543
1544bool CMenus::RenderHslaScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, float DarkestLight)
1545{
1546 const unsigned PrevPackedColor = *pColor;
1547 ColorHSLA Color(*pColor, Alpha);
1548 const ColorHSLA OriginalColor = Color;
1549 const char *apLabels[] = {Localize(pStr: "Hue"), Localize(pStr: "Sat."), Localize(pStr: "Lht."), Localize(pStr: "Alpha")};
1550 const float SizePerEntry = 20.0f;
1551 const float MarginPerEntry = 5.0f;
1552 const float PreviewMargin = 2.5f;
1553 const float PreviewHeight = 40.0f + 2 * PreviewMargin;
1554 const float OffY = (SizePerEntry + MarginPerEntry) * (3 + (Alpha ? 1 : 0)) - PreviewHeight;
1555
1556 CUIRect Preview;
1557 pRect->VSplitLeft(Cut: PreviewHeight, pLeft: &Preview, pRight: pRect);
1558 Preview.HSplitTop(Cut: OffY / 2.0f, pTop: nullptr, pBottom: &Preview);
1559 Preview.HSplitTop(Cut: PreviewHeight, pTop: &Preview, pBottom: nullptr);
1560
1561 Preview.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f + PreviewMargin);
1562 Preview.Margin(Cut: PreviewMargin, pOtherRect: &Preview);
1563 Preview.Draw(Color: color_cast<ColorRGBA>(hsl: Color.UnclampLighting(Darkest: DarkestLight)), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f + PreviewMargin);
1564
1565 auto &&RenderHueRect = [&](CUIRect *pColorRect) {
1566 float CurXOff = pColorRect->x;
1567 const float SizeColor = pColorRect->w / 6;
1568
1569 // red to yellow
1570 {
1571 IGraphics::CColorVertex aColorVertices[] = {
1572 IGraphics::CColorVertex(0, 1, 0, 0, 1),
1573 IGraphics::CColorVertex(1, 1, 1, 0, 1),
1574 IGraphics::CColorVertex(2, 1, 0, 0, 1),
1575 IGraphics::CColorVertex(3, 1, 1, 0, 1)};
1576 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1577
1578 IGraphics::CFreeformItem Freeform(
1579 CurXOff, pColorRect->y,
1580 CurXOff + SizeColor, pColorRect->y,
1581 CurXOff, pColorRect->y + pColorRect->h,
1582 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1583 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1584 }
1585
1586 // yellow to green
1587 CurXOff += SizeColor;
1588 {
1589 IGraphics::CColorVertex aColorVertices[] = {
1590 IGraphics::CColorVertex(0, 1, 1, 0, 1),
1591 IGraphics::CColorVertex(1, 0, 1, 0, 1),
1592 IGraphics::CColorVertex(2, 1, 1, 0, 1),
1593 IGraphics::CColorVertex(3, 0, 1, 0, 1)};
1594 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1595
1596 IGraphics::CFreeformItem Freeform(
1597 CurXOff, pColorRect->y,
1598 CurXOff + SizeColor, pColorRect->y,
1599 CurXOff, pColorRect->y + pColorRect->h,
1600 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1601 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1602 }
1603
1604 CurXOff += SizeColor;
1605 // green to turquoise
1606 {
1607 IGraphics::CColorVertex aColorVertices[] = {
1608 IGraphics::CColorVertex(0, 0, 1, 0, 1),
1609 IGraphics::CColorVertex(1, 0, 1, 1, 1),
1610 IGraphics::CColorVertex(2, 0, 1, 0, 1),
1611 IGraphics::CColorVertex(3, 0, 1, 1, 1)};
1612 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1613
1614 IGraphics::CFreeformItem Freeform(
1615 CurXOff, pColorRect->y,
1616 CurXOff + SizeColor, pColorRect->y,
1617 CurXOff, pColorRect->y + pColorRect->h,
1618 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1619 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1620 }
1621
1622 CurXOff += SizeColor;
1623 // turquoise to blue
1624 {
1625 IGraphics::CColorVertex aColorVertices[] = {
1626 IGraphics::CColorVertex(0, 0, 1, 1, 1),
1627 IGraphics::CColorVertex(1, 0, 0, 1, 1),
1628 IGraphics::CColorVertex(2, 0, 1, 1, 1),
1629 IGraphics::CColorVertex(3, 0, 0, 1, 1)};
1630 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1631
1632 IGraphics::CFreeformItem Freeform(
1633 CurXOff, pColorRect->y,
1634 CurXOff + SizeColor, pColorRect->y,
1635 CurXOff, pColorRect->y + pColorRect->h,
1636 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1637 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1638 }
1639
1640 CurXOff += SizeColor;
1641 // blue to purple
1642 {
1643 IGraphics::CColorVertex aColorVertices[] = {
1644 IGraphics::CColorVertex(0, 0, 0, 1, 1),
1645 IGraphics::CColorVertex(1, 1, 0, 1, 1),
1646 IGraphics::CColorVertex(2, 0, 0, 1, 1),
1647 IGraphics::CColorVertex(3, 1, 0, 1, 1)};
1648 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1649
1650 IGraphics::CFreeformItem Freeform(
1651 CurXOff, pColorRect->y,
1652 CurXOff + SizeColor, pColorRect->y,
1653 CurXOff, pColorRect->y + pColorRect->h,
1654 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1655 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1656 }
1657
1658 CurXOff += SizeColor;
1659 // purple to red
1660 {
1661 IGraphics::CColorVertex aColorVertices[] = {
1662 IGraphics::CColorVertex(0, 1, 0, 1, 1),
1663 IGraphics::CColorVertex(1, 1, 0, 0, 1),
1664 IGraphics::CColorVertex(2, 1, 0, 1, 1),
1665 IGraphics::CColorVertex(3, 1, 0, 0, 1)};
1666 Graphics()->SetColorVertex(pArray: aColorVertices, Num: std::size(aColorVertices));
1667
1668 IGraphics::CFreeformItem Freeform(
1669 CurXOff, pColorRect->y,
1670 CurXOff + SizeColor, pColorRect->y,
1671 CurXOff, pColorRect->y + pColorRect->h,
1672 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
1673 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1674 }
1675 };
1676
1677 auto &&RenderSaturationRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) {
1678 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColor);
1679 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColor);
1680
1681 LeftColor.s = 0.0f;
1682 RightColor.s = 1.0f;
1683
1684 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
1685 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
1686
1687 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1688
1689 IGraphics::CFreeformItem Freeform(
1690 pColorRect->x, pColorRect->y,
1691 pColorRect->x + pColorRect->w, pColorRect->y,
1692 pColorRect->x, pColorRect->y + pColorRect->h,
1693 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1694 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1695 };
1696
1697 auto &&RenderLightingRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) {
1698 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColor);
1699 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColor);
1700
1701 LeftColor.l = DarkestLight;
1702 RightColor.l = 1.0f;
1703
1704 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
1705 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
1706
1707 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1708
1709 IGraphics::CFreeformItem Freeform(
1710 pColorRect->x, pColorRect->y,
1711 pColorRect->x + pColorRect->w, pColorRect->y,
1712 pColorRect->x, pColorRect->y + pColorRect->h,
1713 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1714 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1715 };
1716
1717 auto &&RenderAlphaRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColorFull) {
1718 const ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: color_cast<ColorHSLA>(rgb: CurColorFull).WithAlpha(alpha: 0.0f));
1719 const ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: color_cast<ColorHSLA>(rgb: CurColorFull).WithAlpha(alpha: 1.0f));
1720
1721 Graphics()->SetColor4(TopLeft: LeftColorRGBA, TopRight: RightColorRGBA, BottomLeft: RightColorRGBA, BottomRight: LeftColorRGBA);
1722
1723 IGraphics::CFreeformItem Freeform(
1724 pColorRect->x, pColorRect->y,
1725 pColorRect->x + pColorRect->w, pColorRect->y,
1726 pColorRect->x, pColorRect->y + pColorRect->h,
1727 pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h);
1728 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
1729 };
1730
1731 for(int i = 0; i < 3 + Alpha; i++)
1732 {
1733 CUIRect Button, Label;
1734 pRect->HSplitTop(Cut: SizePerEntry, pTop: &Button, pBottom: pRect);
1735 pRect->HSplitTop(Cut: MarginPerEntry, pTop: nullptr, pBottom: pRect);
1736 Button.VSplitLeft(Cut: 140.0f, pLeft: &Label, pRight: &Button);
1737 Label.VMargin(Cut: 10.0f, pOtherRect: &Label);
1738
1739 Button.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 1.0f);
1740
1741 CUIRect Rail;
1742 Button.Margin(Cut: 2.0f, pOtherRect: &Rail);
1743
1744 char aBuf[32];
1745
1746 // Hue
1747 if(i == 0)
1748 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));
1749 // Lht
1750 else if(i == 2)
1751 {
1752 // handle internal light clamping, see `UnclampLighting`
1753 float Lht = DarkestLight + Color[i] * (1.0f - DarkestLight);
1754 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));
1755 }
1756 // Sat and Alpha
1757 else
1758 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));
1759 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML);
1760
1761 ColorRGBA HandleColor;
1762 Graphics()->TextureClear();
1763 Graphics()->TrianglesBegin();
1764 if(i == 0)
1765 {
1766 RenderHueRect(&Rail);
1767 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f));
1768 }
1769 else if(i == 1)
1770 {
1771 RenderSaturationRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f)));
1772 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, 0.5f, 1.0f));
1773 }
1774 else if(i == 2)
1775 {
1776 RenderLightingRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, 0.5f, 1.0f)));
1777 HandleColor = color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(Darkest: DarkestLight));
1778 }
1779 else if(i == 3)
1780 {
1781 RenderAlphaRect(&Rail, color_cast<ColorRGBA>(hsl: ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(Darkest: DarkestLight)));
1782 HandleColor = color_cast<ColorRGBA>(hsl: Color.UnclampLighting(Darkest: DarkestLight));
1783 }
1784 Graphics()->TrianglesEnd();
1785
1786 Color[i] = Ui()->DoScrollbarH(pId: &((char *)pColor)[i], pRect: &Button, Current: Color[i], pColorInner: &HandleColor);
1787 }
1788
1789 if(OriginalColor != Color)
1790 {
1791 *pColor = Color.Pack(Alpha);
1792 }
1793 return PrevPackedColor != *pColor;
1794}
1795
1796enum
1797{
1798 APPEARANCE_TAB_HUD = 0,
1799 APPEARANCE_TAB_CHAT = 1,
1800 APPEARANCE_TAB_NAME_PLATE = 2,
1801 APPEARANCE_TAB_HOOK_COLLISION = 3,
1802 APPEARANCE_TAB_INFO_MESSAGES = 4,
1803 APPEARANCE_TAB_LASER = 5,
1804 NUMBER_OF_APPEARANCE_TABS = 6,
1805};
1806
1807void CMenus::RenderSettingsAppearance(CUIRect MainView)
1808{
1809 char aBuf[128];
1810 static int s_CurTab = 0;
1811
1812 CUIRect TabBar, LeftView, RightView, Button;
1813
1814 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
1815 const float TabWidth = TabBar.w / (float)NUMBER_OF_APPEARANCE_TABS;
1816 static CButtonContainer s_aPageTabs[NUMBER_OF_APPEARANCE_TABS] = {};
1817 const char *apTabNames[NUMBER_OF_APPEARANCE_TABS] = {
1818 Localize(pStr: "HUD"),
1819 Localize(pStr: "Chat"),
1820 Localize(pStr: "Name Plate"),
1821 Localize(pStr: "Hook Collisions"),
1822 Localize(pStr: "Info Messages"),
1823 Localize(pStr: "Laser")};
1824
1825 for(int Tab = APPEARANCE_TAB_HUD; Tab < NUMBER_OF_APPEARANCE_TABS; ++Tab)
1826 {
1827 TabBar.VSplitLeft(Cut: TabWidth, pLeft: &Button, pRight: &TabBar);
1828 const int Corners = Tab == APPEARANCE_TAB_HUD ? IGraphics::CORNER_L : (Tab == NUMBER_OF_APPEARANCE_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE);
1829 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))
1830 {
1831 s_CurTab = Tab;
1832 }
1833 }
1834
1835 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1836
1837 const float LineSize = 20.0f;
1838 const float ColorPickerLineSize = 25.0f;
1839 const float HeadlineFontSize = 20.0f;
1840 const float HeadlineHeight = 30.0f;
1841 const float MarginSmall = 5.0f;
1842 const float MarginBetweenViews = 20.0f;
1843
1844 const float ColorPickerLabelSize = 13.0f;
1845 const float ColorPickerLineSpacing = 5.0f;
1846
1847 if(s_CurTab == APPEARANCE_TAB_HUD)
1848 {
1849 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
1850
1851 // ***** HUD ***** //
1852 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "HUD"), FontSize: HeadlineFontSize,
1853 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1854 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1855
1856 // Switch of the entire HUD
1857 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhud, pText: Localize(pStr: "Show ingame HUD"), pValue: &g_Config.m_ClShowhud, pRect: &LeftView, VMargin: LineSize);
1858
1859 // Switches of the various normal HUD elements
1860 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudHealthAmmo, pText: Localize(pStr: "Show health, shields and ammo"), pValue: &g_Config.m_ClShowhudHealthAmmo, pRect: &LeftView, VMargin: LineSize);
1861 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudScore, pText: Localize(pStr: "Show score"), pValue: &g_Config.m_ClShowhudScore, pRect: &LeftView, VMargin: LineSize);
1862 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowLocalTimeAlways, pText: Localize(pStr: "Show local time always"), pValue: &g_Config.m_ClShowLocalTimeAlways, pRect: &LeftView, VMargin: LineSize);
1863 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClSpecCursor, pText: Localize(pStr: "Show spectator cursor"), pValue: &g_Config.m_ClSpecCursor, pRect: &LeftView, VMargin: LineSize);
1864
1865 // Settings of the HUD element for votes
1866 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowVotesAfterVoting, pText: Localize(pStr: "Show votes window after voting"), pValue: &g_Config.m_ClShowVotesAfterVoting, pRect: &LeftView, VMargin: LineSize);
1867
1868 // ***** Scoreboard ***** //
1869 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
1870 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Scoreboard"), FontSize: HeadlineFontSize,
1871 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1872 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1873
1874 ColorRGBA GreenDefault(0.78f, 1.0f, 0.8f, 1.0f);
1875 static CButtonContainer s_AuthedColor, s_SameClanColor;
1876 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);
1877 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);
1878
1879 // ***** DDRace HUD ***** //
1880 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "DDRace HUD"), FontSize: HeadlineFontSize,
1881 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
1882 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
1883
1884 // Switches of various DDRace HUD elements
1885 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);
1886 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDDRace, pText: Localize(pStr: "Show DDRace HUD"), pValue: &g_Config.m_ClShowhudDDRace, pRect: &RightView, VMargin: LineSize);
1887 if(g_Config.m_ClShowhudDDRace)
1888 {
1889 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudJumpsIndicator, pText: Localize(pStr: "Show jumps indicator"), pValue: &g_Config.m_ClShowhudJumpsIndicator, pRect: &RightView, VMargin: LineSize);
1890 }
1891 else
1892 {
1893 RightView.HSplitTop(Cut: LineSize, pTop: nullptr, pBottom: &RightView); // Create empty space for hidden option
1894 }
1895
1896 // Eye with a number of spectators
1897 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudSpectatorCount, pText: Localize(pStr: "Show number of spectators"), pValue: &g_Config.m_ClShowhudSpectatorCount, pRect: &RightView, VMargin: LineSize);
1898
1899 // Switch for dummy actions display
1900 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDummyActions, pText: Localize(pStr: "Show dummy actions"), pValue: &g_Config.m_ClShowhudDummyActions, pRect: &RightView, VMargin: LineSize);
1901
1902 // Player movement information display settings
1903 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerPosition, pText: Localize(pStr: "Show player position"), pValue: &g_Config.m_ClShowhudPlayerPosition, pRect: &RightView, VMargin: LineSize);
1904 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerSpeed, pText: Localize(pStr: "Show player speed"), pValue: &g_Config.m_ClShowhudPlayerSpeed, pRect: &RightView, VMargin: LineSize);
1905 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerAngle, pText: Localize(pStr: "Show player target angle"), pValue: &g_Config.m_ClShowhudPlayerAngle, pRect: &RightView, VMargin: LineSize);
1906
1907 // Freeze bar settings
1908 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowFreezeBars, pText: Localize(pStr: "Show freeze bars"), pValue: &g_Config.m_ClShowFreezeBars, pRect: &RightView, VMargin: LineSize);
1909 RightView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &RightView);
1910 if(g_Config.m_ClShowFreezeBars)
1911 {
1912 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: "%");
1913 }
1914 }
1915 else if(s_CurTab == APPEARANCE_TAB_CHAT)
1916 {
1917 CChat &Chat = GameClient()->m_Chat;
1918 CUIRect TopView, PreviewView;
1919 MainView.HSplitBottom(Cut: 220.0f, pTop: &TopView, pBottom: &PreviewView);
1920 TopView.HSplitBottom(Cut: MarginBetweenViews, pTop: &TopView, pBottom: nullptr);
1921 TopView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
1922
1923 // ***** Chat ***** //
1924 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Chat"), FontSize: HeadlineFontSize,
1925 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
1926 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
1927
1928 // General chat settings
1929 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1930 if(DoButton_CheckBox(pId: &g_Config.m_ClShowChat, pText: Localize(pStr: "Show chat"), Checked: g_Config.m_ClShowChat, pRect: &Button))
1931 {
1932 g_Config.m_ClShowChat = g_Config.m_ClShowChat ? 0 : 1;
1933 }
1934 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1935 if(g_Config.m_ClShowChat)
1936 {
1937 static int s_ShowChat = 0;
1938 if(DoButton_CheckBox(pId: &s_ShowChat, pText: Localize(pStr: "Always show chat"), Checked: g_Config.m_ClShowChat == 2, pRect: &Button))
1939 g_Config.m_ClShowChat = g_Config.m_ClShowChat != 2 ? 2 : 1;
1940 }
1941
1942 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);
1943 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);
1944 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);
1945
1946 if(DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClChatOld, pText: Localize(pStr: "Use old chat style"), pValue: &g_Config.m_ClChatOld, pRect: &LeftView, VMargin: LineSize))
1947 GameClient()->m_Chat.RebuildChat();
1948
1949 // DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClCensorChat, Localize("Censor profanity"), &g_Config.m_ClCensorChat, &LeftView, LineSize);
1950
1951 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1952 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))
1953 {
1954 Chat.EnsureCoherentWidth();
1955 Chat.RebuildChat();
1956 }
1957
1958 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
1959 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClChatWidth, pOption: &g_Config.m_ClChatWidth, pRect: &Button, pStr: Localize(pStr: "Chat width"), Min: 120, Max: 400))
1960 {
1961 Chat.EnsureCoherentFontSize();
1962 Chat.RebuildChat();
1963 }
1964
1965 static CButtonContainer s_BackgroundColor;
1966 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);
1967
1968 // ***** Messages ***** //
1969 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Messages"), FontSize: HeadlineFontSize,
1970 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
1971 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
1972
1973 // Message Colors and extra settings
1974 static CButtonContainer s_SystemMessageColor;
1975 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);
1976 static CButtonContainer s_HighlightedMessageColor;
1977 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));
1978 static CButtonContainer s_TeamMessageColor;
1979 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));
1980 static CButtonContainer s_FriendMessageColor;
1981 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);
1982 static CButtonContainer s_NormalMessageColor;
1983 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));
1984
1985 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (echo)", Localize(pStr: "Client message"));
1986 static CButtonContainer s_ClientMessageColor;
1987 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));
1988
1989 // ***** Chat Preview ***** //
1990 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
1991 Align: TEXTALIGN_ML, pRect: &PreviewView, LineSize: HeadlineHeight);
1992 PreviewView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &PreviewView);
1993
1994 // Use the rest of the view for preview
1995 PreviewView.Draw(Color: ColorRGBA(1, 1, 1, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1996 PreviewView.Margin(Cut: MarginSmall, pOtherRect: &PreviewView);
1997
1998 ColorRGBA SystemColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor));
1999 ColorRGBA HighlightedColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageHighlightColor));
2000 ColorRGBA TeamColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageTeamColor));
2001 ColorRGBA FriendColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor));
2002 ColorRGBA NormalColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageColor));
2003 ColorRGBA ClientColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor));
2004 ColorRGBA DefaultNameColor(0.8f, 0.8f, 0.8f, 1.0f);
2005
2006 const float RealFontSize = Chat.FontSize() * 2;
2007 const float RealMsgPaddingX = (!g_Config.m_ClChatOld ? Chat.MessagePaddingX() : 0) * 2;
2008 const float RealMsgPaddingY = (!g_Config.m_ClChatOld ? Chat.MessagePaddingY() : 0) * 2;
2009 const float RealMsgPaddingTee = (!g_Config.m_ClChatOld ? Chat.MessageTeeSize() + CChat::MESSAGE_TEE_PADDING_RIGHT : 0) * 2;
2010 const float RealOffsetY = RealFontSize + RealMsgPaddingY;
2011
2012 const float X = RealMsgPaddingX / 2.0f + PreviewView.x;
2013 float Y = PreviewView.y;
2014 float LineWidth = g_Config.m_ClChatWidth * 2 - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee;
2015
2016 str_copy(dst&: aBuf, src: Client()->PlayerName());
2017
2018 const CAnimState *pIdleState = CAnimState::GetIdle();
2019 const float RealTeeSize = Chat.MessageTeeSize() * 2;
2020 const float RealTeeSizeHalved = Chat.MessageTeeSize();
2021 constexpr float TWSkinUnreliableOffset = -0.25f;
2022 const float OffsetTeeY = RealTeeSizeHalved;
2023 const float FullHeightMinusTee = RealOffsetY - RealTeeSize;
2024
2025 struct SPreviewLine
2026 {
2027 int m_ClientId;
2028 bool m_Team;
2029 char m_aName[64];
2030 char m_aText[256];
2031 bool m_Friend;
2032 bool m_Player;
2033 bool m_Client;
2034 bool m_Highlighted;
2035 int m_TimesRepeated;
2036
2037 CTeeRenderInfo m_RenderInfo;
2038 };
2039
2040 static std::vector<SPreviewLine> s_vLines;
2041
2042 enum ELineFlag
2043 {
2044 FLAG_TEAM = 1 << 0,
2045 FLAG_FRIEND = 1 << 1,
2046 FLAG_HIGHLIGHT = 1 << 2,
2047 FLAG_CLIENT = 1 << 3
2048 };
2049 enum
2050 {
2051 PREVIEW_SYS,
2052 PREVIEW_HIGHLIGHT,
2053 PREVIEW_TEAM,
2054 PREVIEW_FRIEND,
2055 PREVIEW_SPAMMER,
2056 PREVIEW_CLIENT
2057 };
2058 auto &&SetPreviewLine = [](int Index, int ClientId, const char *pName, const char *pText, int Flag, int Repeats) {
2059 SPreviewLine *pLine;
2060 if((int)s_vLines.size() <= Index)
2061 {
2062 s_vLines.emplace_back();
2063 pLine = &s_vLines.back();
2064 }
2065 else
2066 {
2067 pLine = &s_vLines[Index];
2068 }
2069 pLine->m_ClientId = ClientId;
2070 pLine->m_Team = Flag & FLAG_TEAM;
2071 pLine->m_Friend = Flag & FLAG_FRIEND;
2072 pLine->m_Player = ClientId >= 0;
2073 pLine->m_Highlighted = Flag & FLAG_HIGHLIGHT;
2074 pLine->m_Client = Flag & FLAG_CLIENT;
2075 pLine->m_TimesRepeated = Repeats;
2076 str_copy(dst&: pLine->m_aName, src: pName);
2077 str_copy(dst&: pLine->m_aText, src: pText);
2078 };
2079 auto &&SetLineSkin = [RealTeeSize](int Index, const CSkin *pSkin) {
2080 if(Index >= (int)s_vLines.size())
2081 return;
2082 s_vLines[Index].m_RenderInfo.m_Size = RealTeeSize;
2083 s_vLines[Index].m_RenderInfo.Apply(pSkin);
2084 };
2085
2086 auto &&RenderPreview = [&](int LineIndex, int x, int y, bool Render = true) {
2087 if(LineIndex >= (int)s_vLines.size())
2088 return vec2(0, 0);
2089 CTextCursor LocalCursor;
2090 LocalCursor.SetPosition(vec2(x, y));
2091 LocalCursor.m_FontSize = RealFontSize;
2092 LocalCursor.m_Flags = Render ? TEXTFLAG_RENDER : 0;
2093 LocalCursor.m_LineWidth = LineWidth;
2094 const auto &Line = s_vLines[LineIndex];
2095
2096 char aClientId[16] = "";
2097 if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2098 {
2099 GameClient()->FormatClientId(ClientId: Line.m_ClientId, aClientId, Format: EClientIdFormat::INDENT_FORCE);
2100 }
2101
2102 char aCount[12];
2103 if(Line.m_ClientId < 0)
2104 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: "[%d] ", Line.m_TimesRepeated + 1);
2105 else
2106 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: " [%d]", Line.m_TimesRepeated + 1);
2107
2108 if(Line.m_Player)
2109 {
2110 LocalCursor.m_X += RealMsgPaddingTee;
2111
2112 if(Line.m_Friend && g_Config.m_ClMessageFriend)
2113 {
2114 if(Render)
2115 TextRender()->TextColor(Color: FriendColor);
2116 TextRender()->TextEx(pCursor: &LocalCursor, pText: "♥ ", Length: -1);
2117 }
2118 }
2119
2120 ColorRGBA NameColor;
2121 if(Line.m_Team)
2122 NameColor = CalculateNameColor(TextColorHSL: color_cast<ColorHSLA>(rgb: TeamColor));
2123 else if(Line.m_Player)
2124 NameColor = DefaultNameColor;
2125 else if(Line.m_Client)
2126 NameColor = ClientColor;
2127 else
2128 NameColor = SystemColor;
2129
2130 if(Render)
2131 TextRender()->TextColor(Color: NameColor);
2132
2133 TextRender()->TextEx(pCursor: &LocalCursor, pText: aClientId);
2134 TextRender()->TextEx(pCursor: &LocalCursor, pText: Line.m_aName);
2135
2136 if(Line.m_TimesRepeated > 0)
2137 {
2138 if(Render)
2139 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.3f);
2140 TextRender()->TextEx(pCursor: &LocalCursor, pText: aCount, Length: -1);
2141 }
2142
2143 if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2144 {
2145 if(Render)
2146 TextRender()->TextColor(Color: NameColor);
2147 TextRender()->TextEx(pCursor: &LocalCursor, pText: ": ", Length: -1);
2148 }
2149
2150 CTextCursor AppendCursor = LocalCursor;
2151 AppendCursor.m_LongestLineWidth = 0.0f;
2152 if(!g_Config.m_ClChatOld)
2153 {
2154 AppendCursor.m_StartX = LocalCursor.m_X;
2155 AppendCursor.m_LineWidth -= LocalCursor.m_LongestLineWidth;
2156 }
2157
2158 if(Render)
2159 {
2160 if(Line.m_Highlighted)
2161 TextRender()->TextColor(Color: HighlightedColor);
2162 else if(Line.m_Team)
2163 TextRender()->TextColor(Color: TeamColor);
2164 else if(Line.m_Player)
2165 TextRender()->TextColor(Color: NormalColor);
2166 }
2167
2168 TextRender()->TextEx(pCursor: &AppendCursor, pText: Line.m_aText, Length: -1);
2169 if(Render)
2170 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
2171
2172 return vec2{LocalCursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth, AppendCursor.Height() + RealMsgPaddingY};
2173 };
2174
2175 // Set preview lines
2176 {
2177 char aLineBuilder[128];
2178
2179 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "'%s' entered and joined the game", aBuf);
2180 SetPreviewLine(PREVIEW_SYS, -1, "*** ", aLineBuilder, 0, 0);
2181
2182 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "Hey, how are you %s?", aBuf);
2183 SetPreviewLine(PREVIEW_HIGHLIGHT, 7, "Random Tee", aLineBuilder, FLAG_HIGHLIGHT, 0);
2184
2185 SetPreviewLine(PREVIEW_TEAM, 11, "Your Teammate", "Let's speedrun this!", FLAG_TEAM, 0);
2186 SetPreviewLine(PREVIEW_FRIEND, 8, "Friend", "Hello there", FLAG_FRIEND, 0);
2187 SetPreviewLine(PREVIEW_SPAMMER, 9, "Spammer", "Hey fools, I'm spamming here!", 0, 5);
2188 SetPreviewLine(PREVIEW_CLIENT, -1, "— ", "Echo command executed", FLAG_CLIENT, 0);
2189 }
2190
2191 SetLineSkin(1, GameClient()->m_Skins.Find(pName: "pinky"));
2192 SetLineSkin(2, GameClient()->m_Skins.Find(pName: "default"));
2193 SetLineSkin(3, GameClient()->m_Skins.Find(pName: "cammostripes"));
2194 SetLineSkin(4, GameClient()->m_Skins.Find(pName: "beast"));
2195
2196 // Backgrounds first
2197 if(!g_Config.m_ClChatOld)
2198 {
2199 Graphics()->TextureClear();
2200 Graphics()->QuadsBegin();
2201 Graphics()->SetColor(color_cast<ColorRGBA>(hsl: ColorHSLA(g_Config.m_ClChatBackgroundColor, true)));
2202
2203 float TempY = Y;
2204 const float RealBackgroundRounding = Chat.MessageRounding() * 2.0f;
2205
2206 auto &&RenderMessageBackground = [&](int LineIndex) {
2207 auto Size = RenderPreview(LineIndex, 0, 0, false);
2208 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);
2209 return Size.y;
2210 };
2211
2212 if(g_Config.m_ClShowChatSystem)
2213 {
2214 TempY += RenderMessageBackground(PREVIEW_SYS);
2215 }
2216
2217 if(!g_Config.m_ClShowChatFriends)
2218 {
2219 if(!g_Config.m_ClShowChatTeamMembersOnly)
2220 TempY += RenderMessageBackground(PREVIEW_HIGHLIGHT);
2221 TempY += RenderMessageBackground(PREVIEW_TEAM);
2222 }
2223
2224 if(!g_Config.m_ClShowChatTeamMembersOnly)
2225 TempY += RenderMessageBackground(PREVIEW_FRIEND);
2226
2227 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2228 {
2229 TempY += RenderMessageBackground(PREVIEW_SPAMMER);
2230 }
2231
2232 TempY += RenderMessageBackground(PREVIEW_CLIENT);
2233
2234 Graphics()->QuadsEnd();
2235 }
2236
2237 // System
2238 if(g_Config.m_ClShowChatSystem)
2239 {
2240 Y += RenderPreview(PREVIEW_SYS, X, Y).y;
2241 }
2242
2243 if(!g_Config.m_ClShowChatFriends)
2244 {
2245 // Highlighted
2246 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2247 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));
2248 if(!g_Config.m_ClShowChatTeamMembersOnly)
2249 Y += RenderPreview(PREVIEW_HIGHLIGHT, X, Y).y;
2250
2251 // Team
2252 if(!g_Config.m_ClChatOld)
2253 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));
2254 Y += RenderPreview(PREVIEW_TEAM, X, Y).y;
2255 }
2256
2257 // Friend
2258 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2259 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));
2260 if(!g_Config.m_ClShowChatTeamMembersOnly)
2261 Y += RenderPreview(PREVIEW_FRIEND, X, Y).y;
2262
2263 // Normal
2264 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2265 {
2266 if(!g_Config.m_ClChatOld)
2267 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));
2268 Y += RenderPreview(PREVIEW_SPAMMER, X, Y).y;
2269 }
2270 // Client
2271 RenderPreview(PREVIEW_CLIENT, X, Y);
2272
2273 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
2274 }
2275 else if(s_CurTab == APPEARANCE_TAB_NAME_PLATE)
2276 {
2277 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2278
2279 // ***** Name Plate ***** //
2280 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Name Plate"), FontSize: HeadlineFontSize,
2281 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2282 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2283
2284 // General name plate settings
2285 {
2286 int Pressed = (g_Config.m_ClNamePlates ? 2 : 0) + (g_Config.m_ClNamePlatesOwn ? 1 : 0);
2287 if(DoLine_RadioMenu(View&: LeftView, pLabel: Localize(pStr: "Show name plates"),
2288 vButtonContainers&: m_vButtonContainersNamePlateShow,
2289 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")},
2290 vValues: {0, 1, 2, 3},
2291 Value&: Pressed))
2292 {
2293 g_Config.m_ClNamePlates = Pressed & 2 ? 1 : 0;
2294 g_Config.m_ClNamePlatesOwn = Pressed & 1 ? 1 : 0;
2295 }
2296 }
2297 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2298 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesSize, pOption: &g_Config.m_ClNamePlatesSize, pRect: &Button, pStr: Localize(pStr: "Name plates size"), Min: -50, Max: 100);
2299 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2300 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesOffset, pOption: &g_Config.m_ClNamePlatesOffset, pRect: &Button, pStr: Localize(pStr: "Name plates offset"), Min: 10, Max: 50);
2301
2302 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNamePlatesClan, pText: Localize(pStr: "Show clan above name plates"), pValue: &g_Config.m_ClNamePlatesClan, pRect: &LeftView, VMargin: LineSize);
2303 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2304 if(g_Config.m_ClNamePlatesClan)
2305 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesClanSize, pOption: &g_Config.m_ClNamePlatesClanSize, pRect: &Button, pStr: Localize(pStr: "Clan plates size"), Min: -50, Max: 100);
2306
2307 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);
2308 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);
2309
2310 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);
2311 if(g_Config.m_ClNamePlatesIds > 0)
2312 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);
2313 else
2314 LeftView.HSplitTop(Cut: LineSize, pTop: nullptr, pBottom: &LeftView);
2315 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2316 if(g_Config.m_ClNamePlatesIds > 0 && g_Config.m_ClNamePlatesIdsSeparateLine > 0)
2317 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNamePlatesIdsSize, pOption: &g_Config.m_ClNamePlatesIdsSize, pRect: &Button, pStr: Localize(pStr: "Client IDs size"), Min: -50, Max: 100);
2318
2319 // ***** Hook Strength ***** //
2320 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
2321 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Hook Strength"), FontSize: HeadlineFontSize,
2322 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2323 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2324
2325 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2326 if(DoButton_CheckBox(pId: &g_Config.m_ClNamePlatesStrong, pText: Localize(pStr: "Show hook strength icon indicator"), Checked: g_Config.m_ClNamePlatesStrong, pRect: &Button))
2327 {
2328 g_Config.m_ClNamePlatesStrong = g_Config.m_ClNamePlatesStrong ? 0 : 1;
2329 }
2330 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2331 if(g_Config.m_ClNamePlatesStrong)
2332 {
2333 static int s_NamePlatesStrong = 0;
2334 if(DoButton_CheckBox(pId: &s_NamePlatesStrong, pText: Localize(pStr: "Show hook strength number indicator"), Checked: g_Config.m_ClNamePlatesStrong == 2, pRect: &Button))
2335 g_Config.m_ClNamePlatesStrong = g_Config.m_ClNamePlatesStrong != 2 ? 2 : 1;
2336 }
2337
2338 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2339 if(g_Config.m_ClNamePlatesStrong)
2340 {
2341 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);
2342 }
2343
2344 // ***** Key Presses ***** //
2345 LeftView.HSplitTop(Cut: MarginBetweenViews, pTop: nullptr, pBottom: &LeftView);
2346 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Key Presses"), FontSize: HeadlineFontSize,
2347 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2348 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2349
2350 DoLine_RadioMenu(View&: LeftView, pLabel: Localize(pStr: "Show players' key presses"),
2351 vButtonContainers&: m_vButtonContainersNamePlateKeyPresses,
2352 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")},
2353 vValues: {0, 3, 1, 2},
2354 Value&: g_Config.m_ClShowDirection);
2355
2356 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2357 if(g_Config.m_ClShowDirection > 0)
2358 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);
2359
2360 // ***** Name Plate Preview ***** //
2361 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2362 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2363 RightView.HSplitTop(Cut: 2.0f * MarginSmall, pTop: nullptr, pBottom: &RightView);
2364
2365 // ***** Name Plate Dummy Preview ***** //
2366 RightView.HSplitBottom(Cut: LineSize, pTop: &RightView, pBottom: &Button);
2367 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))
2368 m_DummyNamePlatePreview = !m_DummyNamePlatePreview;
2369
2370 int Dummy = g_Config.m_ClDummy != (m_DummyNamePlatePreview ? 1 : 0);
2371
2372 const vec2 Position = RightView.Center();
2373
2374 GameClient()->m_NamePlates.RenderNamePlatePreview(Position, Dummy);
2375 }
2376 else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION)
2377 {
2378 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2379
2380 // ***** Hookline ***** //
2381 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Hook collision line"), FontSize: HeadlineFontSize,
2382 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2383 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2384
2385 // General hookline settings
2386 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2387 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))
2388 {
2389 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn ? 0 : 1;
2390 }
2391 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2392 if(g_Config.m_ClShowHookCollOwn)
2393 {
2394 static int s_ShowHookCollOwn = 0;
2395 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))
2396 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn != 2 ? 2 : 1;
2397 }
2398
2399 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2400 if(DoButton_CheckBox(pId: &g_Config.m_ClShowHookCollOther, pText: Localize(pStr: "Show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther, pRect: &Button))
2401 {
2402 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther >= 1 ? 0 : 1;
2403 }
2404 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2405 if(g_Config.m_ClShowHookCollOther)
2406 {
2407 static int s_ShowHookCollOther = 0;
2408 if(DoButton_CheckBox(pId: &s_ShowHookCollOther, pText: Localize(pStr: "Always show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther == 2, pRect: &Button))
2409 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther != 2 ? 2 : 1;
2410 }
2411
2412 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2413 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);
2414
2415 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2416 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);
2417
2418 LeftView.HSplitTop(Cut: LineSize * 2.0f, pTop: &Button, pBottom: &LeftView);
2419 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: "%");
2420
2421 static CButtonContainer s_HookCollNoCollResetId, s_HookCollHookableCollResetId, s_HookCollTeeCollResetId, s_HookCollTipColorResetId;
2422 static int s_HookCollToolTip;
2423
2424 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Colors of the hook collision line:"), FontSize: 13.0f,
2425 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2426
2427 Ui()->DoButtonLogic(pId: &s_HookCollToolTip, Checked: 0, pRect: &LeftView, Flags: BUTTONFLAG_NONE); // Just for the tooltip, result ignored
2428 GameClient()->m_Tooltips.DoToolTip(pId: &s_HookCollToolTip, pNearRect: &LeftView, pText: Localize(pStr: "Your movements are not taken into account when calculating the line colors"));
2429 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);
2430 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);
2431 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);
2432 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);
2433
2434 // ***** Hook collisions preview ***** //
2435 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2436 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2437 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2438
2439 auto DoHookCollision = [this](const vec2 &Pos, const float &Length, const int &Size, const ColorRGBA &Color, const ColorRGBA &TipColor, const bool &Invert) {
2440 ColorRGBA ColorModified = Color;
2441 ColorRGBA TipColorModified = TipColor;
2442 if(Invert)
2443 ColorModified = color_invert(col: ColorModified);
2444 ColorModified = ColorModified.WithAlpha(alpha: (float)g_Config.m_ClHookCollAlpha / 100);
2445 TipColorModified = TipColor.WithMultipliedAlpha(alpha: (float)g_Config.m_ClHookCollAlpha / 100);
2446 Graphics()->TextureClear();
2447 if(Size > 0)
2448 {
2449 Graphics()->QuadsBegin();
2450 Graphics()->SetColor(ColorModified);
2451 float LineWidth = 0.5f + (float)(Size - 1) * 0.25f;
2452 IGraphics::CQuadItem QuadItem(Pos.x, Pos.y - LineWidth, Length, LineWidth * 2.f);
2453 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
2454 if(TipColor.a > 0.0f)
2455 {
2456 Graphics()->SetColor(TipColorModified);
2457 IGraphics::CQuadItem TipQuadItem(Pos.x + Length, Pos.y - LineWidth, 15.f, LineWidth * 2.f);
2458 Graphics()->QuadsDrawTL(pArray: &TipQuadItem, Num: 1);
2459 }
2460 Graphics()->QuadsEnd();
2461 }
2462 else
2463 {
2464 Graphics()->LinesBegin();
2465 Graphics()->SetColor(ColorModified);
2466 IGraphics::CLineItem LineItem(Pos.x, Pos.y, Pos.x + Length, Pos.y);
2467 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
2468 if(TipColor.a > 0.0f)
2469 {
2470 Graphics()->SetColor(TipColorModified);
2471 IGraphics::CLineItem TipLineItem(Pos.x + Length, Pos.y, Pos.x + Length + 15.f, Pos.y);
2472 Graphics()->LinesDraw(pArray: &TipLineItem, Num: 1);
2473 }
2474 Graphics()->LinesEnd();
2475 }
2476 };
2477
2478 CTeeRenderInfo OwnSkinInfo;
2479 OwnSkinInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClPlayerSkin));
2480 OwnSkinInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClPlayerUseCustomColor, ColorBody: g_Config.m_ClPlayerColorBody, ColorFeet: g_Config.m_ClPlayerColorFeet);
2481 OwnSkinInfo.m_Size = 50.0f;
2482
2483 CTeeRenderInfo DummySkinInfo;
2484 DummySkinInfo.Apply(pSkin: GameClient()->m_Skins.Find(pName: g_Config.m_ClDummySkin));
2485 DummySkinInfo.ApplyColors(CustomColoredSkin: g_Config.m_ClDummyUseCustomColor, ColorBody: g_Config.m_ClDummyColorBody, ColorFeet: g_Config.m_ClDummyColorFeet);
2486 DummySkinInfo.m_Size = 50.0f;
2487
2488 vec2 TeeRenderPos, DummyRenderPos;
2489
2490 const float LineLength = 150.f;
2491 const float LeftMargin = 30.f;
2492
2493 const int TileScale = 32.0f;
2494
2495 // Toggled via checkbox later, inverts some previews
2496 static bool s_HookCollPressed = false;
2497
2498 CUIRect PreviewColl;
2499
2500 // ***** Unhookable Tile Preview *****
2501 CUIRect PreviewNoColl;
2502 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewNoColl, pBottom: &RightView);
2503 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2504 TeeRenderPos = vec2(PreviewNoColl.x + LeftMargin, PreviewNoColl.y + PreviewNoColl.h / 2.0f);
2505 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);
2506 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2507
2508 CUIRect NoHookTileRect;
2509 PreviewNoColl.VSplitRight(Cut: LineLength, pLeft: &PreviewNoColl, pRight: &NoHookTileRect);
2510 NoHookTileRect.VSplitLeft(Cut: 50.0f, pLeft: &NoHookTileRect, pRight: nullptr);
2511 NoHookTileRect.Margin(Cut: 10.0f, pOtherRect: &NoHookTileRect);
2512
2513 // Render unhookable tile
2514 Graphics()->TextureClear();
2515 Graphics()->TextureSet(Texture: GameClient()->m_MapImages.GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
2516 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2517 RenderMap()->RenderTile(x: NoHookTileRect.x, y: NoHookTileRect.y, Index: TILE_NOHOOK, Scale: TileScale, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2518
2519 // ***** Hookable Tile Preview *****
2520 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2521 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2522 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2523 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);
2524 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2525
2526 CUIRect HookTileRect;
2527 PreviewColl.VSplitRight(Cut: LineLength, pLeft: &PreviewColl, pRight: &HookTileRect);
2528 HookTileRect.VSplitLeft(Cut: 50.0f, pLeft: &HookTileRect, pRight: nullptr);
2529 HookTileRect.Margin(Cut: 10.0f, pOtherRect: &HookTileRect);
2530
2531 // Render hookable tile
2532 Graphics()->TextureClear();
2533 Graphics()->TextureSet(Texture: GameClient()->m_MapImages.GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH));
2534 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2535 RenderMap()->RenderTile(x: HookTileRect.x, y: HookTileRect.y, Index: TILE_SOLID, Scale: TileScale, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
2536
2537 // ***** Hook Dummy Preview *****
2538 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2539 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2540 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2541 DummyRenderPos = vec2(PreviewColl.x + PreviewColl.w - LineLength - 5.f + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2542 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);
2543 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &DummySkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: DummyRenderPos);
2544 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2545
2546 // ***** Hook Dummy Reverse Preview *****
2547 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2548 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2549 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2550 DummyRenderPos = vec2(PreviewColl.x + PreviewColl.w - LineLength - 5.f + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2551 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);
2552 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: DummyRenderPos);
2553 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &DummySkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2554
2555 // ***** Hook Line Tip Preview *****
2556 RightView.HSplitTop(Cut: 50.0f, pTop: &PreviewColl, pBottom: &RightView);
2557 RightView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2558 TeeRenderPos = vec2(PreviewColl.x + LeftMargin, PreviewColl.y + PreviewColl.h / 2.0f);
2559 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);
2560 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: 0, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
2561
2562 // ***** Preview +hookcoll pressed toggle *****
2563 RightView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &RightView);
2564 if(DoButton_CheckBox(pId: &s_HookCollPressed, pText: Localize(pStr: "Preview 'Hook collisions' being pressed"), Checked: s_HookCollPressed, pRect: &Button))
2565 s_HookCollPressed = !s_HookCollPressed;
2566 }
2567 else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES)
2568 {
2569 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2570
2571 // ***** Info Messages ***** //
2572 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Info Messages"), FontSize: HeadlineFontSize,
2573 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2574 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2575
2576 // General info messages settings
2577 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2578 if(DoButton_CheckBox(pId: &g_Config.m_ClShowKillMessages, pText: Localize(pStr: "Show kill messages"), Checked: g_Config.m_ClShowKillMessages, pRect: &Button))
2579 {
2580 g_Config.m_ClShowKillMessages ^= 1;
2581 }
2582
2583 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2584 if(DoButton_CheckBox(pId: &g_Config.m_ClShowFinishMessages, pText: Localize(pStr: "Show finish messages"), Checked: g_Config.m_ClShowFinishMessages, pRect: &Button))
2585 {
2586 g_Config.m_ClShowFinishMessages ^= 1;
2587 }
2588
2589 static CButtonContainer s_KillMessageNormalColorId, s_KillMessageHighlightColorId;
2590 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);
2591 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);
2592 }
2593 else if(s_CurTab == APPEARANCE_TAB_LASER)
2594 {
2595 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2596
2597 // ***** Weapons ***** //
2598 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Weapons"), FontSize: HeadlineFontSize,
2599 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2600 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2601
2602 // General weapon laser settings
2603 static CButtonContainer s_LaserRifleOutResetId, s_LaserRifleInResetId, s_LaserShotgunOutResetId, s_LaserShotgunInResetId;
2604
2605 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);
2606 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);
2607 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);
2608 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);
2609
2610 // ***** Entities ***** //
2611 LeftView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &LeftView);
2612 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Entities"), FontSize: HeadlineFontSize,
2613 Align: TEXTALIGN_ML, pRect: &LeftView, LineSize: HeadlineHeight);
2614 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2615
2616 // General entity laser settings
2617 static CButtonContainer s_LaserDoorOutResetId, s_LaserDoorInResetId, s_LaserFreezeOutResetId, s_LaserFreezeInResetId, s_LaserDraggerOutResetId, s_LaserDraggerInResetId;
2618
2619 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);
2620 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);
2621 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);
2622 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);
2623 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);
2624 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);
2625
2626 static CButtonContainer s_AllToRifleResetId, s_AllToDefaultResetId;
2627
2628 LeftView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
2629 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2630 if(DoButton_Menu(pButtonContainer: &s_AllToRifleResetId, pText: Localize(pStr: "Set all to Rifle"), Checked: 0, pRect: &Button))
2631 {
2632 g_Config.m_ClLaserShotgunOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2633 g_Config.m_ClLaserShotgunInnerColor = g_Config.m_ClLaserRifleInnerColor;
2634 g_Config.m_ClLaserDoorOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2635 g_Config.m_ClLaserDoorInnerColor = g_Config.m_ClLaserRifleInnerColor;
2636 g_Config.m_ClLaserFreezeOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2637 g_Config.m_ClLaserFreezeInnerColor = g_Config.m_ClLaserRifleInnerColor;
2638 g_Config.m_ClLaserDraggerOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
2639 g_Config.m_ClLaserDraggerInnerColor = g_Config.m_ClLaserRifleInnerColor;
2640 }
2641
2642 // values taken from the CL commands
2643 LeftView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
2644 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2645 if(DoButton_Menu(pButtonContainer: &s_AllToDefaultResetId, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &Button))
2646 {
2647 g_Config.m_ClLaserRifleOutlineColor = 11176233;
2648 g_Config.m_ClLaserRifleInnerColor = 11206591;
2649 g_Config.m_ClLaserShotgunOutlineColor = 1866773;
2650 g_Config.m_ClLaserShotgunInnerColor = 1467241;
2651 g_Config.m_ClLaserDoorOutlineColor = 7667473;
2652 g_Config.m_ClLaserDoorInnerColor = 7701379;
2653 g_Config.m_ClLaserFreezeOutlineColor = 11613223;
2654 g_Config.m_ClLaserFreezeInnerColor = 12001153;
2655 g_Config.m_ClLaserDraggerOutlineColor = 57618;
2656 g_Config.m_ClLaserDraggerInnerColor = 42398;
2657 }
2658
2659 // ***** Laser Preview ***** //
2660 Ui()->DoLabel_AutoLineSize(pText: Localize(pStr: "Preview"), FontSize: HeadlineFontSize,
2661 Align: TEXTALIGN_ML, pRect: &RightView, LineSize: HeadlineHeight);
2662 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
2663
2664 const float LaserPreviewHeight = 60.0f;
2665 CUIRect LaserPreview;
2666 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2667 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2668 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserRifleOutlineColor, InnerColor: LaserRifleInnerColor, LaserType: LASERTYPE_RIFLE);
2669
2670 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2671 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2672 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserShotgunOutlineColor, InnerColor: LaserShotgunInnerColor, LaserType: LASERTYPE_SHOTGUN);
2673
2674 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2675 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2676 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserDoorOutlineColor, InnerColor: LaserDoorInnerColor, LaserType: LASERTYPE_DOOR);
2677
2678 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2679 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2680 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserFreezeOutlineColor, InnerColor: LaserFreezeInnerColor, LaserType: LASERTYPE_FREEZE);
2681
2682 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
2683 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
2684 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserDraggerOutlineColor, InnerColor: LaserDraggerInnerColor, LaserType: LASERTYPE_DRAGGER);
2685 }
2686}
2687
2688void CMenus::RenderSettingsDDNet(CUIRect MainView)
2689{
2690 CUIRect Button, Left, Right, LeftLeft, Label;
2691
2692#if defined(CONF_AUTOUPDATE)
2693 CUIRect UpdaterRect;
2694 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &UpdaterRect);
2695 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
2696#endif
2697
2698 // demo
2699 CUIRect Demo;
2700 MainView.HSplitTop(Cut: 110.0f, pTop: &Demo, pBottom: &MainView);
2701 Demo.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Demo);
2702 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Demo"), Size: 20.0f, Align: TEXTALIGN_ML);
2703 Label.VSplitMid(pLeft: nullptr, pRight: &Label, Spacing: 20.0f);
2704 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Ghost"), Size: 20.0f, Align: TEXTALIGN_ML);
2705
2706 Demo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Demo);
2707 Demo.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
2708
2709 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2710 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))
2711 {
2712 g_Config.m_ClAutoRaceRecord ^= 1;
2713 }
2714
2715 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2716 if(DoButton_CheckBox(pId: &g_Config.m_ClReplays, pText: Localize(pStr: "Enable replays"), Checked: g_Config.m_ClReplays, pRect: &Button))
2717 {
2718 g_Config.m_ClReplays ^= 1;
2719 if(Client()->State() == IClient::STATE_ONLINE)
2720 {
2721 Client()->DemoRecorder_UpdateReplayRecorder();
2722 }
2723 }
2724
2725 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2726 if(g_Config.m_ClReplays)
2727 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);
2728
2729 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2730 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhost, pText: Localize(pStr: "Enable ghost"), Checked: g_Config.m_ClRaceGhost, pRect: &Button))
2731 {
2732 g_Config.m_ClRaceGhost ^= 1;
2733 }
2734 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"));
2735
2736 if(g_Config.m_ClRaceGhost)
2737 {
2738 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2739 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2740 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceShowGhost, pText: Localize(pStr: "Show ghost"), Checked: g_Config.m_ClRaceShowGhost, pRect: &LeftLeft))
2741 {
2742 g_Config.m_ClRaceShowGhost ^= 1;
2743 }
2744 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: "%");
2745
2746 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2747 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceSaveGhost, pText: Localize(pStr: "Save ghost"), Checked: g_Config.m_ClRaceSaveGhost, pRect: &Button))
2748 {
2749 g_Config.m_ClRaceSaveGhost ^= 1;
2750 }
2751
2752 if(g_Config.m_ClRaceSaveGhost)
2753 {
2754 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2755 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhostSaveBest, pText: Localize(pStr: "Only save improvements"), Checked: g_Config.m_ClRaceGhostSaveBest, pRect: &Button))
2756 {
2757 g_Config.m_ClRaceGhostSaveBest ^= 1;
2758 }
2759 }
2760 }
2761
2762 // gameplay
2763 CUIRect Gameplay;
2764 MainView.HSplitTop(Cut: 170.0f, pTop: &Gameplay, pBottom: &MainView);
2765 Gameplay.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Gameplay);
2766 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Gameplay"), Size: 20.0f, Align: TEXTALIGN_ML);
2767 Gameplay.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Gameplay);
2768 Gameplay.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
2769
2770 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2771 Ui()->DoScrollbarOption(pId: &g_Config.m_ClOverlayEntities, pOption: &g_Config.m_ClOverlayEntities, pRect: &Button, pStr: Localize(pStr: "Overlay entities"), Min: 0, Max: 100);
2772
2773 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2774 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2775
2776 if(DoButton_CheckBox(pId: &g_Config.m_ClTextEntities, pText: Localize(pStr: "Show text entities"), Checked: g_Config.m_ClTextEntities, pRect: &LeftLeft))
2777 g_Config.m_ClTextEntities ^= 1;
2778
2779 if(g_Config.m_ClTextEntities)
2780 {
2781 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))
2782 GameClient()->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
2783 }
2784
2785 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2786 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
2787
2788 if(DoButton_CheckBox(pId: &g_Config.m_ClShowOthers, pText: Localize(pStr: "Show others"), Checked: g_Config.m_ClShowOthers == SHOW_OTHERS_ON, pRect: &LeftLeft))
2789 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF;
2790
2791 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: "%");
2792
2793 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"));
2794
2795 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2796 static int s_ShowOwnTeamId = 0;
2797 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))
2798 {
2799 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ONLY_TEAM ? SHOW_OTHERS_ONLY_TEAM : SHOW_OTHERS_OFF;
2800 }
2801
2802 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2803 if(DoButton_CheckBox(pId: &g_Config.m_ClShowQuads, pText: Localize(pStr: "Show background quads"), Checked: g_Config.m_ClShowQuads, pRect: &Button))
2804 {
2805 g_Config.m_ClShowQuads ^= 1;
2806 }
2807 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowQuads, pNearRect: &Button, pText: Localize(pStr: "Quads are used for background decoration"));
2808
2809 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
2810 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClDefaultZoom, pOption: &g_Config.m_ClDefaultZoom, pRect: &Button, pStr: Localize(pStr: "Default zoom"), Min: 0, Max: 20))
2811 GameClient()->m_Camera.SetZoom(Target: CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: true);
2812
2813 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2814 Ui()->DoScrollbarOption(pId: &g_Config.m_ClPredictionMargin, pOption: &g_Config.m_ClPredictionMargin, pRect: &Button, pStr: Localize(pStr: "Prediction margin"), Min: 1, Max: 300);
2815
2816 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2817 if(DoButton_CheckBox(pId: &g_Config.m_ClPredictEvents, pText: Localize(pStr: "Predict events (experimental)"), Checked: g_Config.m_ClPredictEvents, pRect: &Button))
2818 {
2819 g_Config.m_ClPredictEvents ^= 1;
2820 }
2821
2822 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2823 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPing, pText: Localize(pStr: "AntiPing"), Checked: g_Config.m_ClAntiPing, pRect: &Button))
2824 {
2825 g_Config.m_ClAntiPing ^= 1;
2826 }
2827 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"));
2828
2829 if(g_Config.m_ClAntiPing)
2830 {
2831 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2832 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingPlayers, pText: Localize(pStr: "AntiPing: predict other players"), Checked: g_Config.m_ClAntiPingPlayers, pRect: &Button))
2833 {
2834 g_Config.m_ClAntiPingPlayers ^= 1;
2835 }
2836
2837 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2838 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingWeapons, pText: Localize(pStr: "AntiPing: predict weapons"), Checked: g_Config.m_ClAntiPingWeapons, pRect: &Button))
2839 {
2840 g_Config.m_ClAntiPingWeapons ^= 1;
2841 }
2842
2843 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
2844 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingGrenade, pText: Localize(pStr: "AntiPing: predict grenade paths"), Checked: g_Config.m_ClAntiPingGrenade, pRect: &Button))
2845 {
2846 g_Config.m_ClAntiPingGrenade ^= 1;
2847 }
2848 }
2849
2850 CUIRect Background, Miscellaneous;
2851 MainView.VSplitMid(pLeft: &Background, pRight: &Miscellaneous, Spacing: 20.0f);
2852
2853 // background
2854 Background.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Background);
2855 Background.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Background);
2856 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Background"), Size: 20.0f, Align: TEXTALIGN_ML);
2857
2858 ColorRGBA GreyDefault(0.5f, 0.5f, 0.5f, 1);
2859
2860 static CButtonContainer s_ResetId1;
2861 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);
2862
2863 static CButtonContainer s_ResetId2;
2864 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);
2865
2866 CUIRect EditBox, ReloadButton;
2867 Background.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Background);
2868 Background.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Background);
2869 Label.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &EditBox);
2870 EditBox.VSplitRight(Cut: 60.0f, pLeft: &EditBox, pRight: &Button);
2871 Button.VSplitMid(pLeft: &ReloadButton, pRight: &Button, Spacing: 5.0f);
2872 EditBox.VSplitRight(Cut: 5.0f, pLeft: &EditBox, pRight: nullptr);
2873
2874 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Map"), Size: 14.0f, Align: TEXTALIGN_ML);
2875
2876 static CLineInput s_BackgroundEntitiesInput(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities));
2877 Ui()->DoEditBox(pLineInput: &s_BackgroundEntitiesInput, pRect: &EditBox, FontSize: 14.0f);
2878
2879 static CButtonContainer s_BackgroundEntitiesMapPicker, s_BackgroundEntitiesReload;
2880
2881 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesReload, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &ReloadButton, Flags: BUTTONFLAG_LEFT))
2882 {
2883 GameClient()->m_Background.LoadBackground();
2884 }
2885
2886 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesMapPicker, pText: FontIcon::FOLDER, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
2887 {
2888 static SPopupMenuId s_PopupMapPickerId;
2889 static CPopupMapPickerContext s_PopupMapPickerContext;
2890 s_PopupMapPickerContext.m_pMenus = this;
2891 s_PopupMapPickerContext.MapListPopulate();
2892 Ui()->DoPopupMenu(pId: &s_PopupMapPickerId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 300.0f, Height: 250.0f, pContext: &s_PopupMapPickerContext, pfnFunc: PopupMapPicker);
2893 }
2894
2895 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
2896 const bool UseCurrentMap = str_comp(a: g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0;
2897 static int s_UseCurrentMapId = 0;
2898 if(DoButton_CheckBox(pId: &s_UseCurrentMapId, pText: Localize(pStr: "Use current map as background"), Checked: UseCurrentMap, pRect: &Button))
2899 {
2900 if(UseCurrentMap)
2901 g_Config.m_ClBackgroundEntities[0] = '\0';
2902 else
2903 str_copy(dst&: g_Config.m_ClBackgroundEntities, CURRENT_MAP);
2904 GameClient()->m_Background.LoadBackground();
2905 }
2906
2907 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
2908 if(DoButton_CheckBox(pId: &g_Config.m_ClBackgroundShowTilesLayers, pText: Localize(pStr: "Show tiles layers from BG map"), Checked: g_Config.m_ClBackgroundShowTilesLayers, pRect: &Button))
2909 g_Config.m_ClBackgroundShowTilesLayers ^= 1;
2910
2911 // miscellaneous
2912 Miscellaneous.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Miscellaneous);
2913 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
2914
2915 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Miscellaneous"), Size: 20.0f, Align: TEXTALIGN_ML);
2916
2917 static CButtonContainer s_ButtonTimeout;
2918 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
2919 if(DoButton_Menu(pButtonContainer: &s_ButtonTimeout, pText: Localize(pStr: "New random timeout code"), Checked: 0, pRect: &Button))
2920 {
2921 Client()->GenerateTimeoutSeed();
2922 }
2923
2924 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
2925 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Miscellaneous);
2926 Miscellaneous.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Miscellaneous);
2927 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Run on join"), Size: 14.0f, Align: TEXTALIGN_ML);
2928 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
2929 static CLineInput s_RunOnJoinInput(g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin));
2930 s_RunOnJoinInput.SetEmptyText(Localize(pStr: "Chat command (e.g. showall 1)"));
2931 Ui()->DoEditBox(pLineInput: &s_RunOnJoinInput, pRect: &Button, FontSize: 14.0f);
2932
2933#if defined(CONF_FAMILY_WINDOWS)
2934 static CButtonContainer s_ButtonUnregisterShell;
2935 Miscellaneous.HSplitTop(10.0f, nullptr, &Miscellaneous);
2936 Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous);
2937 if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button))
2938 {
2939 Client()->ShellUnregister();
2940 }
2941#endif
2942
2943 // Updater
2944#if defined(CONF_AUTOUPDATE)
2945 {
2946 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
2947 IUpdater::EUpdaterState State = Updater()->GetCurrentState();
2948
2949 // Update Button
2950 char aBuf[256];
2951 if(NeedUpdate && State <= IUpdater::CLEAN)
2952 {
2953 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is available:"), Client()->LatestVersion());
2954 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
2955 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
2956 static CButtonContainer s_ButtonUpdate;
2957 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &Button))
2958 {
2959 Updater()->InitiateUpdate();
2960 }
2961 }
2962 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
2963 str_copy(dst&: aBuf, src: Localize(pStr: "Updating…"));
2964 else if(State == IUpdater::NEED_RESTART)
2965 {
2966 str_copy(dst&: aBuf, src: Localize(pStr: "DDNet Client updated!"));
2967 m_NeedRestartUpdate = true;
2968 }
2969 else
2970 {
2971 str_copy(dst&: aBuf, src: Localize(pStr: "No updates available"));
2972 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
2973 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
2974 static CButtonContainer s_ButtonUpdate;
2975 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Check now"), Checked: 0, pRect: &Button))
2976 {
2977 Client()->RequestDDNetInfo();
2978 }
2979 }
2980 Ui()->DoLabel(pRect: &UpdaterRect, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
2981 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2982 }
2983#endif
2984}
2985
2986CUi::EPopupMenuFunctionResult CMenus::PopupMapPicker(void *pContext, CUIRect View, bool Active)
2987{
2988 CPopupMapPickerContext *pPopupContext = static_cast<CPopupMapPickerContext *>(pContext);
2989 CMenus *pMenus = pPopupContext->m_pMenus;
2990
2991 static CListBox s_ListBox;
2992 s_ListBox.SetActive(Active);
2993 s_ListBox.DoStart(RowHeight: 20.0f, NumItems: pPopupContext->m_vMaps.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false);
2994
2995 int MapIndex = 0;
2996 for(auto &Map : pPopupContext->m_vMaps)
2997 {
2998 MapIndex++;
2999 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Map, Selected: MapIndex == pPopupContext->m_Selection);
3000 if(!Item.m_Visible)
3001 continue;
3002
3003 CUIRect Label, Icon;
3004 Item.m_Rect.VSplitLeft(Cut: 20.0f, pLeft: &Icon, pRight: &Label);
3005
3006 char aLabelText[IO_MAX_PATH_LENGTH];
3007 str_copy(dst&: aLabelText, src: Map.m_aFilename);
3008 if(Map.m_IsDirectory)
3009 str_append(dst: aLabelText, src: "/", dst_size: sizeof(aLabelText));
3010
3011 const char *pIconType;
3012 if(!Map.m_IsDirectory)
3013 {
3014 pIconType = FontIcon::MAP;
3015 }
3016 else
3017 {
3018 if(!str_comp(a: Map.m_aFilename, b: ".."))
3019 pIconType = FontIcon::FOLDER_TREE;
3020 else
3021 pIconType = FontIcon::FOLDER;
3022 }
3023
3024 pMenus->TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
3025 pMenus->TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
3026 pMenus->Ui()->DoLabel(pRect: &Icon, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
3027 pMenus->TextRender()->SetRenderFlags(0);
3028 pMenus->TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
3029
3030 pMenus->Ui()->DoLabel(pRect: &Label, pText: aLabelText, Size: 10.0f, Align: TEXTALIGN_ML);
3031 }
3032
3033 const int NewSelected = s_ListBox.DoEnd();
3034 pPopupContext->m_Selection = NewSelected >= 0 ? NewSelected : -1;
3035 if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated())
3036 {
3037 const CMapListItem &SelectedItem = pPopupContext->m_vMaps[pPopupContext->m_Selection];
3038
3039 if(SelectedItem.m_IsDirectory)
3040 {
3041 if(!str_comp(a: SelectedItem.m_aFilename, b: ".."))
3042 {
3043 fs_parent_dir(path: pPopupContext->m_aCurrentMapFolder);
3044 }
3045 else
3046 {
3047 str_append(dst: pPopupContext->m_aCurrentMapFolder, src: "/", dst_size: sizeof(pPopupContext->m_aCurrentMapFolder));
3048 str_append(dst: pPopupContext->m_aCurrentMapFolder, src: SelectedItem.m_aFilename, dst_size: sizeof(pPopupContext->m_aCurrentMapFolder));
3049 }
3050 pPopupContext->MapListPopulate();
3051 }
3052 else
3053 {
3054 str_format(buffer: g_Config.m_ClBackgroundEntities, buffer_size: sizeof(g_Config.m_ClBackgroundEntities), format: "%s/%s", pPopupContext->m_aCurrentMapFolder, SelectedItem.m_aFilename);
3055 pMenus->GameClient()->m_Background.LoadBackground();
3056 return CUi::POPUP_CLOSE_CURRENT;
3057 }
3058 }
3059
3060 return CUi::POPUP_KEEP_OPEN;
3061}
3062
3063void CMenus::CPopupMapPickerContext::MapListPopulate()
3064{
3065 m_vMaps.clear();
3066 char aTemp[IO_MAX_PATH_LENGTH];
3067 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "maps/%s", m_aCurrentMapFolder);
3068 m_pMenus->Storage()->ListDirectoryInfo(Type: IStorage::TYPE_ALL, pPath: aTemp, pfnCallback: MapListFetchCallback, pUser: this);
3069 std::stable_sort(first: m_vMaps.begin(), last: m_vMaps.end(), comp: CompareFilenameAscending);
3070 m_Selection = -1;
3071}
3072
3073int CMenus::CPopupMapPickerContext::MapListFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
3074{
3075 CPopupMapPickerContext *pRealUser = (CPopupMapPickerContext *)pUser;
3076 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: ""))))
3077 return 0;
3078
3079 CMapListItem Item;
3080 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
3081 Item.m_IsDirectory = IsDir;
3082
3083 pRealUser->m_vMaps.emplace_back(args&: Item);
3084
3085 return 0;
3086}
3087