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