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