1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include <base/log.h>
4#include <base/math.h>
5#include <base/system.h>
6
7#include <engine/graphics.h>
8#include <engine/shared/config.h>
9#include <engine/shared/linereader.h>
10#include <engine/shared/localization.h>
11#include <engine/storage.h>
12#include <engine/textrender.h>
13#include <engine/updater.h>
14
15#include <game/generated/protocol.h>
16
17#include <game/client/animstate.h>
18#include <game/client/components/chat.h>
19#include <game/client/components/menu_background.h>
20#include <game/client/components/sounds.h>
21#include <game/client/gameclient.h>
22#include <game/client/render.h>
23#include <game/client/skin.h>
24#include <game/client/ui.h>
25#include <game/client/ui_listbox.h>
26#include <game/client/ui_scrollregion.h>
27#include <game/localization.h>
28
29#include "binds.h"
30#include "countryflags.h"
31#include "menus.h"
32#include "skins.h"
33
34#include <array>
35#include <chrono>
36#include <memory>
37#include <numeric>
38#include <string>
39#include <vector>
40
41using namespace FontIcons;
42using namespace std::chrono_literals;
43
44CMenusKeyBinder CMenus::m_Binder;
45
46CMenusKeyBinder::CMenusKeyBinder()
47{
48 m_pKeyReaderId = nullptr;
49 m_TakeKey = false;
50 m_GotKey = false;
51 m_ModifierCombination = CBinds::MODIFIER_NONE;
52}
53
54bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event)
55{
56 if(m_TakeKey)
57 {
58 int TriggeringEvent = (Event.m_Key == KEY_MOUSE_1) ? IInput::FLAG_PRESS : IInput::FLAG_RELEASE;
59 if(Event.m_Flags & TriggeringEvent)
60 {
61 m_Key = Event;
62 m_GotKey = true;
63 m_TakeKey = false;
64
65 m_ModifierCombination = CBinds::GetModifierMask(pInput: Input());
66 if(m_ModifierCombination == CBinds::GetModifierMaskOfKey(Key: Event.m_Key))
67 {
68 m_ModifierCombination = CBinds::MODIFIER_NONE;
69 }
70 }
71 return true;
72 }
73
74 return false;
75}
76
77void CMenus::RenderSettingsGeneral(CUIRect MainView)
78{
79 char aBuf[128 + IO_MAX_PATH_LENGTH];
80 CUIRect Label, Button, Left, Right, Game, ClientSettings;
81 MainView.HSplitTop(Cut: 150.0f, pTop: &Game, pBottom: &ClientSettings);
82
83 // game
84 {
85 // headline
86 Game.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Game);
87 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Game"), Size: 20.0f, Align: TEXTALIGN_ML);
88 Game.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Game);
89 Game.VSplitMid(pLeft: &Left, pRight: nullptr, Spacing: 20.0f);
90
91 // dynamic camera
92 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
93 const bool IsDyncam = g_Config.m_ClDyncam || g_Config.m_ClMouseFollowfactor > 0;
94 if(DoButton_CheckBox(pId: &g_Config.m_ClDyncam, pText: Localize(pStr: "Dynamic Camera"), Checked: IsDyncam, pRect: &Button))
95 {
96 if(IsDyncam)
97 {
98 g_Config.m_ClDyncam = 0;
99 g_Config.m_ClMouseFollowfactor = 0;
100 }
101 else
102 {
103 g_Config.m_ClDyncam = 1;
104 }
105 }
106
107 // smooth dynamic camera
108 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
109 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
110 if(g_Config.m_ClDyncam)
111 {
112 if(DoButton_CheckBox(pId: &g_Config.m_ClDyncamSmoothness, pText: Localize(pStr: "Smooth Dynamic Camera"), Checked: g_Config.m_ClDyncamSmoothness, pRect: &Button))
113 {
114 if(g_Config.m_ClDyncamSmoothness)
115 {
116 g_Config.m_ClDyncamSmoothness = 0;
117 }
118 else
119 {
120 g_Config.m_ClDyncamSmoothness = 50;
121 g_Config.m_ClDyncamStabilizing = 50;
122 }
123 }
124 }
125
126 // weapon pickup
127 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
128 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
129 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoswitchWeapons, pText: Localize(pStr: "Switch weapon on pickup"), Checked: g_Config.m_ClAutoswitchWeapons, pRect: &Button))
130 g_Config.m_ClAutoswitchWeapons ^= 1;
131
132 // weapon out of ammo autoswitch
133 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
134 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
135 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoswitchWeaponsOutOfAmmo, pText: Localize(pStr: "Switch weapon when out of ammo"), Checked: g_Config.m_ClAutoswitchWeaponsOutOfAmmo, pRect: &Button))
136 g_Config.m_ClAutoswitchWeaponsOutOfAmmo ^= 1;
137 }
138
139 // client
140 {
141 // headline
142 ClientSettings.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &ClientSettings);
143 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Client"), Size: 20.0f, Align: TEXTALIGN_ML);
144 ClientSettings.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ClientSettings);
145 ClientSettings.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
146
147 // skip main menu
148 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
149 if(DoButton_CheckBox(pId: &g_Config.m_ClSkipStartMenu, pText: Localize(pStr: "Skip the main menu"), Checked: g_Config.m_ClSkipStartMenu, pRect: &Button))
150 g_Config.m_ClSkipStartMenu ^= 1;
151
152 Left.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Left);
153 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
154 Ui()->DoScrollbarOption(pId: &g_Config.m_ClRefreshRate, pOption: &g_Config.m_ClRefreshRate, pRect: &Button, pStr: Localize(pStr: "Refresh Rate"), Min: 10, Max: 10000, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_INFINITE, pSuffix: " Hz");
155 Left.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Left);
156 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
157 static int s_LowerRefreshRate;
158 if(DoButton_CheckBox(pId: &s_LowerRefreshRate, pText: Localize(pStr: "Save power by lowering refresh rate (higher input latency)"), Checked: g_Config.m_ClRefreshRate <= 480 && g_Config.m_ClRefreshRate != 0, pRect: &Button))
159 g_Config.m_ClRefreshRate = g_Config.m_ClRefreshRate > 480 || g_Config.m_ClRefreshRate == 0 ? 480 : 0;
160
161 CUIRect SettingsButton;
162 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &SettingsButton);
163 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
164 static CButtonContainer s_SettingsButtonId;
165 if(DoButton_Menu(pButtonContainer: &s_SettingsButtonId, pText: Localize(pStr: "Settings file"), Checked: 0, pRect: &SettingsButton))
166 {
167 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, CONFIG_FILE, pBuffer: aBuf, BufferSize: sizeof(aBuf));
168 Client()->ViewFile(pFilename: aBuf);
169 }
170 GameClient()->m_Tooltips.DoToolTip(pId: &s_SettingsButtonId, pNearRect: &SettingsButton, pText: Localize(pStr: "Open the settings file"));
171
172 CUIRect ConfigButton;
173 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &ConfigButton);
174 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
175 static CButtonContainer s_ConfigButtonId;
176 if(DoButton_Menu(pButtonContainer: &s_ConfigButtonId, pText: Localize(pStr: "Config directory"), Checked: 0, pRect: &ConfigButton))
177 {
178 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "", pBuffer: aBuf, BufferSize: sizeof(aBuf));
179 Client()->ViewFile(pFilename: aBuf);
180 }
181 GameClient()->m_Tooltips.DoToolTip(pId: &s_ConfigButtonId, pNearRect: &ConfigButton, pText: Localize(pStr: "Open the directory that contains the configuration and user files"));
182
183 CUIRect DirectoryButton;
184 Left.HSplitBottom(Cut: 20.0f, pTop: &Left, pBottom: &DirectoryButton);
185 Left.HSplitBottom(Cut: 5.0f, pTop: &Left, pBottom: nullptr);
186 static CButtonContainer s_ThemesButtonId;
187 if(DoButton_Menu(pButtonContainer: &s_ThemesButtonId, pText: Localize(pStr: "Themes directory"), Checked: 0, pRect: &DirectoryButton))
188 {
189 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "themes", pBuffer: aBuf, BufferSize: sizeof(aBuf));
190 Storage()->CreateFolder(pFoldername: "themes", Type: IStorage::TYPE_SAVE);
191 Client()->ViewFile(pFilename: aBuf);
192 }
193 GameClient()->m_Tooltips.DoToolTip(pId: &s_ThemesButtonId, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom themes"));
194
195 Left.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Left);
196 RenderThemeSelection(MainView: Left);
197
198 // auto demo settings
199 {
200 Right.HSplitTop(Cut: 40.0f, pTop: nullptr, pBottom: &Right);
201 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
202 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoDemoRecord, pText: Localize(pStr: "Automatically record demos"), Checked: g_Config.m_ClAutoDemoRecord, pRect: &Button))
203 g_Config.m_ClAutoDemoRecord ^= 1;
204
205 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
206 if(g_Config.m_ClAutoDemoRecord)
207 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);
208
209 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
210 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
211 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoScreenshot, pText: Localize(pStr: "Automatically take game over screenshot"), Checked: g_Config.m_ClAutoScreenshot, pRect: &Button))
212 g_Config.m_ClAutoScreenshot ^= 1;
213
214 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
215 if(g_Config.m_ClAutoScreenshot)
216 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);
217 }
218
219 // auto statboard screenshot
220 {
221 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
222 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
223 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoStatboardScreenshot, pText: Localize(pStr: "Automatically take statboard screenshot"), Checked: g_Config.m_ClAutoStatboardScreenshot, pRect: &Button))
224 {
225 g_Config.m_ClAutoStatboardScreenshot ^= 1;
226 }
227
228 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
229 if(g_Config.m_ClAutoStatboardScreenshot)
230 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);
231 }
232
233 // auto statboard csv
234 {
235 Right.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &Right);
236 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
237 if(DoButton_CheckBox(pId: &g_Config.m_ClAutoCSV, pText: Localize(pStr: "Automatically create statboard csv"), Checked: g_Config.m_ClAutoCSV, pRect: &Button))
238 {
239 g_Config.m_ClAutoCSV ^= 1;
240 }
241
242 Right.HSplitTop(Cut: 2 * 20.0f, pTop: &Button, pBottom: &Right);
243 if(g_Config.m_ClAutoCSV)
244 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);
245 }
246 }
247}
248
249void CMenus::SetNeedSendInfo()
250{
251 if(m_Dummy)
252 m_NeedSendDummyinfo = true;
253 else
254 m_NeedSendinfo = true;
255}
256
257void CMenus::RenderSettingsPlayer(CUIRect MainView)
258{
259 CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo, QuickSearch, QuickSearchClearButton;
260 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
261 TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f);
262 TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab);
263 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
264
265 static CButtonContainer s_PlayerTabButton;
266 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))
267 {
268 m_Dummy = false;
269 }
270
271 static CButtonContainer s_DummyTabButton;
272 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))
273 {
274 m_Dummy = true;
275 }
276
277 if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_NextChangeInfo && m_pClient->m_NextChangeInfo > Client()->GameTick(Conn: g_Config.m_ClDummy))
278 {
279 char aChangeInfo[128], aTimeLeft[32];
280 str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left"), (m_pClient->m_NextChangeInfo - Client()->GameTick(Conn: g_Config.m_ClDummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed());
281 str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s", Localize(pStr: "Player info change cooldown"), aTimeLeft);
282 Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML);
283 }
284
285 static CLineInput s_NameInput;
286 static CLineInput s_ClanInput;
287
288 int *pCountry;
289 if(!m_Dummy)
290 {
291 pCountry = &g_Config.m_PlayerCountry;
292 s_NameInput.SetBuffer(pStr: g_Config.m_PlayerName, MaxSize: sizeof(g_Config.m_PlayerName));
293 s_NameInput.SetEmptyText(Client()->PlayerName());
294 s_ClanInput.SetBuffer(pStr: g_Config.m_PlayerClan, MaxSize: sizeof(g_Config.m_PlayerClan));
295 }
296 else
297 {
298 pCountry = &g_Config.m_ClDummyCountry;
299 s_NameInput.SetBuffer(pStr: g_Config.m_ClDummyName, MaxSize: sizeof(g_Config.m_ClDummyName));
300 s_NameInput.SetEmptyText(Client()->DummyName());
301 s_ClanInput.SetBuffer(pStr: g_Config.m_ClDummyClan, MaxSize: sizeof(g_Config.m_ClDummyClan));
302 }
303
304 // player name
305 CUIRect Button, Label;
306 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
307 Button.VSplitLeft(Cut: 80.0f, pLeft: &Label, pRight: &Button);
308 Button.VSplitLeft(Cut: 150.0f, pLeft: &Button, pRight: nullptr);
309 char aBuf[128];
310 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Name"));
311 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
312 if(Ui()->DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 14.0f))
313 {
314 SetNeedSendInfo();
315 }
316
317 // player clan
318 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
319 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
320 Button.VSplitLeft(Cut: 80.0f, pLeft: &Label, pRight: &Button);
321 Button.VSplitLeft(Cut: 150.0f, pLeft: &Button, pRight: nullptr);
322 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Clan"));
323 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
324 if(Ui()->DoEditBox(pLineInput: &s_ClanInput, pRect: &Button, FontSize: 14.0f))
325 {
326 SetNeedSendInfo();
327 }
328
329 // country flag selector
330 static CLineInputBuffered<25> s_FlagFilterInput;
331
332 std::vector<const CCountryFlags::CCountryFlag *> vpFilteredFlags;
333 for(size_t i = 0; i < m_pClient->m_CountryFlags.Num(); ++i)
334 {
335 const CCountryFlags::CCountryFlag *pEntry = m_pClient->m_CountryFlags.GetByIndex(Index: i);
336 if(!str_find_nocase(haystack: pEntry->m_aCountryCodeString, needle: s_FlagFilterInput.GetString()))
337 continue;
338 vpFilteredFlags.push_back(x: pEntry);
339 }
340
341 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
342 MainView.HSplitBottom(Cut: 25.0f, pTop: &MainView, pBottom: &QuickSearch);
343 int OldSelected = -1;
344 static CListBox s_ListBox;
345 s_ListBox.DoStart(RowHeight: 48.0f, NumItems: vpFilteredFlags.size(), ItemsPerRow: 10, RowsPerScroll: 3, SelectedIndex: OldSelected, pRect: &MainView);
346
347 for(size_t i = 0; i < vpFilteredFlags.size(); i++)
348 {
349 const CCountryFlags::CCountryFlag *pEntry = vpFilteredFlags[i];
350
351 if(pEntry->m_CountryCode == *pCountry)
352 OldSelected = i;
353
354 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pEntry->m_CountryCode, Selected: OldSelected >= 0 && (size_t)OldSelected == i);
355 if(!Item.m_Visible)
356 continue;
357
358 CUIRect FlagRect;
359 Item.m_Rect.Margin(Cut: 5.0f, pOtherRect: &FlagRect);
360 FlagRect.HSplitBottom(Cut: 12.0f, pTop: &FlagRect, pBottom: &Label);
361 Label.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Label);
362 const float OldWidth = FlagRect.w;
363 FlagRect.w = FlagRect.h * 2;
364 FlagRect.x += (OldWidth - FlagRect.w) / 2.0f;
365 m_pClient->m_CountryFlags.Render(CountryCode: pEntry->m_CountryCode, Color: ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), x: FlagRect.x, y: FlagRect.y, w: FlagRect.w, h: FlagRect.h);
366
367 if(pEntry->m_Texture.IsValid())
368 {
369 Ui()->DoLabel(pRect: &Label, pText: pEntry->m_aCountryCodeString, Size: 10.0f, Align: TEXTALIGN_MC);
370 }
371 }
372
373 const int NewSelected = s_ListBox.DoEnd();
374 if(OldSelected != NewSelected)
375 {
376 *pCountry = vpFilteredFlags[NewSelected]->m_CountryCode;
377 SetNeedSendInfo();
378 }
379
380 // render quick search
381 QuickSearch.VSplitLeft(Cut: 240.0f, pLeft: &QuickSearch, pRight: nullptr);
382 QuickSearch.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &QuickSearch);
383
384 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
385 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
386 Ui()->DoLabel(pRect: &QuickSearch, pText: FONT_ICON_MAGNIFYING_GLASS, Size: 14.0f, Align: TEXTALIGN_ML);
387 TextRender()->SetRenderFlags(0);
388 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
389
390 float SearchWidth = TextRender()->TextWidth(Size: 14.0f, pText: FONT_ICON_MAGNIFYING_GLASS, StrLength: -1, LineWidth: -1.0f);
391 QuickSearch.VSplitLeft(Cut: SearchWidth - 1.5f, pLeft: nullptr, pRight: &QuickSearch);
392 QuickSearch.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &QuickSearch);
393 QuickSearch.VSplitLeft(Cut: QuickSearch.w - 10.0f, pLeft: &QuickSearch, pRight: &QuickSearchClearButton);
394
395 TextRender()->SetRenderFlags(0);
396 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
397 if(Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
398 {
399 Ui()->SetActiveItem(&s_FlagFilterInput);
400 s_FlagFilterInput.SelectAll();
401 }
402 s_FlagFilterInput.SetEmptyText(Localize(pStr: "Search"));
403 Ui()->DoClearableEditBox(pLineInput: &s_FlagFilterInput, pRect: &QuickSearch, FontSize: 14.0f);
404}
405
406struct CUISkin
407{
408 const CSkin *m_pSkin;
409
410 CUISkin() :
411 m_pSkin(nullptr) {}
412 CUISkin(const CSkin *pSkin) :
413 m_pSkin(pSkin) {}
414
415 bool operator<(const CUISkin &Other) const { return str_comp_nocase(a: m_pSkin->GetName(), b: Other.m_pSkin->GetName()) < 0; }
416
417 bool operator<(const char *pOther) const { return str_comp_nocase(a: m_pSkin->GetName(), b: pOther) < 0; }
418 bool operator==(const char *pOther) const { return !str_comp_nocase(a: m_pSkin->GetName(), b: pOther); }
419};
420
421void CMenus::OnRefreshSkins()
422{
423 m_SkinListNeedsUpdate = true;
424}
425
426void CMenus::RandomSkin()
427{
428 static const float s_aSchemes[] = {1.0f / 2.0f, 1.0f / 3.0f, 1.0f / -3.0f, 1.0f / 12.0f, 1.0f / -12.0f}; // complementary, triadic, analogous
429 const bool UseCustomColor = !m_Dummy ? g_Config.m_ClPlayerUseCustomColor : g_Config.m_ClDummyUseCustomColor;
430 if(UseCustomColor)
431 {
432 float GoalSat = random_float(min: 0.3f, max: 1.0f);
433 float MaxBodyLht = 1.0f - GoalSat * GoalSat; // max allowed lightness before we start losing saturation
434
435 ColorHSLA Body;
436 Body.h = random_float();
437 Body.l = random_float(min: 0.0f, max: MaxBodyLht);
438 Body.s = clamp(val: GoalSat * GoalSat / (1.0f - Body.l), lo: 0.0f, hi: 1.0f);
439
440 ColorHSLA Feet;
441 Feet.h = std::fmod(x: Body.h + s_aSchemes[rand() % std::size(s_aSchemes)], y: 1.0f);
442 Feet.l = random_float();
443 Feet.s = clamp(val: GoalSat * GoalSat / (1.0f - Feet.l), lo: 0.0f, hi: 1.0f);
444
445 unsigned *pColorBody = !m_Dummy ? &g_Config.m_ClPlayerColorBody : &g_Config.m_ClDummyColorBody;
446 unsigned *pColorFeet = !m_Dummy ? &g_Config.m_ClPlayerColorFeet : &g_Config.m_ClDummyColorFeet;
447
448 *pColorBody = Body.Pack(Alpha: false);
449 *pColorFeet = Feet.Pack(Alpha: false);
450 }
451
452 const size_t SkinNameSize = !m_Dummy ? sizeof(g_Config.m_ClPlayerSkin) : sizeof(g_Config.m_ClDummySkin);
453 char aRandomSkinName[24];
454 str_copy(dst: aRandomSkinName, src: "default", dst_size: SkinNameSize);
455 if(!m_pClient->m_Skins.GetSkinsUnsafe().empty())
456 {
457 do
458 {
459 auto it = m_pClient->m_Skins.GetSkinsUnsafe().begin();
460 std::advance(i&: it, n: rand() % m_pClient->m_Skins.GetSkinsUnsafe().size());
461 str_copy(dst: aRandomSkinName, src: (*it).second->GetName(), dst_size: SkinNameSize);
462 } while(!str_comp(a: aRandomSkinName, b: "x_ninja") || !str_comp(a: aRandomSkinName, b: "x_spec"));
463 }
464 char *pSkinName = !m_Dummy ? g_Config.m_ClPlayerSkin : g_Config.m_ClDummySkin;
465 str_copy(dst: pSkinName, src: aRandomSkinName, dst_size: SkinNameSize);
466
467 SetNeedSendInfo();
468}
469
470void CMenus::Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
471{
472 auto *pSelf = (CMenus *)pUserData;
473 const char *pStr = pResult->GetString(Index: 0);
474 if(!CSkin::IsValidName(pName: pStr))
475 {
476 log_error("menus/settings", "Favorite skin name '%s' is not valid", pStr);
477 log_error("menus/settings", "%s", CSkin::m_aSkinNameRestrictions);
478 return;
479 }
480 pSelf->m_SkinFavorites.emplace(args&: pStr);
481 pSelf->m_SkinFavoritesChanged = true;
482}
483
484void CMenus::Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData)
485{
486 auto *pSelf = (CMenus *)pUserData;
487 const auto it = pSelf->m_SkinFavorites.find(x: pResult->GetString(Index: 0));
488 if(it != pSelf->m_SkinFavorites.end())
489 {
490 pSelf->m_SkinFavorites.erase(position: it);
491 pSelf->m_SkinFavoritesChanged = true;
492 }
493}
494
495void CMenus::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData)
496{
497 auto *pSelf = (CMenus *)pUserData;
498 pSelf->OnConfigSave(pConfigManager);
499}
500
501void CMenus::OnConfigSave(IConfigManager *pConfigManager)
502{
503 for(const auto &Entry : m_SkinFavorites)
504 {
505 char aBuffer[256];
506 str_format(buffer: aBuffer, buffer_size: std::size(aBuffer), format: "add_favorite_skin \"%s\"", Entry.c_str());
507 pConfigManager->WriteLine(pLine: aBuffer);
508 }
509}
510
511void CMenus::RenderSettingsTee(CUIRect MainView)
512{
513 CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo;
514 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
515 TabBar.VSplitMid(pLeft: &TabBar, pRight: &ChangeInfo, Spacing: 20.f);
516 TabBar.VSplitMid(pLeft: &PlayerTab, pRight: &DummyTab);
517 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
518
519 static CButtonContainer s_PlayerTabButton;
520 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))
521 {
522 m_Dummy = false;
523 }
524
525 static CButtonContainer s_DummyTabButton;
526 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))
527 {
528 m_Dummy = true;
529 }
530
531 if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_NextChangeInfo && m_pClient->m_NextChangeInfo > Client()->GameTick(Conn: g_Config.m_ClDummy))
532 {
533 char aChangeInfo[128], aTimeLeft[32];
534 str_format(buffer: aTimeLeft, buffer_size: sizeof(aTimeLeft), format: Localize(pStr: "%ds left"), (m_pClient->m_NextChangeInfo - Client()->GameTick(Conn: g_Config.m_ClDummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed());
535 str_format(buffer: aChangeInfo, buffer_size: sizeof(aChangeInfo), format: "%s: %s", Localize(pStr: "Player info change cooldown"), aTimeLeft);
536 Ui()->DoLabel(pRect: &ChangeInfo, pText: aChangeInfo, Size: 10.f, Align: TEXTALIGN_ML);
537 }
538
539 char *pSkinName;
540 size_t SkinNameSize;
541 int *pUseCustomColor;
542 unsigned *pColorBody;
543 unsigned *pColorFeet;
544 int *pEmote;
545 if(!m_Dummy)
546 {
547 pSkinName = g_Config.m_ClPlayerSkin;
548 SkinNameSize = sizeof(g_Config.m_ClPlayerSkin);
549 pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor;
550 pColorBody = &g_Config.m_ClPlayerColorBody;
551 pColorFeet = &g_Config.m_ClPlayerColorFeet;
552 pEmote = &g_Config.m_ClPlayerDefaultEyes;
553 }
554 else
555 {
556 pSkinName = g_Config.m_ClDummySkin;
557 SkinNameSize = sizeof(g_Config.m_ClDummySkin);
558 pUseCustomColor = &g_Config.m_ClDummyUseCustomColor;
559 pColorBody = &g_Config.m_ClDummyColorBody;
560 pColorFeet = &g_Config.m_ClDummyColorFeet;
561 pEmote = &g_Config.m_ClDummyDefaultEyes;
562 }
563
564 const float EyeButtonSize = 40.0f;
565 const bool RenderEyesBelow = MainView.w < 750.0f;
566 CUIRect YourSkin, Checkboxes, SkinPrefix, Eyes, Button, Label;
567 MainView.HSplitTop(Cut: 90.0f, pTop: &YourSkin, pBottom: &MainView);
568 if(RenderEyesBelow)
569 {
570 YourSkin.VSplitLeft(Cut: MainView.w * 0.45f, pLeft: &YourSkin, pRight: &Checkboxes);
571 Checkboxes.VSplitLeft(Cut: MainView.w * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
572 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
573 MainView.HSplitTop(Cut: EyeButtonSize, pTop: &Eyes, pBottom: &MainView);
574 Eyes.VSplitRight(Cut: EyeButtonSize * NUM_EMOTES + 5.0f * (NUM_EMOTES - 1), pLeft: nullptr, pRight: &Eyes);
575 }
576 else
577 {
578 YourSkin.VSplitRight(Cut: 3 * EyeButtonSize + 2 * 5.0f, pLeft: &YourSkin, pRight: &Eyes);
579 const float RemainderWidth = YourSkin.w;
580 YourSkin.VSplitLeft(Cut: RemainderWidth * 0.4f, pLeft: &YourSkin, pRight: &Checkboxes);
581 Checkboxes.VSplitLeft(Cut: RemainderWidth * 0.35f, pLeft: &Checkboxes, pRight: &SkinPrefix);
582 SkinPrefix.VSplitRight(Cut: 20.0f, pLeft: &SkinPrefix, pRight: nullptr);
583 }
584 YourSkin.VSplitRight(Cut: 20.0f, pLeft: &YourSkin, pRight: nullptr);
585 Checkboxes.VSplitRight(Cut: 20.0f, pLeft: &Checkboxes, pRight: nullptr);
586
587 // Checkboxes
588 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
589 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadSkins, pText: Localize(pStr: "Download skins"), Checked: g_Config.m_ClDownloadSkins, pRect: &Button))
590 {
591 g_Config.m_ClDownloadSkins ^= 1;
592 m_pClient->RefreshSkins();
593 }
594
595 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
596 if(DoButton_CheckBox(pId: &g_Config.m_ClDownloadCommunitySkins, pText: Localize(pStr: "Download community skins"), Checked: g_Config.m_ClDownloadCommunitySkins, pRect: &Button))
597 {
598 g_Config.m_ClDownloadCommunitySkins ^= 1;
599 m_pClient->RefreshSkins();
600 }
601
602 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
603 if(DoButton_CheckBox(pId: &g_Config.m_ClVanillaSkinsOnly, pText: Localize(pStr: "Vanilla skins only"), Checked: g_Config.m_ClVanillaSkinsOnly, pRect: &Button))
604 {
605 g_Config.m_ClVanillaSkinsOnly ^= 1;
606 m_pClient->RefreshSkins();
607 }
608
609 Checkboxes.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Checkboxes);
610 if(DoButton_CheckBox(pId: &g_Config.m_ClFatSkins, pText: Localize(pStr: "Fat skins (DDFat)"), Checked: g_Config.m_ClFatSkins, pRect: &Button))
611 {
612 g_Config.m_ClFatSkins ^= 1;
613 }
614
615 // Skin prefix
616 {
617 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &SkinPrefix);
618 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Skin prefix"), Size: 14.0f, Align: TEXTALIGN_ML);
619
620 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
621 static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix));
622 Ui()->DoClearableEditBox(pLineInput: &s_SkinPrefixInput, pRect: &Button, FontSize: 14.0f);
623
624 SkinPrefix.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &SkinPrefix);
625
626 static const char *s_apSkinPrefixes[] = {"kitty", "santa"};
627 static CButtonContainer s_aPrefixButtons[std::size(s_apSkinPrefixes)];
628 for(size_t i = 0; i < std::size(s_apSkinPrefixes); i++)
629 {
630 SkinPrefix.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &SkinPrefix);
631 Button.HMargin(Cut: 2.0f, pOtherRect: &Button);
632 if(DoButton_Menu(pButtonContainer: &s_aPrefixButtons[i], pText: s_apSkinPrefixes[i], Checked: 0, pRect: &Button))
633 {
634 str_copy(dst&: g_Config.m_ClSkinPrefix, src: s_apSkinPrefixes[i]);
635 }
636 }
637 }
638
639 // Player skin area
640 CUIRect CustomColorsButton, RandomSkinButton;
641 YourSkin.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &YourSkin);
642 YourSkin.HSplitBottom(Cut: 20.0f, pTop: &YourSkin, pBottom: &CustomColorsButton);
643 CustomColorsButton.VSplitRight(Cut: 30.0f, pLeft: &CustomColorsButton, pRight: &RandomSkinButton);
644 CustomColorsButton.VSplitRight(Cut: 20.0f, pLeft: &CustomColorsButton, pRight: nullptr);
645 YourSkin.VSplitLeft(Cut: 65.0f, pLeft: &YourSkin, pRight: &Button);
646 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
647 Button.HMargin(Cut: (Button.h - 20.0f) / 2.0f, pOtherRect: &Button);
648
649 char aBuf[128 + IO_MAX_PATH_LENGTH];
650 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: "Your skin"));
651 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
652
653 // Note: get the skin info after the settings buttons, because they can trigger a refresh
654 // which invalidates the skin.
655 const CSkin *pSkin = m_pClient->m_Skins.Find(pName: pSkinName);
656 CTeeRenderInfo OwnSkinInfo;
657 OwnSkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
658 OwnSkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin;
659 OwnSkinInfo.m_SkinMetrics = pSkin->m_Metrics;
660 OwnSkinInfo.m_CustomColoredSkin = *pUseCustomColor;
661 if(*pUseCustomColor)
662 {
663 OwnSkinInfo.m_ColorBody = color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorBody).UnclampLighting());
664 OwnSkinInfo.m_ColorFeet = color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorFeet).UnclampLighting());
665 }
666 else
667 {
668 OwnSkinInfo.m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f);
669 OwnSkinInfo.m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f);
670 }
671 OwnSkinInfo.m_Size = 50.0f;
672
673 // Tee
674 {
675 vec2 OffsetToMid;
676 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, TeeOffsetToMid&: OffsetToMid);
677 const vec2 TeeRenderPos = vec2(YourSkin.x + YourSkin.w / 2.0f, YourSkin.y + YourSkin.h / 2.0f + OffsetToMid.y);
678 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &OwnSkinInfo, Emote: *pEmote, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
679 }
680
681 // Skin name
682 static CLineInput s_SkinInput;
683 s_SkinInput.SetBuffer(pStr: pSkinName, MaxSize: SkinNameSize);
684 s_SkinInput.SetEmptyText("default");
685 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinInput, pRect: &Button, FontSize: 14.0f))
686 {
687 SetNeedSendInfo();
688 }
689
690 // Random skin button
691 static CButtonContainer s_RandomSkinButton;
692 static const char *s_apDice[] = {FONT_ICON_DICE_ONE, FONT_ICON_DICE_TWO, FONT_ICON_DICE_THREE, FONT_ICON_DICE_FOUR, FONT_ICON_DICE_FIVE, FONT_ICON_DICE_SIX};
693 static int s_CurrentDie = rand() % std::size(s_apDice);
694 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
695 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
696 if(DoButton_Menu(pButtonContainer: &s_RandomSkinButton, pText: s_apDice[s_CurrentDie], Checked: 0, pRect: &RandomSkinButton, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: -0.2f))
697 {
698 RandomSkin();
699 s_CurrentDie = rand() % std::size(s_apDice);
700 }
701 TextRender()->SetRenderFlags(0);
702 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
703 GameClient()->m_Tooltips.DoToolTip(pId: &s_RandomSkinButton, pNearRect: &RandomSkinButton, pText: Localize(pStr: "Create a random skin"));
704
705 // Custom colors button
706 if(DoButton_CheckBox(pId: pUseCustomColor, pText: Localize(pStr: "Custom colors"), Checked: *pUseCustomColor, pRect: &CustomColorsButton))
707 {
708 *pUseCustomColor = *pUseCustomColor ? 0 : 1;
709 SetNeedSendInfo();
710 }
711
712 // Default eyes
713 {
714 CTeeRenderInfo EyeSkinInfo = OwnSkinInfo;
715 EyeSkinInfo.m_Size = EyeButtonSize;
716 vec2 OffsetToMid;
717 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &EyeSkinInfo, TeeOffsetToMid&: OffsetToMid);
718
719 CUIRect EyesRow;
720 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
721 static CButtonContainer s_aEyeButtons[NUM_EMOTES];
722 for(int CurrentEyeEmote = 0; CurrentEyeEmote < NUM_EMOTES; CurrentEyeEmote++)
723 {
724 EyesRow.VSplitLeft(Cut: EyeButtonSize, pLeft: &Button, pRight: &EyesRow);
725 EyesRow.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &EyesRow);
726 if(!RenderEyesBelow && (CurrentEyeEmote + 1) % 3 == 0)
727 {
728 Eyes.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Eyes);
729 Eyes.HSplitTop(Cut: EyeButtonSize, pTop: &EyesRow, pBottom: &Eyes);
730 }
731
732 const ColorRGBA EyeButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + (*pEmote == CurrentEyeEmote ? 0.25f : 0.0f));
733 if(DoButton_Menu(pButtonContainer: &s_aEyeButtons[CurrentEyeEmote], pText: "", Checked: 0, pRect: &Button, pImageName: nullptr, Corners: IGraphics::CORNER_ALL, Rounding: 5.0f, FontFactor: 0.0f, Color: EyeButtonColor))
734 {
735 *pEmote = CurrentEyeEmote;
736 if((int)m_Dummy == g_Config.m_ClDummy)
737 GameClient()->m_Emoticon.EyeEmote(EyeEmote: CurrentEyeEmote);
738 }
739 GameClient()->m_Tooltips.DoToolTip(pId: &s_aEyeButtons[CurrentEyeEmote], pNearRect: &Button, pText: Localize(pStr: "Choose default eyes when joining a server"));
740 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));
741 }
742 }
743
744 // Custom color pickers
745 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
746 if(*pUseCustomColor)
747 {
748 CUIRect CustomColors;
749 MainView.HSplitTop(Cut: 95.0f, pTop: &CustomColors, pBottom: &MainView);
750 CUIRect aRects[2];
751 CustomColors.VSplitMid(pLeft: &aRects[0], pRight: &aRects[1], Spacing: 20.0f);
752
753 unsigned *apColors[] = {pColorBody, pColorFeet};
754 const char *apParts[] = {Localize(pStr: "Body"), Localize(pStr: "Feet")};
755
756 for(int i = 0; i < 2; i++)
757 {
758 aRects[i].HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &aRects[i]);
759 Ui()->DoLabel(pRect: &Label, pText: apParts[i], Size: 14.0f, Align: TEXTALIGN_ML);
760
761 const unsigned PrevColor = *apColors[i];
762 RenderHSLScrollbars(pRect: &aRects[i], pColor: apColors[i], Alpha: false, ClampedLight: true);
763 if(PrevColor != *apColors[i])
764 {
765 SetNeedSendInfo();
766 }
767 }
768 }
769 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
770
771 // Layout bottom controls and use remainder for skin selector
772 CUIRect QuickSearch, DatabaseButton, DirectoryButton, RefreshButton;
773 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &QuickSearch);
774 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
775 QuickSearch.VSplitLeft(Cut: 240.0f, pLeft: &QuickSearch, pRight: &DatabaseButton);
776 QuickSearch.VSplitRight(Cut: 10.0f, pLeft: &QuickSearch, pRight: nullptr);
777 DatabaseButton.VSplitLeft(Cut: 150.0f, pLeft: &DatabaseButton, pRight: &DirectoryButton);
778 DirectoryButton.VSplitRight(Cut: 175.0f, pLeft: nullptr, pRight: &DirectoryButton);
779 DirectoryButton.VSplitRight(Cut: 25.0f, pLeft: &DirectoryButton, pRight: &RefreshButton);
780 DirectoryButton.VSplitRight(Cut: 10.0f, pLeft: &DirectoryButton, pRight: nullptr);
781
782 // Skin selector
783 static std::vector<CUISkin> s_vSkinList;
784 static std::vector<CUISkin> s_vSkinListHelper;
785 static std::vector<CUISkin> s_vFavoriteSkinListHelper;
786 static int s_SkinCount = 0;
787 static CListBox s_ListBox;
788
789 // be nice to the CPU
790 static auto s_SkinLastRebuildTime = time_get_nanoseconds();
791 const auto CurTime = time_get_nanoseconds();
792 if(m_SkinListNeedsUpdate || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms)))
793 {
794 s_SkinLastRebuildTime = CurTime;
795 s_vSkinList.clear();
796 s_vSkinListHelper.clear();
797 s_vFavoriteSkinListHelper.clear();
798 // set skin count early, since Find of the skin class might load
799 // a downloading skin
800 s_SkinCount = m_pClient->m_Skins.Num();
801 m_SkinFavoritesChanged = false;
802
803 auto &&SkinNotFiltered = [&](const CSkin *pSkinToBeSelected) {
804 // filter quick search
805 if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(haystack: pSkinToBeSelected->GetName(), needle: g_Config.m_ClSkinFilterString))
806 return false;
807
808 // no special skins
809 if((pSkinToBeSelected->GetName()[0] == 'x' && pSkinToBeSelected->GetName()[1] == '_'))
810 return false;
811
812 return true;
813 };
814
815 for(const auto &it : m_SkinFavorites)
816 {
817 const CSkin *pSkinToBeSelected = m_pClient->m_Skins.FindOrNullptr(pName: it.c_str(), IgnorePrefix: true);
818
819 if(pSkinToBeSelected == nullptr || !SkinNotFiltered(pSkinToBeSelected))
820 continue;
821
822 s_vFavoriteSkinListHelper.emplace_back(args&: pSkinToBeSelected);
823 }
824 for(const auto &SkinIt : m_pClient->m_Skins.GetSkinsUnsafe())
825 {
826 const auto &pSkinToBeSelected = SkinIt.second;
827 if(!SkinNotFiltered(pSkinToBeSelected.get()))
828 continue;
829
830 if(std::find(first: m_SkinFavorites.begin(), last: m_SkinFavorites.end(), val: pSkinToBeSelected->GetName()) == m_SkinFavorites.end())
831 s_vSkinListHelper.emplace_back(args: pSkinToBeSelected.get());
832 }
833 std::sort(first: s_vSkinListHelper.begin(), last: s_vSkinListHelper.end());
834 std::sort(first: s_vFavoriteSkinListHelper.begin(), last: s_vFavoriteSkinListHelper.end());
835 s_vSkinList = s_vFavoriteSkinListHelper;
836 s_vSkinList.insert(position: s_vSkinList.end(), first: s_vSkinListHelper.begin(), last: s_vSkinListHelper.end());
837 m_SkinListNeedsUpdate = false;
838 }
839
840 int OldSelected = -1;
841 s_ListBox.DoStart(RowHeight: 50.0f, NumItems: s_vSkinList.size(), ItemsPerRow: 4, RowsPerScroll: 1, SelectedIndex: OldSelected, pRect: &MainView);
842 for(size_t i = 0; i < s_vSkinList.size(); ++i)
843 {
844 const CSkin *pSkinToBeDraw = s_vSkinList[i].m_pSkin;
845 if(str_comp(a: pSkinToBeDraw->GetName(), b: pSkinName) == 0)
846 OldSelected = i;
847
848 const CListboxItem Item = s_ListBox.DoNextItem(pId: pSkinToBeDraw, Selected: OldSelected >= 0 && (size_t)OldSelected == i);
849 if(!Item.m_Visible)
850 continue;
851
852 Item.m_Rect.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &Label);
853
854 CTeeRenderInfo Info = OwnSkinInfo;
855 Info.m_CustomColoredSkin = *pUseCustomColor;
856 Info.m_OriginalRenderSkin = pSkinToBeDraw->m_OriginalSkin;
857 Info.m_ColorableRenderSkin = pSkinToBeDraw->m_ColorableSkin;
858 Info.m_SkinMetrics = pSkinToBeDraw->m_Metrics;
859
860 vec2 OffsetToMid;
861 CRenderTools::GetRenderTeeOffsetToRenderedTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, TeeOffsetToMid&: OffsetToMid);
862 const vec2 TeeRenderPos = vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2 + OffsetToMid.y);
863 RenderTools()->RenderTee(pAnim: CAnimState::GetIdle(), pInfo: &Info, Emote: *pEmote, Dir: vec2(1.0f, 0.0f), Pos: TeeRenderPos);
864
865 SLabelProperties Props;
866 Props.m_MaxWidth = Label.w - 5.0f;
867 Ui()->DoLabel(pRect: &Label, pText: pSkinToBeDraw->GetName(), Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
868
869 if(g_Config.m_Debug)
870 {
871 const ColorRGBA BloodColor = *pUseCustomColor ? color_cast<ColorRGBA>(hsl: ColorHSLA(*pColorBody).UnclampLighting()) : pSkinToBeDraw->m_BloodColor;
872 Graphics()->TextureClear();
873 Graphics()->QuadsBegin();
874 Graphics()->SetColor(r: BloodColor.r, g: BloodColor.g, b: BloodColor.b, a: 1.0f);
875 IGraphics::CQuadItem QuadItem(Label.x, Label.y, 12.0f, 12.0f);
876 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
877 Graphics()->QuadsEnd();
878 }
879
880 // render skin favorite icon
881 {
882 const auto SkinItFav = m_SkinFavorites.find(x: pSkinToBeDraw->GetName());
883 const bool IsFav = SkinItFav != m_SkinFavorites.end();
884 CUIRect FavIcon;
885 Item.m_Rect.HSplitTop(Cut: 20.0f, pTop: &FavIcon, pBottom: nullptr);
886 FavIcon.VSplitRight(Cut: 20.0f, pLeft: nullptr, pRight: &FavIcon);
887 if(DoButton_Favorite(pButtonId: &pSkinToBeDraw->m_Metrics.m_Body, pParentId: pSkinToBeDraw, Checked: IsFav, pRect: &FavIcon))
888 {
889 if(IsFav)
890 {
891 m_SkinFavorites.erase(position: SkinItFav);
892 }
893 else
894 {
895 m_SkinFavorites.emplace(args: pSkinToBeDraw->GetName());
896 }
897 m_SkinListNeedsUpdate = true;
898 }
899 }
900 }
901
902 const int NewSelected = s_ListBox.DoEnd();
903 if(OldSelected != NewSelected)
904 {
905 str_copy(dst: pSkinName, src: s_vSkinList[NewSelected].m_pSkin->GetName(), dst_size: SkinNameSize);
906 SetNeedSendInfo();
907 }
908
909 // Quick search
910 {
911 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
912 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
913 Ui()->DoLabel(pRect: &QuickSearch, pText: FONT_ICON_MAGNIFYING_GLASS, Size: 14.0f, Align: TEXTALIGN_ML);
914 float SearchWidth = TextRender()->TextWidth(Size: 14.0f, pText: FONT_ICON_MAGNIFYING_GLASS, StrLength: -1, LineWidth: -1.0f);
915 TextRender()->SetRenderFlags(0);
916 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
917 QuickSearch.VSplitLeft(Cut: SearchWidth + 5.0f, pLeft: nullptr, pRight: &QuickSearch);
918 static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString));
919 if(Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed())
920 {
921 Ui()->SetActiveItem(&s_SkinFilterInput);
922 s_SkinFilterInput.SelectAll();
923 }
924 s_SkinFilterInput.SetEmptyText(Localize(pStr: "Search"));
925 if(Ui()->DoClearableEditBox(pLineInput: &s_SkinFilterInput, pRect: &QuickSearch, FontSize: 14.0f))
926 m_SkinListNeedsUpdate = true;
927 }
928
929 static CButtonContainer s_SkinDatabaseButton;
930 if(DoButton_Menu(pButtonContainer: &s_SkinDatabaseButton, pText: Localize(pStr: "Skin Database"), Checked: 0, pRect: &DatabaseButton))
931 {
932 Client()->ViewLink(pLink: "https://ddnet.org/skins/");
933 }
934
935 static CButtonContainer s_DirectoryButton;
936 if(DoButton_Menu(pButtonContainer: &s_DirectoryButton, pText: Localize(pStr: "Skins directory"), Checked: 0, pRect: &DirectoryButton))
937 {
938 Storage()->GetCompletePath(Type: IStorage::TYPE_SAVE, pDir: "skins", pBuffer: aBuf, BufferSize: sizeof(aBuf));
939 Storage()->CreateFolder(pFoldername: "skins", Type: IStorage::TYPE_SAVE);
940 Client()->ViewFile(pFilename: aBuf);
941 }
942 GameClient()->m_Tooltips.DoToolTip(pId: &s_DirectoryButton, pNearRect: &DirectoryButton, pText: Localize(pStr: "Open the directory to add custom skins"));
943
944 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
945 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
946 static CButtonContainer s_SkinRefreshButton;
947 if(DoButton_Menu(pButtonContainer: &s_SkinRefreshButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
948 {
949 // reset render flags for possible loading screen
950 TextRender()->SetRenderFlags(0);
951 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
952 m_pClient->RefreshSkins();
953 }
954 TextRender()->SetRenderFlags(0);
955 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
956}
957
958typedef struct
959{
960 const char *m_pName;
961 const char *m_pCommand;
962 int m_KeyId;
963 int m_ModifierCombination;
964} CKeyInfo;
965
966static CKeyInfo gs_aKeys[] =
967 {
968 {.m_pName: Localizable(pStr: "Move left"), .m_pCommand: "+left", .m_KeyId: 0, .m_ModifierCombination: 0},
969 {.m_pName: Localizable(pStr: "Move right"), .m_pCommand: "+right", .m_KeyId: 0, .m_ModifierCombination: 0},
970 {.m_pName: Localizable(pStr: "Jump"), .m_pCommand: "+jump", .m_KeyId: 0, .m_ModifierCombination: 0},
971 {.m_pName: Localizable(pStr: "Fire"), .m_pCommand: "+fire", .m_KeyId: 0, .m_ModifierCombination: 0},
972 {.m_pName: Localizable(pStr: "Hook"), .m_pCommand: "+hook", .m_KeyId: 0, .m_ModifierCombination: 0},
973 {.m_pName: Localizable(pStr: "Hook collisions"), .m_pCommand: "+showhookcoll", .m_KeyId: 0, .m_ModifierCombination: 0},
974 {.m_pName: Localizable(pStr: "Pause"), .m_pCommand: "say /pause", .m_KeyId: 0, .m_ModifierCombination: 0},
975 {.m_pName: Localizable(pStr: "Kill"), .m_pCommand: "kill", .m_KeyId: 0, .m_ModifierCombination: 0},
976 {.m_pName: Localizable(pStr: "Zoom in"), .m_pCommand: "zoom+", .m_KeyId: 0, .m_ModifierCombination: 0},
977 {.m_pName: Localizable(pStr: "Zoom out"), .m_pCommand: "zoom-", .m_KeyId: 0, .m_ModifierCombination: 0},
978 {.m_pName: Localizable(pStr: "Default zoom"), .m_pCommand: "zoom", .m_KeyId: 0, .m_ModifierCombination: 0},
979 {.m_pName: Localizable(pStr: "Show others"), .m_pCommand: "say /showothers", .m_KeyId: 0, .m_ModifierCombination: 0},
980 {.m_pName: Localizable(pStr: "Show all"), .m_pCommand: "say /showall", .m_KeyId: 0, .m_ModifierCombination: 0},
981 {.m_pName: Localizable(pStr: "Toggle dyncam"), .m_pCommand: "toggle cl_dyncam 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
982 {.m_pName: Localizable(pStr: "Toggle ghost"), .m_pCommand: "toggle cl_race_show_ghost 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
983
984 {.m_pName: Localizable(pStr: "Hammer"), .m_pCommand: "+weapon1", .m_KeyId: 0, .m_ModifierCombination: 0},
985 {.m_pName: Localizable(pStr: "Pistol"), .m_pCommand: "+weapon2", .m_KeyId: 0, .m_ModifierCombination: 0},
986 {.m_pName: Localizable(pStr: "Shotgun"), .m_pCommand: "+weapon3", .m_KeyId: 0, .m_ModifierCombination: 0},
987 {.m_pName: Localizable(pStr: "Grenade"), .m_pCommand: "+weapon4", .m_KeyId: 0, .m_ModifierCombination: 0},
988 {.m_pName: Localizable(pStr: "Laser"), .m_pCommand: "+weapon5", .m_KeyId: 0, .m_ModifierCombination: 0},
989 {.m_pName: Localizable(pStr: "Next weapon"), .m_pCommand: "+nextweapon", .m_KeyId: 0, .m_ModifierCombination: 0},
990 {.m_pName: Localizable(pStr: "Prev. weapon"), .m_pCommand: "+prevweapon", .m_KeyId: 0, .m_ModifierCombination: 0},
991
992 {.m_pName: Localizable(pStr: "Vote yes"), .m_pCommand: "vote yes", .m_KeyId: 0, .m_ModifierCombination: 0},
993 {.m_pName: Localizable(pStr: "Vote no"), .m_pCommand: "vote no", .m_KeyId: 0, .m_ModifierCombination: 0},
994
995 {.m_pName: Localizable(pStr: "Chat"), .m_pCommand: "+show_chat; chat all", .m_KeyId: 0, .m_ModifierCombination: 0},
996 {.m_pName: Localizable(pStr: "Team chat"), .m_pCommand: "+show_chat; chat team", .m_KeyId: 0, .m_ModifierCombination: 0},
997 {.m_pName: Localizable(pStr: "Converse"), .m_pCommand: "+show_chat; chat all /c ", .m_KeyId: 0, .m_ModifierCombination: 0},
998 {.m_pName: Localizable(pStr: "Chat command"), .m_pCommand: "+show_chat; chat all /", .m_KeyId: 0, .m_ModifierCombination: 0},
999 {.m_pName: Localizable(pStr: "Show chat"), .m_pCommand: "+show_chat", .m_KeyId: 0, .m_ModifierCombination: 0},
1000
1001 {.m_pName: Localizable(pStr: "Toggle dummy"), .m_pCommand: "toggle cl_dummy 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
1002 {.m_pName: Localizable(pStr: "Dummy copy"), .m_pCommand: "toggle cl_dummy_copy_moves 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
1003 {.m_pName: Localizable(pStr: "Hammerfly dummy"), .m_pCommand: "toggle cl_dummy_hammer 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
1004
1005 {.m_pName: Localizable(pStr: "Emoticon"), .m_pCommand: "+emote", .m_KeyId: 0, .m_ModifierCombination: 0},
1006 {.m_pName: Localizable(pStr: "Spectator mode"), .m_pCommand: "+spectate", .m_KeyId: 0, .m_ModifierCombination: 0},
1007 {.m_pName: Localizable(pStr: "Spectate next"), .m_pCommand: "spectate_next", .m_KeyId: 0, .m_ModifierCombination: 0},
1008 {.m_pName: Localizable(pStr: "Spectate previous"), .m_pCommand: "spectate_previous", .m_KeyId: 0, .m_ModifierCombination: 0},
1009 {.m_pName: Localizable(pStr: "Console"), .m_pCommand: "toggle_local_console", .m_KeyId: 0, .m_ModifierCombination: 0},
1010 {.m_pName: Localizable(pStr: "Remote console"), .m_pCommand: "toggle_remote_console", .m_KeyId: 0, .m_ModifierCombination: 0},
1011 {.m_pName: Localizable(pStr: "Screenshot"), .m_pCommand: "screenshot", .m_KeyId: 0, .m_ModifierCombination: 0},
1012 {.m_pName: Localizable(pStr: "Scoreboard"), .m_pCommand: "+scoreboard", .m_KeyId: 0, .m_ModifierCombination: 0},
1013 {.m_pName: Localizable(pStr: "Statboard"), .m_pCommand: "+statboard", .m_KeyId: 0, .m_ModifierCombination: 0},
1014 {.m_pName: Localizable(pStr: "Lock team"), .m_pCommand: "say /lock", .m_KeyId: 0, .m_ModifierCombination: 0},
1015 {.m_pName: Localizable(pStr: "Show entities"), .m_pCommand: "toggle cl_overlay_entities 0 100", .m_KeyId: 0, .m_ModifierCombination: 0},
1016 {.m_pName: Localizable(pStr: "Show HUD"), .m_pCommand: "toggle cl_showhud 0 1", .m_KeyId: 0, .m_ModifierCombination: 0},
1017};
1018
1019void CMenus::DoSettingsControlsButtons(int Start, int Stop, CUIRect View)
1020{
1021 for(int i = Start; i < Stop; i++)
1022 {
1023 const CKeyInfo &Key = gs_aKeys[i];
1024
1025 CUIRect Button, Label;
1026 View.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &View);
1027 Button.VSplitLeft(Cut: 135.0f, pLeft: &Label, pRight: &Button);
1028
1029 char aBuf[64];
1030 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s:", Localize(pStr: Key.m_pName));
1031
1032 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 13.0f, Align: TEXTALIGN_ML);
1033 int OldId = Key.m_KeyId, OldModifierCombination = Key.m_ModifierCombination, NewModifierCombination;
1034 int NewId = DoKeyReader(pId: &Key.m_KeyId, pRect: &Button, Key: OldId, ModifierCombination: OldModifierCombination, pNewModifierCombination: &NewModifierCombination);
1035 if(NewId != OldId || NewModifierCombination != OldModifierCombination)
1036 {
1037 if(OldId != 0 || NewId == 0)
1038 m_pClient->m_Binds.Bind(KeyId: OldId, pStr: "", FreeOnly: false, ModifierCombination: OldModifierCombination);
1039 if(NewId != 0)
1040 m_pClient->m_Binds.Bind(KeyId: NewId, pStr: Key.m_pCommand, FreeOnly: false, ModifierCombination: NewModifierCombination);
1041 }
1042
1043 View.HSplitTop(Cut: 2.0f, pTop: 0, pBottom: &View);
1044 }
1045}
1046
1047float CMenus::RenderSettingsControlsJoystick(CUIRect View)
1048{
1049 bool JoystickEnabled = g_Config.m_InpControllerEnable;
1050 int NumJoysticks = Input()->NumJoysticks();
1051 int NumOptions = 1; // expandable header
1052 if(JoystickEnabled)
1053 {
1054 NumOptions++; // message or joystick name/selection
1055 if(NumJoysticks > 0)
1056 {
1057 NumOptions += 3; // mode, ui sens, tolerance
1058 if(!g_Config.m_InpControllerAbsolute)
1059 NumOptions++; // ingame sens
1060 NumOptions += Input()->GetActiveJoystick()->GetNumAxes() + 1; // axis selection + header
1061 }
1062 }
1063 const float ButtonHeight = 20.0f;
1064 const float Spacing = 2.0f;
1065 const float BackgroundHeight = NumOptions * (ButtonHeight + Spacing) + (NumOptions == 1 ? 0.0f : Spacing);
1066 if(View.h < BackgroundHeight)
1067 return BackgroundHeight;
1068
1069 View.HSplitTop(Cut: BackgroundHeight, pTop: &View, pBottom: nullptr);
1070
1071 CUIRect Button;
1072 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1073 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1074 if(DoButton_CheckBox(pId: &g_Config.m_InpControllerEnable, pText: Localize(pStr: "Enable controller"), Checked: g_Config.m_InpControllerEnable, pRect: &Button))
1075 {
1076 g_Config.m_InpControllerEnable ^= 1;
1077 }
1078 if(JoystickEnabled)
1079 {
1080 if(NumJoysticks > 0)
1081 {
1082 // show joystick device selection if more than one available or just the joystick name if there is only one
1083 {
1084 CUIRect JoystickDropDown;
1085 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1086 View.HSplitTop(Cut: ButtonHeight, pTop: &JoystickDropDown, pBottom: &View);
1087 if(NumJoysticks > 1)
1088 {
1089 static std::vector<std::string> s_vJoystickNames;
1090 static std::vector<const char *> s_vpJoystickNames;
1091 s_vJoystickNames.resize(new_size: NumJoysticks);
1092 s_vpJoystickNames.resize(new_size: NumJoysticks);
1093
1094 for(int i = 0; i < NumJoysticks; ++i)
1095 {
1096 char aBuf[256];
1097 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d: %s", Localize(pStr: "Controller"), i, Input()->GetJoystick(Index: i)->GetName());
1098 s_vJoystickNames[i] = aBuf;
1099 s_vpJoystickNames[i] = s_vJoystickNames[i].c_str();
1100 }
1101
1102 static CUi::SDropDownState s_JoystickDropDownState;
1103 static CScrollRegion s_JoystickDropDownScrollRegion;
1104 s_JoystickDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_JoystickDropDownScrollRegion;
1105 const int CurrentJoystick = Input()->GetActiveJoystick()->GetIndex();
1106 const int NewJoystick = Ui()->DoDropDown(pRect: &JoystickDropDown, CurSelection: CurrentJoystick, pStrs: s_vpJoystickNames.data(), Num: s_vpJoystickNames.size(), State&: s_JoystickDropDownState);
1107 if(NewJoystick != CurrentJoystick)
1108 {
1109 Input()->SetActiveJoystick(NewJoystick);
1110 }
1111 }
1112 else
1113 {
1114 char aBuf[256];
1115 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s 0: %s", Localize(pStr: "Controller"), Input()->GetJoystick(Index: 0)->GetName());
1116 Ui()->DoLabel(pRect: &JoystickDropDown, pText: aBuf, Size: 13.0f, Align: TEXTALIGN_ML);
1117 }
1118 }
1119
1120 {
1121 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1122 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1123 CUIRect Label, ButtonRelative, ButtonAbsolute;
1124 Button.VSplitMid(pLeft: &Label, pRight: &Button, Spacing: 10.0f);
1125 Button.HMargin(Cut: 2.0f, pOtherRect: &Button);
1126 Button.VSplitMid(pLeft: &ButtonRelative, pRight: &ButtonAbsolute);
1127 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Ingame controller mode"), Size: 13.0f, Align: TEXTALIGN_ML);
1128 CButtonContainer s_RelativeButton;
1129 if(DoButton_Menu(pButtonContainer: &s_RelativeButton, pText: Localize(pStr: "Relative", pContext: "Ingame controller mode"), Checked: g_Config.m_InpControllerAbsolute == 0, pRect: &ButtonRelative, pImageName: nullptr, Corners: IGraphics::CORNER_L))
1130 {
1131 g_Config.m_InpControllerAbsolute = 0;
1132 }
1133 CButtonContainer s_AbsoluteButton;
1134 if(DoButton_Menu(pButtonContainer: &s_AbsoluteButton, pText: Localize(pStr: "Absolute", pContext: "Ingame controller mode"), Checked: g_Config.m_InpControllerAbsolute == 1, pRect: &ButtonAbsolute, pImageName: nullptr, Corners: IGraphics::CORNER_R))
1135 {
1136 g_Config.m_InpControllerAbsolute = 1;
1137 }
1138 }
1139
1140 if(!g_Config.m_InpControllerAbsolute)
1141 {
1142 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1143 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1144 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerSens, pOption: &g_Config.m_InpControllerSens, pRect: &Button, pStr: Localize(pStr: "Ingame controller sens."), Min: 1, Max: 500, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
1145 }
1146
1147 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1148 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1149 Ui()->DoScrollbarOption(pId: &g_Config.m_UiControllerSens, pOption: &g_Config.m_UiControllerSens, pRect: &Button, pStr: Localize(pStr: "UI controller sens."), Min: 1, Max: 500, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
1150
1151 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1152 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1153 Ui()->DoScrollbarOption(pId: &g_Config.m_InpControllerTolerance, pOption: &g_Config.m_InpControllerTolerance, pRect: &Button, pStr: Localize(pStr: "Controller jitter tolerance"), Min: 0, Max: 50);
1154
1155 View.HSplitTop(Cut: Spacing, pTop: nullptr, pBottom: &View);
1156 View.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.125f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1157 DoJoystickAxisPicker(View);
1158 }
1159 else
1160 {
1161 View.HSplitTop(Cut: View.h - ButtonHeight, pTop: nullptr, pBottom: &View);
1162 View.HSplitTop(Cut: ButtonHeight, pTop: &Button, pBottom: &View);
1163 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "No controller found. Plug in a controller."), Size: 13.0f, Align: TEXTALIGN_ML);
1164 }
1165 }
1166
1167 return BackgroundHeight;
1168}
1169
1170void CMenus::DoJoystickAxisPicker(CUIRect View)
1171{
1172 const float FontSize = 13.0f;
1173 const float RowHeight = 20.0f;
1174 const float SpacingH = 2.0f;
1175 const float AxisWidth = 0.2f * View.w;
1176 const float StatusWidth = 0.4f * View.w;
1177 const float AimBindWidth = 90.0f;
1178 const float SpacingV = (View.w - AxisWidth - StatusWidth - AimBindWidth) / 2.0f;
1179
1180 CUIRect Row, Axis, Status, AimBind;
1181 View.HSplitTop(Cut: SpacingH, pTop: nullptr, pBottom: &View);
1182 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1183 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
1184 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
1185 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
1186 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
1187 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
1188
1189 Ui()->DoLabel(pRect: &Axis, pText: Localize(pStr: "Axis"), Size: FontSize, Align: TEXTALIGN_MC);
1190 Ui()->DoLabel(pRect: &Status, pText: Localize(pStr: "Status"), Size: FontSize, Align: TEXTALIGN_MC);
1191 Ui()->DoLabel(pRect: &AimBind, pText: Localize(pStr: "Aim bind"), Size: FontSize, Align: TEXTALIGN_MC);
1192
1193 IInput::IJoystick *pJoystick = Input()->GetActiveJoystick();
1194 static int s_aActive[NUM_JOYSTICK_AXES][2];
1195 for(int i = 0; i < minimum<int>(a: pJoystick->GetNumAxes(), b: NUM_JOYSTICK_AXES); i++)
1196 {
1197 const bool Active = g_Config.m_InpControllerX == i || g_Config.m_InpControllerY == i;
1198
1199 View.HSplitTop(Cut: SpacingH, pTop: nullptr, pBottom: &View);
1200 View.HSplitTop(Cut: RowHeight, pTop: &Row, pBottom: &View);
1201 Row.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.125f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
1202 Row.VSplitLeft(Cut: AxisWidth, pLeft: &Axis, pRight: &Row);
1203 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
1204 Row.VSplitLeft(Cut: StatusWidth, pLeft: &Status, pRight: &Row);
1205 Row.VSplitLeft(Cut: SpacingV, pLeft: nullptr, pRight: &Row);
1206 Row.VSplitLeft(Cut: AimBindWidth, pLeft: &AimBind, pRight: &Row);
1207
1208 // Axis label
1209 char aBuf[16];
1210 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", i + 1);
1211 if(Active)
1212 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1213 else
1214 TextRender()->TextColor(r: 0.7f, g: 0.7f, b: 0.7f, a: 1.0f);
1215 Ui()->DoLabel(pRect: &Axis, pText: aBuf, Size: FontSize, Align: TEXTALIGN_MC);
1216
1217 // Axis status
1218 Status.HMargin(Cut: 7.0f, pOtherRect: &Status);
1219 DoJoystickBar(pRect: &Status, Current: (pJoystick->GetAxisValue(Axis: i) + 1.0f) / 2.0f, Tolerance: g_Config.m_InpControllerTolerance / 50.0f, Active);
1220
1221 // Bind to X/Y
1222 CUIRect AimBindX, AimBindY;
1223 AimBind.VSplitMid(pLeft: &AimBindX, pRight: &AimBindY);
1224 if(DoButton_CheckBox(pId: &s_aActive[i][0], pText: "X", Checked: g_Config.m_InpControllerX == i, pRect: &AimBindX))
1225 {
1226 if(g_Config.m_InpControllerY == i)
1227 g_Config.m_InpControllerY = g_Config.m_InpControllerX;
1228 g_Config.m_InpControllerX = i;
1229 }
1230 if(DoButton_CheckBox(pId: &s_aActive[i][1], pText: "Y", Checked: g_Config.m_InpControllerY == i, pRect: &AimBindY))
1231 {
1232 if(g_Config.m_InpControllerX == i)
1233 g_Config.m_InpControllerX = g_Config.m_InpControllerY;
1234 g_Config.m_InpControllerY = i;
1235 }
1236 }
1237 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
1238}
1239
1240void CMenus::DoJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active)
1241{
1242 CUIRect Handle;
1243 pRect->VSplitLeft(Cut: pRect->h, pLeft: &Handle, pRight: nullptr); // Slider size
1244 Handle.x += (pRect->w - Handle.w) * Current;
1245
1246 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, Active ? 0.25f : 0.125f), Corners: IGraphics::CORNER_ALL, Rounding: pRect->h / 2.0f);
1247
1248 CUIRect ToleranceArea = *pRect;
1249 ToleranceArea.w *= Tolerance;
1250 ToleranceArea.x += (pRect->w - ToleranceArea.w) / 2.0f;
1251 const ColorRGBA ToleranceColor = Active ? ColorRGBA(0.8f, 0.35f, 0.35f, 1.0f) : ColorRGBA(0.7f, 0.5f, 0.5f, 1.0f);
1252 ToleranceArea.Draw(Color: ToleranceColor, Corners: IGraphics::CORNER_ALL, Rounding: ToleranceArea.h / 2.0f);
1253
1254 const ColorRGBA SliderColor = Active ? ColorRGBA(0.95f, 0.95f, 0.95f, 1.0f) : ColorRGBA(0.8f, 0.8f, 0.8f, 1.0f);
1255 Handle.Draw(Color: SliderColor, Corners: IGraphics::CORNER_ALL, Rounding: Handle.h / 2.0f);
1256}
1257
1258void CMenus::RenderSettingsControls(CUIRect MainView)
1259{
1260 // this is kinda slow, but whatever
1261 for(auto &Key : gs_aKeys)
1262 Key.m_KeyId = Key.m_ModifierCombination = 0;
1263
1264 for(int Mod = 0; Mod < CBinds::MODIFIER_COMBINATION_COUNT; Mod++)
1265 {
1266 for(int KeyId = 0; KeyId < KEY_LAST; KeyId++)
1267 {
1268 const char *pBind = m_pClient->m_Binds.Get(KeyId, ModifierCombination: Mod);
1269 if(!pBind[0])
1270 continue;
1271
1272 for(auto &Key : gs_aKeys)
1273 if(str_comp(a: pBind, b: Key.m_pCommand) == 0)
1274 {
1275 Key.m_KeyId = KeyId;
1276 Key.m_ModifierCombination = Mod;
1277 break;
1278 }
1279 }
1280 }
1281
1282 // scrollable controls
1283 static float s_JoystickSettingsHeight = 0.0f; // we calculate this later and don't render until enough space is available
1284 static CScrollRegion s_ScrollRegion;
1285 vec2 ScrollOffset(0.0f, 0.0f);
1286 CScrollRegionParams ScrollParams;
1287 ScrollParams.m_ScrollUnit = 120.0f;
1288 s_ScrollRegion.Begin(pClipRect: &MainView, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
1289 MainView.y += ScrollOffset.y;
1290
1291 const float FontSize = 14.0f;
1292 const float Margin = 10.0f;
1293 const float HeaderHeight = FontSize + 5.0f + Margin;
1294
1295 CUIRect MouseSettings, MovementSettings, WeaponSettings, VotingSettings, ChatSettings, DummySettings, MiscSettings, JoystickSettings, ResetButton, Button;
1296 MainView.VSplitMid(pLeft: &MouseSettings, pRight: &VotingSettings);
1297
1298 // mouse settings
1299 {
1300 MouseSettings.VMargin(Cut: 5.0f, pOtherRect: &MouseSettings);
1301 MouseSettings.HSplitTop(Cut: 80.0f, pTop: &MouseSettings, pBottom: &JoystickSettings);
1302 if(s_ScrollRegion.AddRect(Rect: MouseSettings))
1303 {
1304 MouseSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1305 MouseSettings.VMargin(Cut: 10.0f, pOtherRect: &MouseSettings);
1306
1307 MouseSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &MouseSettings);
1308 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Mouse"), Size: FontSize, Align: TEXTALIGN_ML);
1309
1310 MouseSettings.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MouseSettings);
1311 Ui()->DoScrollbarOption(pId: &g_Config.m_InpMousesens, pOption: &g_Config.m_InpMousesens, pRect: &Button, pStr: Localize(pStr: "Ingame mouse sens."), Min: 1, Max: 500, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
1312
1313 MouseSettings.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MouseSettings);
1314
1315 MouseSettings.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MouseSettings);
1316 Ui()->DoScrollbarOption(pId: &g_Config.m_UiMousesens, pOption: &g_Config.m_UiMousesens, pRect: &Button, pStr: Localize(pStr: "UI mouse sens."), Min: 1, Max: 500, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_NOCLAMPVALUE);
1317 }
1318 }
1319
1320 // joystick settings
1321 {
1322 JoystickSettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &JoystickSettings);
1323 JoystickSettings.HSplitTop(Cut: s_JoystickSettingsHeight + HeaderHeight + Margin, pTop: &JoystickSettings, pBottom: &MovementSettings);
1324 if(s_ScrollRegion.AddRect(Rect: JoystickSettings))
1325 {
1326 JoystickSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1327 JoystickSettings.VMargin(Cut: Margin, pOtherRect: &JoystickSettings);
1328
1329 JoystickSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &JoystickSettings);
1330 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Controller"), Size: FontSize, Align: TEXTALIGN_ML);
1331
1332 s_JoystickSettingsHeight = RenderSettingsControlsJoystick(View: JoystickSettings);
1333 }
1334 }
1335
1336 // movement settings
1337 {
1338 MovementSettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &MovementSettings);
1339 MovementSettings.HSplitTop(Cut: 365.0f, pTop: &MovementSettings, pBottom: &WeaponSettings);
1340 if(s_ScrollRegion.AddRect(Rect: MovementSettings))
1341 {
1342 MovementSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1343 MovementSettings.VMargin(Cut: Margin, pOtherRect: &MovementSettings);
1344
1345 MovementSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &MovementSettings);
1346 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Movement"), Size: FontSize, Align: TEXTALIGN_ML);
1347
1348 DoSettingsControlsButtons(Start: 0, Stop: 15, View: MovementSettings);
1349 }
1350 }
1351
1352 // weapon settings
1353 {
1354 WeaponSettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &WeaponSettings);
1355 WeaponSettings.HSplitTop(Cut: 190.0f, pTop: &WeaponSettings, pBottom: &ResetButton);
1356 if(s_ScrollRegion.AddRect(Rect: WeaponSettings))
1357 {
1358 WeaponSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1359 WeaponSettings.VMargin(Cut: Margin, pOtherRect: &WeaponSettings);
1360
1361 WeaponSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &WeaponSettings);
1362 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Weapon"), Size: FontSize, Align: TEXTALIGN_ML);
1363
1364 DoSettingsControlsButtons(Start: 15, Stop: 22, View: WeaponSettings);
1365 }
1366 }
1367
1368 // defaults
1369 {
1370 ResetButton.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &ResetButton);
1371 ResetButton.HSplitTop(Cut: 40.0f, pTop: &ResetButton, pBottom: nullptr);
1372 if(s_ScrollRegion.AddRect(Rect: ResetButton))
1373 {
1374 ResetButton.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1375 ResetButton.Margin(Cut: 10.0f, pOtherRect: &ResetButton);
1376 static CButtonContainer s_DefaultButton;
1377 if(DoButton_Menu(pButtonContainer: &s_DefaultButton, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &ResetButton))
1378 {
1379 PopupConfirm(pTitle: Localize(pStr: "Reset controls"), pMessage: Localize(pStr: "Are you sure that you want to reset the controls to their defaults?"),
1380 pConfirmButtonLabel: Localize(pStr: "Reset"), pCancelButtonLabel: Localize(pStr: "Cancel"), pfnConfirmButtonCallback: &CMenus::ResetSettingsControls);
1381 }
1382 }
1383 }
1384
1385 // voting settings
1386 {
1387 VotingSettings.VMargin(Cut: 5.0f, pOtherRect: &VotingSettings);
1388 VotingSettings.HSplitTop(Cut: 80.0f, pTop: &VotingSettings, pBottom: &ChatSettings);
1389 if(s_ScrollRegion.AddRect(Rect: VotingSettings))
1390 {
1391 VotingSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1392 VotingSettings.VMargin(Cut: Margin, pOtherRect: &VotingSettings);
1393
1394 VotingSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &VotingSettings);
1395 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Voting"), Size: FontSize, Align: TEXTALIGN_ML);
1396
1397 DoSettingsControlsButtons(Start: 22, Stop: 24, View: VotingSettings);
1398 }
1399 }
1400
1401 // chat settings
1402 {
1403 ChatSettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &ChatSettings);
1404 ChatSettings.HSplitTop(Cut: 145.0f, pTop: &ChatSettings, pBottom: &DummySettings);
1405 if(s_ScrollRegion.AddRect(Rect: ChatSettings))
1406 {
1407 ChatSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1408 ChatSettings.VMargin(Cut: Margin, pOtherRect: &ChatSettings);
1409
1410 ChatSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &ChatSettings);
1411 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Chat"), Size: FontSize, Align: TEXTALIGN_ML);
1412
1413 DoSettingsControlsButtons(Start: 24, Stop: 29, View: ChatSettings);
1414 }
1415 }
1416
1417 // dummy settings
1418 {
1419 DummySettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &DummySettings);
1420 DummySettings.HSplitTop(Cut: 100.0f, pTop: &DummySettings, pBottom: &MiscSettings);
1421 if(s_ScrollRegion.AddRect(Rect: DummySettings))
1422 {
1423 DummySettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1424 DummySettings.VMargin(Cut: Margin, pOtherRect: &DummySettings);
1425
1426 DummySettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &DummySettings);
1427 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Dummy"), Size: FontSize, Align: TEXTALIGN_ML);
1428
1429 DoSettingsControlsButtons(Start: 29, Stop: 32, View: DummySettings);
1430 }
1431 }
1432
1433 // misc settings
1434 {
1435 MiscSettings.HSplitTop(Cut: Margin, pTop: nullptr, pBottom: &MiscSettings);
1436 MiscSettings.HSplitTop(Cut: 300.0f, pTop: &MiscSettings, pBottom: 0);
1437 if(s_ScrollRegion.AddRect(Rect: MiscSettings))
1438 {
1439 MiscSettings.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 10.0f);
1440 MiscSettings.VMargin(Cut: Margin, pOtherRect: &MiscSettings);
1441
1442 MiscSettings.HSplitTop(Cut: HeaderHeight, pTop: &Button, pBottom: &MiscSettings);
1443 Ui()->DoLabel(pRect: &Button, pText: Localize(pStr: "Miscellaneous"), Size: FontSize, Align: TEXTALIGN_ML);
1444
1445 DoSettingsControlsButtons(Start: 32, Stop: 44, View: MiscSettings);
1446 }
1447 }
1448
1449 s_ScrollRegion.End();
1450}
1451
1452void CMenus::ResetSettingsControls()
1453{
1454 m_pClient->m_Binds.SetDefaults();
1455
1456 g_Config.m_InpMousesens = 200;
1457 g_Config.m_UiMousesens = 200;
1458
1459 g_Config.m_InpControllerEnable = 0;
1460 g_Config.m_InpControllerGUID[0] = '\0';
1461 g_Config.m_InpControllerAbsolute = 0;
1462 g_Config.m_InpControllerSens = 100;
1463 g_Config.m_InpControllerX = 0;
1464 g_Config.m_InpControllerY = 1;
1465 g_Config.m_InpControllerTolerance = 5;
1466 g_Config.m_UiControllerSens = 100;
1467}
1468
1469void CMenus::RenderSettingsGraphics(CUIRect MainView)
1470{
1471 CUIRect Button;
1472 char aBuf[128];
1473 bool CheckSettings = false;
1474
1475 static const int MAX_RESOLUTIONS = 256;
1476 static CVideoMode s_aModes[MAX_RESOLUTIONS];
1477 static int s_NumNodes = Graphics()->GetVideoModes(pModes: s_aModes, MaxModes: MAX_RESOLUTIONS, Screen: g_Config.m_GfxScreen);
1478 static int s_GfxFsaaSamples = g_Config.m_GfxFsaaSamples;
1479 static bool s_GfxBackendChanged = false;
1480 static bool s_GfxGpuChanged = false;
1481 static int s_GfxHighdpi = g_Config.m_GfxHighdpi;
1482
1483 static int s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes;
1484
1485 static bool s_WasInit = false;
1486 static bool s_ModesReload = false;
1487 if(!s_WasInit)
1488 {
1489 s_WasInit = true;
1490
1491 Graphics()->AddWindowPropChangeListener(pFunc: []() {
1492 s_ModesReload = true;
1493 });
1494 }
1495
1496 if(s_ModesReload || g_Config.m_GfxDisplayAllVideoModes != s_InitDisplayAllVideoModes)
1497 {
1498 s_NumNodes = Graphics()->GetVideoModes(pModes: s_aModes, MaxModes: MAX_RESOLUTIONS, Screen: g_Config.m_GfxScreen);
1499 s_ModesReload = false;
1500 s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes;
1501 }
1502
1503 CUIRect ModeList, ModeLabel;
1504 MainView.VSplitLeft(Cut: 350.0f, pLeft: &MainView, pRight: &ModeList);
1505 ModeList.HSplitTop(Cut: 24.0f, pTop: &ModeLabel, pBottom: &ModeList);
1506 MainView.VSplitLeft(Cut: 340.0f, pLeft: &MainView, pRight: 0);
1507
1508 // display mode list
1509 static CListBox s_ListBox;
1510 static const float sc_RowHeightResList = 22.0f;
1511 static const float sc_FontSizeResListHeader = 12.0f;
1512 static const float sc_FontSizeResList = 10.0f;
1513
1514 {
1515 int G = std::gcd(m: g_Config.m_GfxScreenWidth, n: g_Config.m_GfxScreenHeight);
1516 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %dx%d @%dhz %d bit (%d:%d)", Localize(pStr: "Current"), (int)(g_Config.m_GfxScreenWidth * Graphics()->ScreenHiDPIScale()), (int)(g_Config.m_GfxScreenHeight * Graphics()->ScreenHiDPIScale()), g_Config.m_GfxScreenRefreshRate, g_Config.m_GfxColorDepth, g_Config.m_GfxScreenWidth / G, g_Config.m_GfxScreenHeight / G);
1517 Ui()->DoLabel(pRect: &ModeLabel, pText: aBuf, Size: sc_FontSizeResListHeader, Align: TEXTALIGN_MC);
1518 }
1519
1520 int OldSelected = -1;
1521 s_ListBox.SetActive(!Ui()->IsPopupOpen());
1522 s_ListBox.DoStart(RowHeight: sc_RowHeightResList, NumItems: s_NumNodes, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: OldSelected, pRect: &ModeList);
1523
1524 for(int i = 0; i < s_NumNodes; ++i)
1525 {
1526 const int Depth = s_aModes[i].m_Red + s_aModes[i].m_Green + s_aModes[i].m_Blue > 16 ? 24 : 16;
1527 if(g_Config.m_GfxColorDepth == Depth &&
1528 g_Config.m_GfxScreenWidth == s_aModes[i].m_WindowWidth &&
1529 g_Config.m_GfxScreenHeight == s_aModes[i].m_WindowHeight &&
1530 g_Config.m_GfxScreenRefreshRate == s_aModes[i].m_RefreshRate)
1531 {
1532 OldSelected = i;
1533 }
1534
1535 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_aModes[i], Selected: OldSelected == i);
1536 if(!Item.m_Visible)
1537 continue;
1538
1539 int G = std::gcd(m: s_aModes[i].m_CanvasWidth, n: s_aModes[i].m_CanvasHeight);
1540 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: " %dx%d @%dhz %d bit (%d:%d)", s_aModes[i].m_CanvasWidth, s_aModes[i].m_CanvasHeight, s_aModes[i].m_RefreshRate, Depth, s_aModes[i].m_CanvasWidth / G, s_aModes[i].m_CanvasHeight / G);
1541 Ui()->DoLabel(pRect: &Item.m_Rect, pText: aBuf, Size: sc_FontSizeResList, Align: TEXTALIGN_ML);
1542 }
1543
1544 const int NewSelected = s_ListBox.DoEnd();
1545 if(OldSelected != NewSelected)
1546 {
1547 const int Depth = s_aModes[NewSelected].m_Red + s_aModes[NewSelected].m_Green + s_aModes[NewSelected].m_Blue > 16 ? 24 : 16;
1548 g_Config.m_GfxColorDepth = Depth;
1549 g_Config.m_GfxScreenWidth = s_aModes[NewSelected].m_WindowWidth;
1550 g_Config.m_GfxScreenHeight = s_aModes[NewSelected].m_WindowHeight;
1551 g_Config.m_GfxScreenRefreshRate = s_aModes[NewSelected].m_RefreshRate;
1552 Graphics()->ResizeToScreen();
1553 }
1554
1555 // switches
1556 CUIRect WindowModeDropDown;
1557 MainView.HSplitTop(Cut: 20.0f, pTop: &WindowModeDropDown, pBottom: &MainView);
1558
1559 const char *apWindowModes[] = {Localize(pStr: "Windowed"), Localize(pStr: "Windowed borderless"), Localize(pStr: "Windowed fullscreen"), Localize(pStr: "Desktop fullscreen"), Localize(pStr: "Fullscreen")};
1560 static const int s_NumWindowMode = std::size(apWindowModes);
1561
1562 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));
1563
1564 static CUi::SDropDownState s_WindowModeDropDownState;
1565 static CScrollRegion s_WindowModeDropDownScrollRegion;
1566 s_WindowModeDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_WindowModeDropDownScrollRegion;
1567 const int NewWindowMode = Ui()->DoDropDown(pRect: &WindowModeDropDown, CurSelection: OldWindowMode, pStrs: apWindowModes, Num: s_NumWindowMode, State&: s_WindowModeDropDownState);
1568 if(OldWindowMode != NewWindowMode)
1569 {
1570 if(NewWindowMode == 0)
1571 Client()->SetWindowParams(FullscreenMode: 0, IsBorderless: false);
1572 else if(NewWindowMode == 1)
1573 Client()->SetWindowParams(FullscreenMode: 0, IsBorderless: true);
1574 else if(NewWindowMode == 2)
1575 Client()->SetWindowParams(FullscreenMode: 3, IsBorderless: false);
1576 else if(NewWindowMode == 3)
1577 Client()->SetWindowParams(FullscreenMode: 2, IsBorderless: false);
1578 else if(NewWindowMode == 4)
1579 Client()->SetWindowParams(FullscreenMode: 1, IsBorderless: false);
1580 }
1581
1582 if(Graphics()->GetNumScreens() > 1)
1583 {
1584 CUIRect ScreenDropDown;
1585 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1586 MainView.HSplitTop(Cut: 20.0f, pTop: &ScreenDropDown, pBottom: &MainView);
1587
1588 const int NumScreens = Graphics()->GetNumScreens();
1589 static std::vector<std::string> s_vScreenNames;
1590 static std::vector<const char *> s_vpScreenNames;
1591 s_vScreenNames.resize(new_size: NumScreens);
1592 s_vpScreenNames.resize(new_size: NumScreens);
1593
1594 for(int i = 0; i < NumScreens; ++i)
1595 {
1596 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d: %s", Localize(pStr: "Screen"), i, Graphics()->GetScreenName(Screen: i));
1597 s_vScreenNames[i] = aBuf;
1598 s_vpScreenNames[i] = s_vScreenNames[i].c_str();
1599 }
1600
1601 static CUi::SDropDownState s_ScreenDropDownState;
1602 static CScrollRegion s_ScreenDropDownScrollRegion;
1603 s_ScreenDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ScreenDropDownScrollRegion;
1604 const int NewScreen = Ui()->DoDropDown(pRect: &ScreenDropDown, CurSelection: g_Config.m_GfxScreen, pStrs: s_vpScreenNames.data(), Num: s_vpScreenNames.size(), State&: s_ScreenDropDownState);
1605 if(NewScreen != g_Config.m_GfxScreen)
1606 Client()->SwitchWindowScreen(Index: NewScreen);
1607 }
1608
1609 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1610 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1611 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "V-Sync"), Localize(pStr: "may cause delay"));
1612 if(DoButton_CheckBox(pId: &g_Config.m_GfxVsync, pText: aBuf, Checked: g_Config.m_GfxVsync, pRect: &Button))
1613 {
1614 Client()->ToggleWindowVSync();
1615 }
1616
1617 bool MultiSamplingChanged = false;
1618 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1619 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (%s)", Localize(pStr: "FSAA samples"), Localize(pStr: "may cause delay"));
1620 int GfxFsaaSamplesMouseButton = DoButton_CheckBox_Number(pId: &g_Config.m_GfxFsaaSamples, pText: aBuf, Checked: g_Config.m_GfxFsaaSamples, pRect: &Button);
1621 int CurFSAA = g_Config.m_GfxFsaaSamples == 0 ? 1 : g_Config.m_GfxFsaaSamples;
1622 if(GfxFsaaSamplesMouseButton == 1) // inc
1623 {
1624 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) + 1);
1625 if(g_Config.m_GfxFsaaSamples > 64)
1626 g_Config.m_GfxFsaaSamples = 0;
1627 MultiSamplingChanged = true;
1628 }
1629 else if(GfxFsaaSamplesMouseButton == 2) // dec
1630 {
1631 if(CurFSAA == 1)
1632 g_Config.m_GfxFsaaSamples = 64;
1633 else if(CurFSAA == 2)
1634 g_Config.m_GfxFsaaSamples = 0;
1635 else
1636 g_Config.m_GfxFsaaSamples = std::pow(x: 2, y: (int)std::log2(x: CurFSAA) - 1);
1637 MultiSamplingChanged = true;
1638 }
1639
1640 uint32_t MultiSamplingCountBackend = 0;
1641 if(MultiSamplingChanged)
1642 {
1643 if(Graphics()->SetMultiSampling(ReqMultiSamplingCount: g_Config.m_GfxFsaaSamples, MultiSamplingCountBackend))
1644 {
1645 // try again with 0 if mouse click was increasing multi sampling
1646 // else just accept the current value as is
1647 if((uint32_t)g_Config.m_GfxFsaaSamples > MultiSamplingCountBackend && GfxFsaaSamplesMouseButton == 1)
1648 Graphics()->SetMultiSampling(ReqMultiSamplingCount: 0, MultiSamplingCountBackend);
1649 g_Config.m_GfxFsaaSamples = (int)MultiSamplingCountBackend;
1650 }
1651 else
1652 {
1653 CheckSettings = true;
1654 }
1655 }
1656
1657 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1658 if(DoButton_CheckBox(pId: &g_Config.m_GfxHighDetail, pText: Localize(pStr: "High Detail"), Checked: g_Config.m_GfxHighDetail, pRect: &Button))
1659 g_Config.m_GfxHighDetail ^= 1;
1660 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_GfxHighDetail, pNearRect: &Button, pText: Localize(pStr: "Allows maps to render with more detail"));
1661
1662 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1663 if(DoButton_CheckBox(pId: &g_Config.m_GfxHighdpi, pText: Localize(pStr: "Use high DPI"), Checked: g_Config.m_GfxHighdpi, pRect: &Button))
1664 {
1665 CheckSettings = true;
1666 g_Config.m_GfxHighdpi ^= 1;
1667 }
1668
1669 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1670 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, pSuffix: " Hz");
1671
1672 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1673 static CButtonContainer s_UiColorResetId;
1674 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);
1675
1676 // Backend list
1677 struct SMenuBackendInfo
1678 {
1679 int m_Major = 0;
1680 int m_Minor = 0;
1681 int m_Patch = 0;
1682 const char *m_pBackendName = "";
1683 bool m_Found = false;
1684 };
1685 std::array<std::array<SMenuBackendInfo, EGraphicsDriverAgeType::GRAPHICS_DRIVER_AGE_TYPE_COUNT>, EBackendType::BACKEND_TYPE_COUNT> aaSupportedBackends{};
1686 uint32_t FoundBackendCount = 0;
1687 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1688 {
1689 if(EBackendType(i) == BACKEND_TYPE_AUTO)
1690 continue;
1691 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1692 {
1693 auto &Info = aaSupportedBackends[i][n];
1694 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)))
1695 {
1696 // don't count blocked opengl drivers
1697 if(EBackendType(i) != BACKEND_TYPE_OPENGL || EGraphicsDriverAgeType(n) == GRAPHICS_DRIVER_AGE_TYPE_LEGACY || g_Config.m_GfxDriverIsBlocked == 0)
1698 {
1699 Info.m_Found = true;
1700 ++FoundBackendCount;
1701 }
1702 }
1703 }
1704 }
1705
1706 if(FoundBackendCount > 1)
1707 {
1708 CUIRect Text, BackendDropDown;
1709 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1710 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1711 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1712 MainView.HSplitTop(Cut: 20.0f, pTop: &BackendDropDown, pBottom: &MainView);
1713 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Renderer"), Size: 16.0f, Align: TEXTALIGN_MC);
1714
1715 static std::vector<std::string> s_vBackendIdNames;
1716 static std::vector<const char *> s_vpBackendIdNamesCStr;
1717 static std::vector<SMenuBackendInfo> s_vBackendInfos;
1718
1719 size_t BackendCount = FoundBackendCount + 1;
1720 s_vBackendIdNames.resize(new_size: BackendCount);
1721 s_vpBackendIdNamesCStr.resize(new_size: BackendCount);
1722 s_vBackendInfos.resize(new_size: BackendCount);
1723
1724 char aTmpBackendName[256];
1725
1726 auto IsInfoDefault = [](const SMenuBackendInfo &CheckInfo) {
1727 return str_comp_nocase(a: CheckInfo.m_pBackendName, b: CConfig::ms_pGfxBackend) == 0 && CheckInfo.m_Major == CConfig::ms_GfxGLMajor && CheckInfo.m_Minor == CConfig::ms_GfxGLMinor && CheckInfo.m_Patch == CConfig::ms_GfxGLPatch;
1728 };
1729
1730 int OldSelectedBackend = -1;
1731 uint32_t CurCounter = 0;
1732 for(uint32_t i = 0; i < BACKEND_TYPE_COUNT; ++i)
1733 {
1734 for(uint32_t n = 0; n < GRAPHICS_DRIVER_AGE_TYPE_COUNT; ++n)
1735 {
1736 auto &Info = aaSupportedBackends[i][n];
1737 if(Info.m_Found)
1738 {
1739 bool IsDefault = IsInfoDefault(Info);
1740 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") : "");
1741 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1742 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1743 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)
1744 {
1745 OldSelectedBackend = CurCounter;
1746 }
1747
1748 s_vBackendInfos[CurCounter] = Info;
1749 ++CurCounter;
1750 }
1751 }
1752 }
1753
1754 if(OldSelectedBackend != -1)
1755 {
1756 // no custom selected
1757 BackendCount -= 1;
1758 }
1759 else
1760 {
1761 // custom selected one
1762 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);
1763 s_vBackendIdNames[CurCounter] = aTmpBackendName;
1764 s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str();
1765 OldSelectedBackend = CurCounter;
1766
1767 s_vBackendInfos[CurCounter].m_pBackendName = "custom";
1768 s_vBackendInfos[CurCounter].m_Major = g_Config.m_GfxGLMajor;
1769 s_vBackendInfos[CurCounter].m_Minor = g_Config.m_GfxGLMinor;
1770 s_vBackendInfos[CurCounter].m_Patch = g_Config.m_GfxGLPatch;
1771 }
1772
1773 static int s_OldSelectedBackend = -1;
1774 if(s_OldSelectedBackend == -1)
1775 s_OldSelectedBackend = OldSelectedBackend;
1776
1777 static CUi::SDropDownState s_BackendDropDownState;
1778 static CScrollRegion s_BackendDropDownScrollRegion;
1779 s_BackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_BackendDropDownScrollRegion;
1780 const int NewBackend = Ui()->DoDropDown(pRect: &BackendDropDown, CurSelection: OldSelectedBackend, pStrs: s_vpBackendIdNamesCStr.data(), Num: BackendCount, State&: s_BackendDropDownState);
1781 if(OldSelectedBackend != NewBackend)
1782 {
1783 str_copy(dst&: g_Config.m_GfxBackend, src: s_vBackendInfos[NewBackend].m_pBackendName);
1784 g_Config.m_GfxGLMajor = s_vBackendInfos[NewBackend].m_Major;
1785 g_Config.m_GfxGLMinor = s_vBackendInfos[NewBackend].m_Minor;
1786 g_Config.m_GfxGLPatch = s_vBackendInfos[NewBackend].m_Patch;
1787
1788 CheckSettings = true;
1789 s_GfxBackendChanged = s_OldSelectedBackend != NewBackend;
1790 }
1791 }
1792
1793 // GPU list
1794 const auto &GpuList = Graphics()->GetGpus();
1795 if(GpuList.m_vGpus.size() > 1)
1796 {
1797 CUIRect Text, GpuDropDown;
1798 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
1799 MainView.HSplitTop(Cut: 20.0f, pTop: &Text, pBottom: &MainView);
1800 MainView.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &MainView);
1801 MainView.HSplitTop(Cut: 20.0f, pTop: &GpuDropDown, pBottom: &MainView);
1802 Ui()->DoLabel(pRect: &Text, pText: Localize(pStr: "Graphics card"), Size: 16.0f, Align: TEXTALIGN_MC);
1803
1804 static std::vector<const char *> s_vpGpuIdNames;
1805
1806 size_t GpuCount = GpuList.m_vGpus.size() + 1;
1807 s_vpGpuIdNames.resize(new_size: GpuCount);
1808
1809 char aCurDeviceName[256 + 4];
1810
1811 int OldSelectedGpu = -1;
1812 for(size_t i = 0; i < GpuCount; ++i)
1813 {
1814 if(i == 0)
1815 {
1816 str_format(buffer: aCurDeviceName, buffer_size: sizeof(aCurDeviceName), format: "%s (%s)", Localize(pStr: "auto"), GpuList.m_AutoGpu.m_aName);
1817 s_vpGpuIdNames[i] = aCurDeviceName;
1818 if(str_comp(a: "auto", b: g_Config.m_GfxGpuName) == 0)
1819 {
1820 OldSelectedGpu = 0;
1821 }
1822 }
1823 else
1824 {
1825 s_vpGpuIdNames[i] = GpuList.m_vGpus[i - 1].m_aName;
1826 if(str_comp(a: GpuList.m_vGpus[i - 1].m_aName, b: g_Config.m_GfxGpuName) == 0)
1827 {
1828 OldSelectedGpu = i;
1829 }
1830 }
1831 }
1832
1833 static int s_OldSelectedGpu = -1;
1834 if(s_OldSelectedGpu == -1)
1835 s_OldSelectedGpu = OldSelectedGpu;
1836
1837 static CUi::SDropDownState s_GpuDropDownState;
1838 static CScrollRegion s_GpuDropDownScrollRegion;
1839 s_GpuDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_GpuDropDownScrollRegion;
1840 const int NewGpu = Ui()->DoDropDown(pRect: &GpuDropDown, CurSelection: OldSelectedGpu, pStrs: s_vpGpuIdNames.data(), Num: GpuCount, State&: s_GpuDropDownState);
1841 if(OldSelectedGpu != NewGpu)
1842 {
1843 if(NewGpu == 0)
1844 str_copy(dst&: g_Config.m_GfxGpuName, src: "auto");
1845 else
1846 str_copy(dst&: g_Config.m_GfxGpuName, src: GpuList.m_vGpus[NewGpu - 1].m_aName);
1847 CheckSettings = true;
1848 s_GfxGpuChanged = NewGpu != s_OldSelectedGpu;
1849 }
1850 }
1851
1852 // check if the new settings require a restart
1853 if(CheckSettings)
1854 {
1855 m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples &&
1856 !s_GfxBackendChanged &&
1857 !s_GfxGpuChanged &&
1858 s_GfxHighdpi == g_Config.m_GfxHighdpi);
1859 }
1860}
1861
1862void CMenus::RenderSettingsSound(CUIRect MainView)
1863{
1864 static int s_SndEnable = g_Config.m_SndEnable;
1865
1866 CUIRect Button;
1867 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1868 if(DoButton_CheckBox(pId: &g_Config.m_SndEnable, pText: Localize(pStr: "Use sounds"), Checked: g_Config.m_SndEnable, pRect: &Button))
1869 {
1870 g_Config.m_SndEnable ^= 1;
1871 UpdateMusicState();
1872 m_NeedRestartSound = g_Config.m_SndEnable && !s_SndEnable;
1873 }
1874
1875 if(!g_Config.m_SndEnable)
1876 return;
1877
1878 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1879 if(DoButton_CheckBox(pId: &g_Config.m_SndMusic, pText: Localize(pStr: "Play background music"), Checked: g_Config.m_SndMusic, pRect: &Button))
1880 {
1881 g_Config.m_SndMusic ^= 1;
1882 UpdateMusicState();
1883 }
1884
1885 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1886 if(DoButton_CheckBox(pId: &g_Config.m_SndNonactiveMute, pText: Localize(pStr: "Mute when not active"), Checked: g_Config.m_SndNonactiveMute, pRect: &Button))
1887 g_Config.m_SndNonactiveMute ^= 1;
1888
1889 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1890 if(DoButton_CheckBox(pId: &g_Config.m_SndGame, pText: Localize(pStr: "Enable game sounds"), Checked: g_Config.m_SndGame, pRect: &Button))
1891 g_Config.m_SndGame ^= 1;
1892
1893 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1894 if(DoButton_CheckBox(pId: &g_Config.m_SndGun, pText: Localize(pStr: "Enable gun sound"), Checked: g_Config.m_SndGun, pRect: &Button))
1895 g_Config.m_SndGun ^= 1;
1896
1897 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1898 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))
1899 g_Config.m_SndLongPain ^= 1;
1900
1901 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1902 if(DoButton_CheckBox(pId: &g_Config.m_SndServerMessage, pText: Localize(pStr: "Enable server message sound"), Checked: g_Config.m_SndServerMessage, pRect: &Button))
1903 g_Config.m_SndServerMessage ^= 1;
1904
1905 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1906 if(DoButton_CheckBox(pId: &g_Config.m_SndChat, pText: Localize(pStr: "Enable regular chat sound"), Checked: g_Config.m_SndChat, pRect: &Button))
1907 g_Config.m_SndChat ^= 1;
1908
1909 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1910 if(DoButton_CheckBox(pId: &g_Config.m_SndTeamChat, pText: Localize(pStr: "Enable team chat sound"), Checked: g_Config.m_SndTeamChat, pRect: &Button))
1911 g_Config.m_SndTeamChat ^= 1;
1912
1913 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1914 if(DoButton_CheckBox(pId: &g_Config.m_SndHighlight, pText: Localize(pStr: "Enable highlighted chat sound"), Checked: g_Config.m_SndHighlight, pRect: &Button))
1915 g_Config.m_SndHighlight ^= 1;
1916
1917 // volume slider
1918 {
1919 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1920 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1921 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: "%");
1922 }
1923
1924 // volume slider game sounds
1925 {
1926 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1927 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1928 Ui()->DoScrollbarOption(pId: &g_Config.m_SndGameSoundVolume, pOption: &g_Config.m_SndGameSoundVolume, pRect: &Button, pStr: Localize(pStr: "Game sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1929 }
1930
1931 // volume slider gui sounds
1932 {
1933 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1934 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1935 Ui()->DoScrollbarOption(pId: &g_Config.m_SndChatSoundVolume, pOption: &g_Config.m_SndChatSoundVolume, pRect: &Button, pStr: Localize(pStr: "Chat sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1936 }
1937
1938 // volume slider map sounds
1939 {
1940 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1941 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1942 Ui()->DoScrollbarOption(pId: &g_Config.m_SndMapSoundVolume, pOption: &g_Config.m_SndMapSoundVolume, pRect: &Button, pStr: Localize(pStr: "Map sound volume"), Min: 0, Max: 100, pScale: &CUi::ms_LogarithmicScrollbarScale, Flags: 0u, pSuffix: "%");
1943 }
1944
1945 // volume slider background music
1946 {
1947 MainView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &MainView);
1948 MainView.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &MainView);
1949 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: "%");
1950 }
1951}
1952
1953bool CMenus::RenderLanguageSelection(CUIRect MainView)
1954{
1955 static int s_SelectedLanguage = -2; // -2 = unloaded, -1 = unset
1956 static CListBox s_ListBox;
1957
1958 if(s_SelectedLanguage == -2)
1959 {
1960 s_SelectedLanguage = -1;
1961 for(size_t i = 0; i < g_Localization.Languages().size(); i++)
1962 {
1963 if(str_comp(a: g_Localization.Languages()[i].m_FileName.c_str(), b: g_Config.m_ClLanguagefile) == 0)
1964 {
1965 s_SelectedLanguage = i;
1966 s_ListBox.ScrollToSelected();
1967 break;
1968 }
1969 }
1970 }
1971
1972 const int OldSelected = s_SelectedLanguage;
1973
1974 s_ListBox.DoStart(RowHeight: 24.0f, NumItems: g_Localization.Languages().size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_SelectedLanguage, pRect: &MainView);
1975
1976 for(const auto &Language : g_Localization.Languages())
1977 {
1978 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()));
1979 if(!Item.m_Visible)
1980 continue;
1981
1982 CUIRect FlagRect, Label;
1983 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h * 2.0f, pLeft: &FlagRect, pRight: &Label);
1984 FlagRect.VMargin(Cut: 6.0f, pOtherRect: &FlagRect);
1985 FlagRect.HMargin(Cut: 3.0f, pOtherRect: &FlagRect);
1986 m_pClient->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);
1987
1988 Ui()->DoLabel(pRect: &Label, pText: Language.m_Name.c_str(), Size: 16.0f, Align: TEXTALIGN_ML);
1989 }
1990
1991 s_SelectedLanguage = s_ListBox.DoEnd();
1992
1993 if(OldSelected != s_SelectedLanguage)
1994 {
1995 str_copy(dst&: g_Config.m_ClLanguagefile, src: g_Localization.Languages()[s_SelectedLanguage].m_FileName.c_str());
1996 GameClient()->OnLanguageChange();
1997 }
1998
1999 return s_ListBox.WasItemActivated();
2000}
2001
2002void CMenus::RenderSettings(CUIRect MainView)
2003{
2004 // render background
2005 CUIRect Button, TabBar, RestartBar;
2006 MainView.VSplitRight(Cut: 120.0f, pLeft: &MainView, pRight: &TabBar);
2007 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
2008 MainView.Margin(Cut: 20.0f, pOtherRect: &MainView);
2009
2010 const bool NeedRestart = m_NeedRestartGraphics || m_NeedRestartSound || m_NeedRestartUpdate;
2011 if(NeedRestart)
2012 {
2013 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &RestartBar);
2014 MainView.HSplitBottom(Cut: 10.0f, pTop: &MainView, pBottom: nullptr);
2015 }
2016
2017 TabBar.HSplitTop(Cut: 50.0f, pTop: &Button, pBottom: &TabBar);
2018 Button.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_BR, Rounding: 10.0f);
2019
2020 const char *apTabs[SETTINGS_LENGTH] = {
2021 Localize(pStr: "Language"),
2022 Localize(pStr: "General"),
2023 Localize(pStr: "Player"),
2024 Localize(pStr: "Tee"),
2025 Localize(pStr: "Appearance"),
2026 Localize(pStr: "Controls"),
2027 Localize(pStr: "Graphics"),
2028 Localize(pStr: "Sound"),
2029 Localize(pStr: "DDNet"),
2030 Localize(pStr: "Assets")};
2031 static CButtonContainer s_aTabButtons[SETTINGS_LENGTH];
2032
2033 for(int i = 0; i < SETTINGS_LENGTH; i++)
2034 {
2035 TabBar.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &TabBar);
2036 TabBar.HSplitTop(Cut: 26.0f, pTop: &Button, pBottom: &TabBar);
2037 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]))
2038 g_Config.m_UiSettingsPage = i;
2039 }
2040
2041 if(g_Config.m_UiSettingsPage == SETTINGS_LANGUAGE)
2042 {
2043 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_LANGUAGE);
2044 RenderLanguageSelection(MainView);
2045 }
2046 else if(g_Config.m_UiSettingsPage == SETTINGS_GENERAL)
2047 {
2048 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GENERAL);
2049 RenderSettingsGeneral(MainView);
2050 }
2051 else if(g_Config.m_UiSettingsPage == SETTINGS_PLAYER)
2052 {
2053 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_PLAYER);
2054 RenderSettingsPlayer(MainView);
2055 }
2056 else if(g_Config.m_UiSettingsPage == SETTINGS_TEE)
2057 {
2058 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_TEE);
2059 RenderSettingsTee(MainView);
2060 }
2061 else if(g_Config.m_UiSettingsPage == SETTINGS_APPEARANCE)
2062 {
2063 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_APPEARANCE);
2064 RenderSettingsAppearance(MainView);
2065 }
2066 else if(g_Config.m_UiSettingsPage == SETTINGS_CONTROLS)
2067 {
2068 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_CONTROLS);
2069 RenderSettingsControls(MainView);
2070 }
2071 else if(g_Config.m_UiSettingsPage == SETTINGS_GRAPHICS)
2072 {
2073 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_GRAPHICS);
2074 RenderSettingsGraphics(MainView);
2075 }
2076 else if(g_Config.m_UiSettingsPage == SETTINGS_SOUND)
2077 {
2078 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_SOUND);
2079 RenderSettingsSound(MainView);
2080 }
2081 else if(g_Config.m_UiSettingsPage == SETTINGS_DDNET)
2082 {
2083 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_DDNET);
2084 RenderSettingsDDNet(MainView);
2085 }
2086 else if(g_Config.m_UiSettingsPage == SETTINGS_ASSETS)
2087 {
2088 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_SETTINGS_ASSETS);
2089 RenderSettingsCustom(MainView);
2090 }
2091 else
2092 {
2093 dbg_assert(false, "ui_settings_page invalid");
2094 }
2095
2096 if(NeedRestart)
2097 {
2098 CUIRect RestartWarning, RestartButton;
2099 RestartBar.VSplitRight(Cut: 125.0f, pLeft: &RestartWarning, pRight: &RestartButton);
2100 RestartWarning.VSplitRight(Cut: 10.0f, pLeft: &RestartWarning, pRight: nullptr);
2101 if(m_NeedRestartUpdate)
2102 {
2103 TextRender()->TextColor(r: 1.0f, g: 0.4f, b: 0.4f, a: 1.0f);
2104 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "DDNet Client needs to be restarted to complete update!"), Size: 14.0f, Align: TEXTALIGN_ML);
2105 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2106 }
2107 else
2108 {
2109 Ui()->DoLabel(pRect: &RestartWarning, pText: Localize(pStr: "You must restart the game for all settings to take effect."), Size: 14.0f, Align: TEXTALIGN_ML);
2110 }
2111
2112 static CButtonContainer s_RestartButton;
2113 if(DoButton_Menu(pButtonContainer: &s_RestartButton, pText: Localize(pStr: "Restart"), Checked: 0, pRect: &RestartButton))
2114 {
2115 if(Client()->State() == IClient::STATE_ONLINE || m_pClient->Editor()->HasUnsavedData())
2116 {
2117 m_Popup = POPUP_RESTART;
2118 }
2119 else
2120 {
2121 Client()->Restart();
2122 }
2123 }
2124 }
2125}
2126
2127ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, bool ClampedLight)
2128{
2129 ColorHSLA Color(*pColor, Alpha);
2130 CUIRect Preview, Button, Label;
2131 char aBuf[32];
2132 float *apComponent[] = {&Color.h, &Color.s, &Color.l, &Color.a};
2133 const char *apLabels[] = {Localize(pStr: "Hue"), Localize(pStr: "Sat."), Localize(pStr: "Lht."), Localize(pStr: "Alpha")};
2134
2135 float SizePerEntry = 20.0f;
2136 float MarginPerEntry = 5.0f;
2137
2138 float OffY = (SizePerEntry + MarginPerEntry) * (3 + (Alpha ? 1 : 0)) - 40.0f;
2139 pRect->VSplitLeft(Cut: 40.0f, pLeft: &Preview, pRight: pRect);
2140 Preview.HSplitTop(Cut: OffY / 2.0f, NULL, pBottom: &Preview);
2141 Preview.HSplitTop(Cut: 40.0f, pTop: &Preview, NULL);
2142
2143 Graphics()->TextureClear();
2144 {
2145 const float SizeBorder = 5.0f;
2146 Graphics()->SetColor(ColorRGBA(0.15f, 0.15f, 0.15f, 1));
2147 int TmpCont = Graphics()->CreateRectQuadContainer(x: Preview.x - SizeBorder / 2.0f, y: Preview.y - SizeBorder / 2.0f, w: Preview.w + SizeBorder, h: Preview.h + SizeBorder, r: 4.0f + SizeBorder / 2.0f, Corners: IGraphics::CORNER_ALL);
2148 Graphics()->RenderQuadContainer(ContainerIndex: TmpCont, QuadDrawNum: -1);
2149 Graphics()->DeleteQuadContainer(ContainerIndex&: TmpCont);
2150 }
2151 ColorHSLA RenderColorHSLA(Color.r, Color.g, Color.b, Color.a);
2152 if(ClampedLight)
2153 RenderColorHSLA = RenderColorHSLA.UnclampLighting();
2154 Graphics()->SetColor(color_cast<ColorRGBA>(hsl: RenderColorHSLA));
2155 int TmpCont = Graphics()->CreateRectQuadContainer(x: Preview.x, y: Preview.y, w: Preview.w, h: Preview.h, r: 4.0f, Corners: IGraphics::CORNER_ALL);
2156 Graphics()->RenderQuadContainer(ContainerIndex: TmpCont, QuadDrawNum: -1);
2157 Graphics()->DeleteQuadContainer(ContainerIndex&: TmpCont);
2158
2159 auto &&RenderHSLColorsRect = [&](CUIRect *pColorRect) {
2160 Graphics()->TextureClear();
2161 Graphics()->TrianglesBegin();
2162
2163 float CurXOff = pColorRect->x;
2164 float SizeColor = pColorRect->w / 6;
2165
2166 // red to yellow
2167 {
2168 IGraphics::CColorVertex Array[4] = {
2169 IGraphics::CColorVertex(0, 1, 0, 0, 1),
2170 IGraphics::CColorVertex(1, 1, 1, 0, 1),
2171 IGraphics::CColorVertex(2, 1, 0, 0, 1),
2172 IGraphics::CColorVertex(3, 1, 1, 0, 1)};
2173 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2174
2175 IGraphics::CFreeformItem Freeform(
2176 CurXOff, pColorRect->y,
2177 CurXOff + SizeColor, pColorRect->y,
2178 CurXOff, pColorRect->y + pColorRect->h,
2179 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2180 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2181 }
2182
2183 // yellow to green
2184 CurXOff += SizeColor;
2185 {
2186 IGraphics::CColorVertex Array[4] = {
2187 IGraphics::CColorVertex(0, 1, 1, 0, 1),
2188 IGraphics::CColorVertex(1, 0, 1, 0, 1),
2189 IGraphics::CColorVertex(2, 1, 1, 0, 1),
2190 IGraphics::CColorVertex(3, 0, 1, 0, 1)};
2191 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2192
2193 IGraphics::CFreeformItem Freeform(
2194 CurXOff, pColorRect->y,
2195 CurXOff + SizeColor, pColorRect->y,
2196 CurXOff, pColorRect->y + pColorRect->h,
2197 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2198 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2199 }
2200
2201 CurXOff += SizeColor;
2202 // green to turquoise
2203 {
2204 IGraphics::CColorVertex Array[4] = {
2205 IGraphics::CColorVertex(0, 0, 1, 0, 1),
2206 IGraphics::CColorVertex(1, 0, 1, 1, 1),
2207 IGraphics::CColorVertex(2, 0, 1, 0, 1),
2208 IGraphics::CColorVertex(3, 0, 1, 1, 1)};
2209 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2210
2211 IGraphics::CFreeformItem Freeform(
2212 CurXOff, pColorRect->y,
2213 CurXOff + SizeColor, pColorRect->y,
2214 CurXOff, pColorRect->y + pColorRect->h,
2215 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2216 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2217 }
2218
2219 CurXOff += SizeColor;
2220 // turquoise to blue
2221 {
2222 IGraphics::CColorVertex Array[4] = {
2223 IGraphics::CColorVertex(0, 0, 1, 1, 1),
2224 IGraphics::CColorVertex(1, 0, 0, 1, 1),
2225 IGraphics::CColorVertex(2, 0, 1, 1, 1),
2226 IGraphics::CColorVertex(3, 0, 0, 1, 1)};
2227 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2228
2229 IGraphics::CFreeformItem Freeform(
2230 CurXOff, pColorRect->y,
2231 CurXOff + SizeColor, pColorRect->y,
2232 CurXOff, pColorRect->y + pColorRect->h,
2233 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2234 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2235 }
2236
2237 CurXOff += SizeColor;
2238 // blue to purple
2239 {
2240 IGraphics::CColorVertex Array[4] = {
2241 IGraphics::CColorVertex(0, 0, 0, 1, 1),
2242 IGraphics::CColorVertex(1, 1, 0, 1, 1),
2243 IGraphics::CColorVertex(2, 0, 0, 1, 1),
2244 IGraphics::CColorVertex(3, 1, 0, 1, 1)};
2245 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2246
2247 IGraphics::CFreeformItem Freeform(
2248 CurXOff, pColorRect->y,
2249 CurXOff + SizeColor, pColorRect->y,
2250 CurXOff, pColorRect->y + pColorRect->h,
2251 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2252 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2253 }
2254
2255 CurXOff += SizeColor;
2256 // purple to red
2257 {
2258 IGraphics::CColorVertex Array[4] = {
2259 IGraphics::CColorVertex(0, 1, 0, 1, 1),
2260 IGraphics::CColorVertex(1, 1, 0, 0, 1),
2261 IGraphics::CColorVertex(2, 1, 0, 1, 1),
2262 IGraphics::CColorVertex(3, 1, 0, 0, 1)};
2263 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2264
2265 IGraphics::CFreeformItem Freeform(
2266 CurXOff, pColorRect->y,
2267 CurXOff + SizeColor, pColorRect->y,
2268 CurXOff, pColorRect->y + pColorRect->h,
2269 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2270 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2271 }
2272
2273 Graphics()->TrianglesEnd();
2274 };
2275
2276 auto &&RenderHSLSatRect = [&](CUIRect *pColorRect, ColorRGBA &CurColor) {
2277 Graphics()->TextureClear();
2278 Graphics()->TrianglesBegin();
2279
2280 float CurXOff = pColorRect->x;
2281 float SizeColor = pColorRect->w;
2282
2283 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColor);
2284 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColor);
2285
2286 LeftColor.g = 0;
2287 RightColor.g = 1;
2288
2289 ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
2290 ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
2291
2292 // saturation
2293 {
2294 IGraphics::CColorVertex Array[4] = {
2295 IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2296 IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1),
2297 IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2298 IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)};
2299 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2300
2301 IGraphics::CFreeformItem Freeform(
2302 CurXOff, pColorRect->y,
2303 CurXOff + SizeColor, pColorRect->y,
2304 CurXOff, pColorRect->y + pColorRect->h,
2305 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2306 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2307 }
2308
2309 Graphics()->TrianglesEnd();
2310 };
2311
2312 auto &&RenderHSLLightRect = [&](CUIRect *pColorRect, ColorRGBA &CurColorSat) {
2313 Graphics()->TextureClear();
2314 Graphics()->TrianglesBegin();
2315
2316 float CurXOff = pColorRect->x;
2317 float SizeColor = pColorRect->w / (ClampedLight ? 1.0f : 2.0f);
2318
2319 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColorSat);
2320 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColorSat);
2321
2322 LeftColor.b = ColorHSLA::DARKEST_LGT;
2323 RightColor.b = 1;
2324
2325 ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
2326 ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
2327
2328 if(!ClampedLight)
2329 CurXOff += SizeColor;
2330
2331 // light
2332 {
2333 IGraphics::CColorVertex Array[4] = {
2334 IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2335 IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1),
2336 IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2337 IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)};
2338 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2339
2340 IGraphics::CFreeformItem Freeform(
2341 CurXOff, pColorRect->y,
2342 CurXOff + SizeColor, pColorRect->y,
2343 CurXOff, pColorRect->y + pColorRect->h,
2344 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2345 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2346 }
2347
2348 if(!ClampedLight)
2349 {
2350 CurXOff -= SizeColor;
2351 LeftColor.b = 0;
2352 RightColor.b = ColorHSLA::DARKEST_LGT;
2353
2354 RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
2355 LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
2356
2357 IGraphics::CColorVertex Array[4] = {
2358 IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2359 IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1),
2360 IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1),
2361 IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)};
2362 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2363
2364 IGraphics::CFreeformItem Freeform(
2365 CurXOff, pColorRect->y,
2366 CurXOff + SizeColor, pColorRect->y,
2367 CurXOff, pColorRect->y + pColorRect->h,
2368 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2369 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2370 }
2371
2372 Graphics()->TrianglesEnd();
2373 };
2374
2375 auto &&RenderHSLAlphaRect = [&](CUIRect *pColorRect, ColorRGBA &CurColorFull) {
2376 Graphics()->TextureClear();
2377 Graphics()->TrianglesBegin();
2378
2379 float CurXOff = pColorRect->x;
2380 float SizeColor = pColorRect->w;
2381
2382 ColorHSLA RightColor = color_cast<ColorHSLA>(rgb: CurColorFull);
2383 ColorHSLA LeftColor = color_cast<ColorHSLA>(rgb: CurColorFull);
2384
2385 LeftColor.a = 0;
2386 RightColor.a = 1;
2387
2388 ColorRGBA RightColorRGBA = color_cast<ColorRGBA>(hsl: RightColor);
2389 ColorRGBA LeftColorRGBA = color_cast<ColorRGBA>(hsl: LeftColor);
2390
2391 // alpha
2392 {
2393 IGraphics::CColorVertex Array[4] = {
2394 IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, LeftColorRGBA.a),
2395 IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, RightColorRGBA.a),
2396 IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, LeftColorRGBA.a),
2397 IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, RightColorRGBA.a)};
2398 Graphics()->SetColorVertex(pArray: Array, Num: 4);
2399
2400 IGraphics::CFreeformItem Freeform(
2401 CurXOff, pColorRect->y,
2402 CurXOff + SizeColor, pColorRect->y,
2403 CurXOff, pColorRect->y + pColorRect->h,
2404 CurXOff + SizeColor, pColorRect->y + pColorRect->h);
2405 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2406 }
2407
2408 Graphics()->TrianglesEnd();
2409 };
2410
2411 for(int i = 0; i < 3 + Alpha; i++)
2412 {
2413 pRect->HSplitTop(Cut: SizePerEntry, pTop: &Button, pBottom: pRect);
2414 pRect->HSplitTop(Cut: MarginPerEntry, NULL, pBottom: pRect);
2415 Button.VSplitLeft(Cut: 10.0f, pLeft: 0, pRight: &Button);
2416 Button.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &Button);
2417
2418 Button.Draw(Color: ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), Corners: IGraphics::CORNER_ALL, Rounding: 1.0f);
2419
2420 CUIRect Rail;
2421 Button.Margin(Cut: 2.0f, pOtherRect: &Rail);
2422
2423 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %03d", apLabels[i], (int)(*apComponent[i] * 255));
2424 Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
2425
2426 ColorHSLA CurColorPureHSLA(RenderColorHSLA.r, 1, 0.5f, 1);
2427 ColorRGBA CurColorPure = color_cast<ColorRGBA>(hsl: CurColorPureHSLA);
2428 ColorRGBA ColorInner(1, 1, 1, 0.25f);
2429
2430 if(i == 0)
2431 {
2432 ColorInner = CurColorPure;
2433 RenderHSLColorsRect(&Rail);
2434 }
2435 else if(i == 1)
2436 {
2437 RenderHSLSatRect(&Rail, CurColorPure);
2438 ColorInner = color_cast<ColorRGBA>(hsl: ColorHSLA(CurColorPureHSLA.r, *apComponent[1], CurColorPureHSLA.b, 1));
2439 }
2440 else if(i == 2)
2441 {
2442 ColorRGBA CurColorSat = color_cast<ColorRGBA>(hsl: ColorHSLA(CurColorPureHSLA.r, *apComponent[1], 0.5f, 1));
2443 RenderHSLLightRect(&Rail, CurColorSat);
2444 float LightVal = *apComponent[2];
2445 if(ClampedLight)
2446 LightVal = ColorHSLA::DARKEST_LGT + LightVal * (1.0f - ColorHSLA::DARKEST_LGT);
2447 ColorInner = color_cast<ColorRGBA>(hsl: ColorHSLA(CurColorPureHSLA.r, *apComponent[1], LightVal, 1));
2448 }
2449 else if(i == 3)
2450 {
2451 ColorRGBA CurColorFull = color_cast<ColorRGBA>(hsl: ColorHSLA(CurColorPureHSLA.r, *apComponent[1], *apComponent[2], 1));
2452 RenderHSLAlphaRect(&Rail, CurColorFull);
2453 float LightVal = *apComponent[2];
2454 if(ClampedLight)
2455 LightVal = ColorHSLA::DARKEST_LGT + LightVal * (1.0f - ColorHSLA::DARKEST_LGT);
2456 ColorInner = color_cast<ColorRGBA>(hsl: ColorHSLA(CurColorPureHSLA.r, *apComponent[1], LightVal, *apComponent[3]));
2457 }
2458
2459 *apComponent[i] = Ui()->DoScrollbarH(pId: &((char *)pColor)[i], pRect: &Button, Current: *apComponent[i], pColorInner: &ColorInner);
2460 }
2461
2462 *pColor = Color.Pack(Alpha);
2463 return Color;
2464}
2465
2466enum
2467{
2468 APPEARANCE_TAB_HUD = 0,
2469 APPEARANCE_TAB_CHAT = 1,
2470 APPEARANCE_TAB_NAME_PLATE = 2,
2471 APPEARANCE_TAB_HOOK_COLLISION = 3,
2472 APPEARANCE_TAB_INFO_MESSAGES = 4,
2473 APPEARANCE_TAB_LASER = 5,
2474 NUMBER_OF_APPEARANCE_TABS = 6,
2475};
2476
2477void CMenus::RenderSettingsAppearance(CUIRect MainView)
2478{
2479 char aBuf[128];
2480 static int s_CurTab = 0;
2481
2482 CUIRect TabBar, LeftView, RightView, Button, Label;
2483
2484 MainView.HSplitTop(Cut: 20.0f, pTop: &TabBar, pBottom: &MainView);
2485 const float TabWidth = TabBar.w / NUMBER_OF_APPEARANCE_TABS;
2486 static CButtonContainer s_aPageTabs[NUMBER_OF_APPEARANCE_TABS] = {};
2487 const char *apTabNames[NUMBER_OF_APPEARANCE_TABS] = {
2488 Localize(pStr: "HUD"),
2489 Localize(pStr: "Chat"),
2490 Localize(pStr: "Name Plate"),
2491 Localize(pStr: "Hook Collisions"),
2492 Localize(pStr: "Info Messages"),
2493 Localize(pStr: "Laser")};
2494
2495 for(int Tab = APPEARANCE_TAB_HUD; Tab < NUMBER_OF_APPEARANCE_TABS; ++Tab)
2496 {
2497 TabBar.VSplitLeft(Cut: TabWidth, pLeft: &Button, pRight: &TabBar);
2498 const int Corners = Tab == APPEARANCE_TAB_HUD ? IGraphics::CORNER_L : Tab == NUMBER_OF_APPEARANCE_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE;
2499 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))
2500 {
2501 s_CurTab = Tab;
2502 }
2503 }
2504
2505 MainView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &MainView);
2506
2507 const float LineSize = 20.0f;
2508 const float ColorPickerLineSize = 25.0f;
2509 const float HeadlineFontSize = 20.0f;
2510 const float HeadlineHeight = 30.0f;
2511 const float MarginSmall = 5.0f;
2512 const float MarginBetweenViews = 20.0f;
2513
2514 const float ColorPickerLabelSize = 13.0f;
2515 const float ColorPickerLineSpacing = 5.0f;
2516
2517 if(s_CurTab == APPEARANCE_TAB_HUD)
2518 {
2519 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2520
2521 // ***** HUD ***** //
2522 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
2523 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "HUD"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2524 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2525
2526 // Switch of the entire HUD
2527 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhud, pText: Localize(pStr: "Show ingame HUD"), pValue: &g_Config.m_ClShowhud, pRect: &LeftView, VMargin: LineSize);
2528
2529 // Switches of the various normal HUD elements
2530 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudHealthAmmo, pText: Localize(pStr: "Show health, shields and ammo"), pValue: &g_Config.m_ClShowhudHealthAmmo, pRect: &LeftView, VMargin: LineSize);
2531 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudScore, pText: Localize(pStr: "Show score"), pValue: &g_Config.m_ClShowhudScore, pRect: &LeftView, VMargin: LineSize);
2532 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowLocalTimeAlways, pText: Localize(pStr: "Show local time always"), pValue: &g_Config.m_ClShowLocalTimeAlways, pRect: &LeftView, VMargin: LineSize);
2533
2534 // Settings of the HUD element for votes
2535 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowVotesAfterVoting, pText: Localize(pStr: "Show votes window after voting"), pValue: &g_Config.m_ClShowVotesAfterVoting, pRect: &LeftView, VMargin: LineSize);
2536
2537 // ***** DDRace HUD ***** //
2538 RightView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &RightView);
2539 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "DDRace HUD"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2540 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
2541
2542 // Switches of various DDRace HUD elements
2543 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowIds, pText: Localize(pStr: "Show client IDs in scoreboard"), pValue: &g_Config.m_ClShowIds, pRect: &RightView, VMargin: LineSize);
2544 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDDRace, pText: Localize(pStr: "Show DDRace HUD"), pValue: &g_Config.m_ClShowhudDDRace, pRect: &RightView, VMargin: LineSize);
2545 if(g_Config.m_ClShowhudDDRace)
2546 {
2547 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudJumpsIndicator, pText: Localize(pStr: "Show jumps indicator"), pValue: &g_Config.m_ClShowhudJumpsIndicator, pRect: &RightView, VMargin: LineSize);
2548 }
2549 else
2550 {
2551 RightView.HSplitTop(Cut: LineSize, pTop: nullptr, pBottom: &RightView); // Create empty space for hidden option
2552 }
2553
2554 // Switch for dummy actions display
2555 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudDummyActions, pText: Localize(pStr: "Show dummy actions"), pValue: &g_Config.m_ClShowhudDummyActions, pRect: &RightView, VMargin: LineSize);
2556
2557 // Player movement information display settings
2558 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerPosition, pText: Localize(pStr: "Show player position"), pValue: &g_Config.m_ClShowhudPlayerPosition, pRect: &RightView, VMargin: LineSize);
2559 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerSpeed, pText: Localize(pStr: "Show player speed"), pValue: &g_Config.m_ClShowhudPlayerSpeed, pRect: &RightView, VMargin: LineSize);
2560 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowhudPlayerAngle, pText: Localize(pStr: "Show player target angle"), pValue: &g_Config.m_ClShowhudPlayerAngle, pRect: &RightView, VMargin: LineSize);
2561
2562 // Freeze bar settings
2563 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClShowFreezeBars, pText: Localize(pStr: "Show freeze bars"), pValue: &g_Config.m_ClShowFreezeBars, pRect: &RightView, VMargin: LineSize);
2564 RightView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &RightView);
2565 if(g_Config.m_ClShowFreezeBars)
2566 {
2567 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: "%");
2568 }
2569 }
2570 else if(s_CurTab == APPEARANCE_TAB_CHAT)
2571 {
2572 CChat &Chat = GameClient()->m_Chat;
2573 CUIRect TopView, PreviewView;
2574 MainView.HSplitBottom(Cut: 220.0f, pTop: &TopView, pBottom: &PreviewView);
2575 TopView.HSplitBottom(Cut: MarginBetweenViews, pTop: &TopView, pBottom: nullptr);
2576 TopView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2577
2578 // ***** Chat ***** //
2579 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
2580 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Chat"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2581 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2582
2583 // General chat settings
2584 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2585 if(DoButton_CheckBox(pId: &g_Config.m_ClShowChat, pText: Localize(pStr: "Show chat"), Checked: g_Config.m_ClShowChat, pRect: &Button))
2586 {
2587 g_Config.m_ClShowChat = g_Config.m_ClShowChat ? 0 : 1;
2588 }
2589 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2590 if(g_Config.m_ClShowChat)
2591 {
2592 static int s_ShowChat = 0;
2593 if(DoButton_CheckBox(pId: &s_ShowChat, pText: Localize(pStr: "Always show chat"), Checked: g_Config.m_ClShowChat == 2, pRect: &Button))
2594 g_Config.m_ClShowChat = g_Config.m_ClShowChat != 2 ? 2 : 1;
2595 }
2596
2597 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);
2598 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);
2599 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);
2600
2601 if(DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClChatOld, pText: Localize(pStr: "Use old chat style"), pValue: &g_Config.m_ClChatOld, pRect: &LeftView, VMargin: LineSize))
2602 GameClient()->m_Chat.RebuildChat();
2603
2604 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
2605 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, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE))
2606 {
2607 Chat.EnsureCoherentWidth();
2608 Chat.RebuildChat();
2609 }
2610
2611 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
2612 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClChatWidth, pOption: &g_Config.m_ClChatWidth, pRect: &Button, pStr: Localize(pStr: "Chat width"), Min: 120, Max: 400, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE))
2613 {
2614 Chat.EnsureCoherentFontSize();
2615 Chat.RebuildChat();
2616 }
2617
2618 // ***** Messages ***** //
2619 RightView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &RightView);
2620 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Messages"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2621 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
2622
2623 // Message Colors and extra settings
2624 static CButtonContainer s_SystemMessageColor;
2625 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);
2626 static CButtonContainer s_HighlightedMessageColor;
2627 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));
2628 static CButtonContainer s_TeamMessageColor;
2629 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));
2630 static CButtonContainer s_FriendMessageColor;
2631 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);
2632 static CButtonContainer s_NormalMessageColor;
2633 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));
2634
2635 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s (echo)", Localize(pStr: "Client message"));
2636 static CButtonContainer s_ClientMessageColor;
2637 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));
2638
2639 // ***** Chat Preview ***** //
2640 PreviewView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &PreviewView);
2641 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Preview"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2642 PreviewView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &PreviewView);
2643
2644 // Use the rest of the view for preview
2645 PreviewView.Draw(Color: ColorRGBA(1, 1, 1, 0.1f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
2646 PreviewView.Margin(Cut: MarginSmall, pOtherRect: &PreviewView);
2647
2648 ColorRGBA SystemColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageSystemColor));
2649 ColorRGBA HighlightedColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageHighlightColor));
2650 ColorRGBA TeamColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageTeamColor));
2651 ColorRGBA FriendColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageFriendColor));
2652 ColorRGBA NormalColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageColor));
2653 ColorRGBA ClientColor = color_cast<ColorRGBA, ColorHSLA>(hsl: ColorHSLA(g_Config.m_ClMessageClientColor));
2654 ColorRGBA DefaultNameColor(0.8f, 0.8f, 0.8f, 1.0f);
2655
2656 const float RealFontSize = Chat.FontSize() * 2;
2657 const float RealMsgPaddingX = (!g_Config.m_ClChatOld ? Chat.MessagePaddingX() : 0) * 2;
2658 const float RealMsgPaddingY = (!g_Config.m_ClChatOld ? Chat.MessagePaddingY() : 0) * 2;
2659 const float RealMsgPaddingTee = (!g_Config.m_ClChatOld ? Chat.MessageTeeSize() + CChat::MESSAGE_TEE_PADDING_RIGHT : 0) * 2;
2660 const float RealOffsetY = RealFontSize + RealMsgPaddingY;
2661
2662 const float X = RealMsgPaddingX / 2.0f + PreviewView.x;
2663 float Y = PreviewView.y;
2664 float LineWidth = g_Config.m_ClChatWidth * 2 - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee;
2665
2666 str_copy(dst&: aBuf, src: Client()->PlayerName());
2667
2668 const CAnimState *pIdleState = CAnimState::GetIdle();
2669 const float RealTeeSize = Chat.MessageTeeSize() * 2;
2670 const float RealTeeSizeHalved = Chat.MessageTeeSize();
2671 constexpr float TWSkinUnreliableOffset = -0.25f;
2672 const float OffsetTeeY = RealTeeSizeHalved;
2673 const float FullHeightMinusTee = RealOffsetY - RealTeeSize;
2674
2675 struct SPreviewLine
2676 {
2677 int m_ClientId;
2678 bool m_Team;
2679 char m_aName[64];
2680 char m_aText[256];
2681 bool m_Friend;
2682 bool m_Player;
2683 bool m_Client;
2684 bool m_Highlighted;
2685 int m_TimesRepeated;
2686
2687 CTeeRenderInfo m_RenderInfo;
2688 };
2689
2690 static std::vector<SPreviewLine> s_vLines;
2691
2692 const auto *pDefaultSkin = GameClient()->m_Skins.Find(pName: "default");
2693 enum ELineFlag
2694 {
2695 FLAG_TEAM = 1 << 0,
2696 FLAG_FRIEND = 1 << 1,
2697 FLAG_HIGHLIGHT = 1 << 2,
2698 FLAG_CLIENT = 1 << 3
2699 };
2700 enum
2701 {
2702 PREVIEW_SYS,
2703 PREVIEW_HIGHLIGHT,
2704 PREVIEW_TEAM,
2705 PREVIEW_FRIEND,
2706 PREVIEW_SPAMMER,
2707 PREVIEW_CLIENT
2708 };
2709 auto &&SetPreviewLine = [](int Index, int ClientId, const char *pName, const char *pText, int Flag, int Repeats) {
2710 SPreviewLine *pLine;
2711 if((int)s_vLines.size() <= Index)
2712 {
2713 s_vLines.emplace_back();
2714 pLine = &s_vLines.back();
2715 }
2716 else
2717 {
2718 pLine = &s_vLines[Index];
2719 }
2720 pLine->m_ClientId = ClientId;
2721 pLine->m_Team = Flag & FLAG_TEAM;
2722 pLine->m_Friend = Flag & FLAG_FRIEND;
2723 pLine->m_Player = ClientId >= 0;
2724 pLine->m_Highlighted = Flag & FLAG_HIGHLIGHT;
2725 pLine->m_Client = Flag & FLAG_CLIENT;
2726 pLine->m_TimesRepeated = Repeats;
2727 str_copy(dst&: pLine->m_aName, src: pName);
2728 str_copy(dst&: pLine->m_aText, src: pText);
2729 };
2730 auto &&SetLineSkin = [RealTeeSize, &pDefaultSkin](int Index, const CSkin *pSkin) {
2731 if(Index >= (int)s_vLines.size())
2732 return;
2733 s_vLines[Index].m_RenderInfo.m_Size = RealTeeSize;
2734 s_vLines[Index].m_RenderInfo.m_CustomColoredSkin = false;
2735 if(pSkin != nullptr)
2736 s_vLines[Index].m_RenderInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin;
2737 else if(pDefaultSkin != nullptr)
2738 s_vLines[Index].m_RenderInfo.m_OriginalRenderSkin = pDefaultSkin->m_OriginalSkin;
2739 };
2740
2741 auto &&RenderPreview = [&](int LineIndex, int x, int y, bool Render = true) {
2742 if(LineIndex >= (int)s_vLines.size())
2743 return vec2(0, 0);
2744 CTextCursor LocalCursor;
2745 TextRender()->SetCursor(pCursor: &LocalCursor, x, y, FontSize: RealFontSize, Flags: Render ? TEXTFLAG_RENDER : 0);
2746 LocalCursor.m_LineWidth = LineWidth;
2747 const auto &Line = s_vLines[LineIndex];
2748
2749 char aName[64 + 12] = "";
2750
2751 if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2752 {
2753 if(Line.m_ClientId < 10)
2754 str_format(buffer: aName, buffer_size: sizeof(aName), format: " %d: ", Line.m_ClientId);
2755 else
2756 str_format(buffer: aName, buffer_size: sizeof(aName), format: "%d: ", Line.m_ClientId);
2757 }
2758
2759 str_append(dst&: aName, src: Line.m_aName);
2760
2761 char aCount[12];
2762 if(Line.m_ClientId < 0)
2763 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: "[%d] ", Line.m_TimesRepeated + 1);
2764 else
2765 str_format(buffer: aCount, buffer_size: sizeof(aCount), format: " [%d]", Line.m_TimesRepeated + 1);
2766
2767 if(Line.m_Player)
2768 {
2769 LocalCursor.m_X += RealMsgPaddingTee;
2770
2771 if(Line.m_Friend && g_Config.m_ClMessageFriend)
2772 {
2773 if(Render)
2774 TextRender()->TextColor(rgb: FriendColor);
2775 TextRender()->TextEx(pCursor: &LocalCursor, pText: "♥ ", Length: -1);
2776 }
2777 }
2778
2779 ColorRGBA NameColor;
2780 if(Line.m_Team)
2781 NameColor = CalculateNameColor(TextColorHSL: color_cast<ColorHSLA>(rgb: TeamColor));
2782 else if(Line.m_Player)
2783 NameColor = DefaultNameColor;
2784 else if(Line.m_Client)
2785 NameColor = ClientColor;
2786 else
2787 NameColor = SystemColor;
2788
2789 if(Render)
2790 TextRender()->TextColor(rgb: NameColor);
2791
2792 TextRender()->TextEx(pCursor: &LocalCursor, pText: aName, Length: -1);
2793
2794 if(Line.m_TimesRepeated > 0)
2795 {
2796 if(Render)
2797 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.3f);
2798 TextRender()->TextEx(pCursor: &LocalCursor, pText: aCount, Length: -1);
2799 }
2800
2801 if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0')
2802 {
2803 if(Render)
2804 TextRender()->TextColor(rgb: NameColor);
2805 TextRender()->TextEx(pCursor: &LocalCursor, pText: ": ", Length: -1);
2806 }
2807
2808 CTextCursor AppendCursor = LocalCursor;
2809 AppendCursor.m_LongestLineWidth = 0.0f;
2810 if(!g_Config.m_ClChatOld)
2811 {
2812 AppendCursor.m_StartX = LocalCursor.m_X;
2813 AppendCursor.m_LineWidth -= LocalCursor.m_LongestLineWidth;
2814 }
2815
2816 if(Render)
2817 {
2818 if(Line.m_Highlighted)
2819 TextRender()->TextColor(rgb: HighlightedColor);
2820 else if(Line.m_Team)
2821 TextRender()->TextColor(rgb: TeamColor);
2822 else if(Line.m_Player)
2823 TextRender()->TextColor(rgb: NormalColor);
2824 }
2825
2826 TextRender()->TextEx(pCursor: &AppendCursor, pText: Line.m_aText, Length: -1);
2827 if(Render)
2828 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
2829
2830 return vec2{LocalCursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth, AppendCursor.Height() + RealMsgPaddingY};
2831 };
2832
2833 // Set preview lines
2834 {
2835 char aLineBuilder[128];
2836
2837 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "'%s' entered and joined the game", aBuf);
2838 SetPreviewLine(PREVIEW_SYS, -1, "*** ", aLineBuilder, 0, 0);
2839
2840 str_format(buffer: aLineBuilder, buffer_size: sizeof(aLineBuilder), format: "Hey, how are you %s?", aBuf);
2841 SetPreviewLine(PREVIEW_HIGHLIGHT, 7, "Random Tee", aLineBuilder, FLAG_HIGHLIGHT, 0);
2842
2843 SetPreviewLine(PREVIEW_TEAM, 11, "Your Teammate", "Let's speedrun this!", FLAG_TEAM, 0);
2844 SetPreviewLine(PREVIEW_FRIEND, 8, "Friend", "Hello there", FLAG_FRIEND, 0);
2845 SetPreviewLine(PREVIEW_SPAMMER, 9, "Spammer", "Hey fools, I'm spamming here!", 0, 5);
2846 SetPreviewLine(PREVIEW_CLIENT, -1, "— ", "Echo command executed", FLAG_CLIENT, 0);
2847 }
2848
2849 SetLineSkin(1, GameClient()->m_Skins.FindOrNullptr(pName: "pinky"));
2850 SetLineSkin(2, pDefaultSkin);
2851 SetLineSkin(3, GameClient()->m_Skins.FindOrNullptr(pName: "cammostripes"));
2852 SetLineSkin(4, GameClient()->m_Skins.FindOrNullptr(pName: "beast"));
2853
2854 // Backgrounds first
2855 if(!g_Config.m_ClChatOld)
2856 {
2857 Graphics()->TextureClear();
2858 Graphics()->QuadsBegin();
2859 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 0.12f);
2860
2861 float TempY = Y;
2862 const float RealBackgroundRounding = Chat.MessageRounding() * 2.0f;
2863
2864 auto &&RenderMessageBackground = [&](int LineIndex) {
2865 auto Size = RenderPreview(LineIndex, 0, 0, false);
2866 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);
2867 return Size.y;
2868 };
2869
2870 if(g_Config.m_ClShowChatSystem)
2871 {
2872 TempY += RenderMessageBackground(PREVIEW_SYS);
2873 }
2874
2875 if(!g_Config.m_ClShowChatFriends)
2876 {
2877 if(!g_Config.m_ClShowChatTeamMembersOnly)
2878 TempY += RenderMessageBackground(PREVIEW_HIGHLIGHT);
2879 TempY += RenderMessageBackground(PREVIEW_TEAM);
2880 }
2881
2882 if(!g_Config.m_ClShowChatTeamMembersOnly)
2883 TempY += RenderMessageBackground(PREVIEW_FRIEND);
2884
2885 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2886 {
2887 TempY += RenderMessageBackground(PREVIEW_SPAMMER);
2888 }
2889
2890 TempY += RenderMessageBackground(PREVIEW_CLIENT);
2891
2892 Graphics()->QuadsEnd();
2893 }
2894
2895 // System
2896 if(g_Config.m_ClShowChatSystem)
2897 {
2898 Y += RenderPreview(PREVIEW_SYS, X, Y).y;
2899 }
2900
2901 if(!g_Config.m_ClShowChatFriends)
2902 {
2903 // Highlighted
2904 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2905 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));
2906 if(!g_Config.m_ClShowChatTeamMembersOnly)
2907 Y += RenderPreview(PREVIEW_HIGHLIGHT, X, Y).y;
2908
2909 // Team
2910 if(!g_Config.m_ClChatOld)
2911 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));
2912 Y += RenderPreview(PREVIEW_TEAM, X, Y).y;
2913 }
2914
2915 // Friend
2916 if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly)
2917 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));
2918 if(!g_Config.m_ClShowChatTeamMembersOnly)
2919 Y += RenderPreview(PREVIEW_FRIEND, X, Y).y;
2920
2921 // Normal
2922 if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly)
2923 {
2924 if(!g_Config.m_ClChatOld)
2925 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));
2926 Y += RenderPreview(PREVIEW_SPAMMER, X, Y).y;
2927 }
2928 // Client
2929 RenderPreview(PREVIEW_CLIENT, X, Y);
2930
2931 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
2932 }
2933 else if(s_CurTab == APPEARANCE_TAB_NAME_PLATE)
2934 {
2935 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2936
2937 // ***** Name Plate ***** //
2938 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
2939 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Name Plate"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2940 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2941
2942 // General name plate settings
2943 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNameplates, pText: Localize(pStr: "Show name plates"), pValue: &g_Config.m_ClNameplates, pRect: &LeftView, VMargin: LineSize);
2944 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
2945 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNameplatesSize, pOption: &g_Config.m_ClNameplatesSize, pRect: &Button, pStr: Localize(pStr: "Name plates size"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
2946
2947 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNameplatesClan, pText: Localize(pStr: "Show clan above name plates"), pValue: &g_Config.m_ClNameplatesClan, pRect: &LeftView, VMargin: LineSize);
2948 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
2949 if(g_Config.m_ClNameplatesClan)
2950 {
2951 Ui()->DoScrollbarOption(pId: &g_Config.m_ClNameplatesClanSize, pOption: &g_Config.m_ClNameplatesClanSize, pRect: &Button, pStr: Localize(pStr: "Clan plates size"), Min: 0, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
2952 }
2953
2954 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);
2955 DoButton_CheckBoxAutoVMarginAndSet(pId: &g_Config.m_ClNameplatesFriendMark, pText: Localize(pStr: "Show friend mark (♥) in name plates"), pValue: &g_Config.m_ClNameplatesFriendMark, pRect: &LeftView, VMargin: LineSize);
2956
2957 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2958 if(DoButton_CheckBox(pId: &g_Config.m_ClNameplatesStrong, pText: Localize(pStr: "Show hook strength icon indicator"), Checked: g_Config.m_ClNameplatesStrong, pRect: &Button))
2959 {
2960 g_Config.m_ClNameplatesStrong = g_Config.m_ClNameplatesStrong ? 0 : 1;
2961 }
2962 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2963 if(g_Config.m_ClNameplatesStrong)
2964 {
2965 static int s_NameplatesStrong = 0;
2966 if(DoButton_CheckBox(pId: &s_NameplatesStrong, pText: Localize(pStr: "Show hook strength number indicator"), Checked: g_Config.m_ClNameplatesStrong == 2, pRect: &Button))
2967 g_Config.m_ClNameplatesStrong = g_Config.m_ClNameplatesStrong != 2 ? 2 : 1;
2968 }
2969
2970 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2971 if(DoButton_CheckBox(pId: &g_Config.m_ClShowDirection, pText: Localize(pStr: "Show other players' key presses"), Checked: g_Config.m_ClShowDirection >= 1 && g_Config.m_ClShowDirection != 3, pRect: &Button))
2972 {
2973 g_Config.m_ClShowDirection = g_Config.m_ClShowDirection ^ 1;
2974 }
2975
2976 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2977 static int s_ShowLocalPlayer = 0;
2978 if(DoButton_CheckBox(pId: &s_ShowLocalPlayer, pText: Localize(pStr: "Show local player's key presses"), Checked: g_Config.m_ClShowDirection >= 2, pRect: &Button))
2979 {
2980 g_Config.m_ClShowDirection = g_Config.m_ClShowDirection ^ 3;
2981 }
2982
2983 ColorRGBA GreenDefault(0.78f, 1.0f, 0.8f, 1.0f);
2984 static CButtonContainer s_AuthedColor, s_SameClanColor;
2985 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);
2986 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);
2987 }
2988 else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION)
2989 {
2990 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
2991
2992 // ***** Hookline ***** //
2993 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
2994 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Hook collision line"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
2995 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
2996
2997 // General hookline settings
2998 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
2999 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))
3000 {
3001 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn ? 0 : 1;
3002 }
3003 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3004 if(g_Config.m_ClShowHookCollOwn)
3005 {
3006 static int s_ShowHookCollOwn = 0;
3007 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))
3008 g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn != 2 ? 2 : 1;
3009 }
3010
3011 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3012 if(DoButton_CheckBox(pId: &g_Config.m_ClShowHookCollOther, pText: Localize(pStr: "Show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther, pRect: &Button))
3013 {
3014 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther >= 1 ? 0 : 1;
3015 }
3016 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3017 if(g_Config.m_ClShowHookCollOther)
3018 {
3019 static int s_ShowHookCollOther = 0;
3020 if(DoButton_CheckBox(pId: &s_ShowHookCollOther, pText: Localize(pStr: "Always show other players' hook collision lines"), Checked: g_Config.m_ClShowHookCollOther == 2, pRect: &Button))
3021 g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther != 2 ? 2 : 1;
3022 }
3023
3024 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
3025 Ui()->DoScrollbarOption(pId: &g_Config.m_ClHookCollSize, pOption: &g_Config.m_ClHookCollSize, pRect: &Button, pStr: Localize(pStr: "Hook collision line width"), Min: 0, Max: 20, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_MULTILINE);
3026
3027 LeftView.HSplitTop(Cut: 2 * LineSize, pTop: &Button, pBottom: &LeftView);
3028 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: "%");
3029
3030 static CButtonContainer s_HookCollNoCollResetId, s_HookCollHookableCollResetId, s_HookCollTeeCollResetId;
3031 static int s_HookCollToolTip;
3032
3033 LeftView.HSplitTop(Cut: LineSize, pTop: &Label, pBottom: &LeftView);
3034 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
3035 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Colors of the hook collision line, in case of a possible collision with:"), Size: 13.0f, Align: TEXTALIGN_ML);
3036 Ui()->DoButtonLogic(pId: &s_HookCollToolTip, Checked: 0, pRect: &Label); // Just for the tooltip, result ignored
3037 GameClient()->m_Tooltips.DoToolTip(pId: &s_HookCollToolTip, pNearRect: &Label, pText: Localize(pStr: "Your movements are not taken into account when calculating the line colors"));
3038 DoLine_ColorPicker(pResetId: &s_HookCollNoCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Nothing hookable"), pColorValue: &g_Config.m_ClHookCollColorNoColl, DefaultColor: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), CheckBoxSpacing: false);
3039 DoLine_ColorPicker(pResetId: &s_HookCollHookableCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "Something hookable"), pColorValue: &g_Config.m_ClHookCollColorHookableColl, DefaultColor: ColorRGBA(130.0f / 255.0f, 232.0f / 255.0f, 160.0f / 255.0f, 1.0f), CheckBoxSpacing: false);
3040 DoLine_ColorPicker(pResetId: &s_HookCollTeeCollResetId, LineSize: ColorPickerLineSize, LabelSize: ColorPickerLabelSize, BottomMargin: ColorPickerLineSpacing, pMainRect: &LeftView, pText: Localize(pStr: "A Tee"), pColorValue: &g_Config.m_ClHookCollColorTeeColl, DefaultColor: ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f), CheckBoxSpacing: false);
3041 }
3042 else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES)
3043 {
3044 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
3045
3046 // ***** Info Messages ***** //
3047 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
3048 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Info Messages"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
3049 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
3050
3051 // General info messages settings
3052 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3053 if(DoButton_CheckBox(pId: &g_Config.m_ClShowKillMessages, pText: Localize(pStr: "Show kill messages"), Checked: g_Config.m_ClShowKillMessages, pRect: &Button))
3054 {
3055 g_Config.m_ClShowKillMessages ^= 1;
3056 }
3057
3058 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3059 if(DoButton_CheckBox(pId: &g_Config.m_ClShowFinishMessages, pText: Localize(pStr: "Show finish messages"), Checked: g_Config.m_ClShowFinishMessages, pRect: &Button))
3060 {
3061 g_Config.m_ClShowFinishMessages ^= 1;
3062 }
3063
3064 static CButtonContainer s_KillMessageNormalColorId, s_KillMessageHighlightColorId;
3065 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);
3066 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);
3067 }
3068 else if(s_CurTab == APPEARANCE_TAB_LASER)
3069 {
3070 MainView.VSplitMid(pLeft: &LeftView, pRight: &RightView, Spacing: MarginBetweenViews);
3071
3072 // ***** Weapons ***** //
3073 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
3074 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Weapons"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
3075 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
3076
3077 // General weapon laser settings
3078 static CButtonContainer s_LaserRifleOutResetId, s_LaserRifleInResetId, s_LaserShotgunOutResetId, s_LaserShotgunInResetId;
3079
3080 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);
3081 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);
3082 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);
3083 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);
3084
3085 // ***** Entities ***** //
3086 LeftView.HSplitTop(Cut: 10.0f, pTop: nullptr, pBottom: &LeftView);
3087 LeftView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &LeftView);
3088 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Entities"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
3089 LeftView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &LeftView);
3090
3091 // General entity laser settings
3092 static CButtonContainer s_LaserDoorOutResetId, s_LaserDoorInResetId, s_LaserFreezeOutResetId, s_LaserFreezeInResetId;
3093
3094 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);
3095 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);
3096 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);
3097 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);
3098
3099 static CButtonContainer s_AllToRifleResetId, s_AllToDefaultResetId;
3100
3101 LeftView.HSplitTop(Cut: 4 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
3102 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3103 if(DoButton_Menu(pButtonContainer: &s_AllToRifleResetId, pText: Localize(pStr: "Set all to Rifle"), Checked: 0, pRect: &Button))
3104 {
3105 g_Config.m_ClLaserShotgunOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
3106 g_Config.m_ClLaserShotgunInnerColor = g_Config.m_ClLaserRifleInnerColor;
3107 g_Config.m_ClLaserDoorOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
3108 g_Config.m_ClLaserDoorInnerColor = g_Config.m_ClLaserRifleInnerColor;
3109 g_Config.m_ClLaserFreezeOutlineColor = g_Config.m_ClLaserRifleOutlineColor;
3110 g_Config.m_ClLaserFreezeInnerColor = g_Config.m_ClLaserRifleInnerColor;
3111 }
3112
3113 // values taken from the CL commands
3114 LeftView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &LeftView);
3115 LeftView.HSplitTop(Cut: LineSize, pTop: &Button, pBottom: &LeftView);
3116 if(DoButton_Menu(pButtonContainer: &s_AllToDefaultResetId, pText: Localize(pStr: "Reset to defaults"), Checked: 0, pRect: &Button))
3117 {
3118 g_Config.m_ClLaserRifleOutlineColor = 11176233;
3119 g_Config.m_ClLaserRifleInnerColor = 11206591;
3120 g_Config.m_ClLaserShotgunOutlineColor = 1866773;
3121 g_Config.m_ClLaserShotgunInnerColor = 1467241;
3122 g_Config.m_ClLaserDoorOutlineColor = 7667473;
3123 g_Config.m_ClLaserDoorInnerColor = 7701379;
3124 g_Config.m_ClLaserFreezeOutlineColor = 11613223;
3125 g_Config.m_ClLaserFreezeInnerColor = 12001153;
3126 }
3127
3128 // ***** Laser Preview ***** //
3129 RightView.HSplitTop(Cut: HeadlineHeight, pTop: &Label, pBottom: &RightView);
3130 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Preview"), Size: HeadlineFontSize, Align: TEXTALIGN_ML);
3131 RightView.HSplitTop(Cut: MarginSmall, pTop: nullptr, pBottom: &RightView);
3132
3133 const float LaserPreviewHeight = 50.0f;
3134 CUIRect LaserPreview;
3135 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
3136 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
3137 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserRifleOutlineColor, InnerColor: LaserRifleInnerColor, LaserType: LASERTYPE_RIFLE);
3138
3139 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
3140 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
3141 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserShotgunOutlineColor, InnerColor: LaserShotgunInnerColor, LaserType: LASERTYPE_SHOTGUN);
3142
3143 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
3144 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
3145 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserDoorOutlineColor, InnerColor: LaserDoorInnerColor, LaserType: LASERTYPE_DOOR);
3146
3147 RightView.HSplitTop(Cut: LaserPreviewHeight, pTop: &LaserPreview, pBottom: &RightView);
3148 RightView.HSplitTop(Cut: 2 * MarginSmall, pTop: nullptr, pBottom: &RightView);
3149 DoLaserPreview(pRect: &LaserPreview, OutlineColor: LaserFreezeOutlineColor, InnerColor: LaserFreezeInnerColor, LaserType: LASERTYPE_DOOR);
3150 }
3151}
3152
3153void CMenus::RenderSettingsDDNet(CUIRect MainView)
3154{
3155 CUIRect Button, Left, Right, LeftLeft, Label;
3156
3157#if defined(CONF_AUTOUPDATE)
3158 CUIRect UpdaterRect;
3159 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &UpdaterRect);
3160 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
3161#endif
3162
3163 // demo
3164 CUIRect Demo;
3165 MainView.HSplitTop(Cut: 110.0f, pTop: &Demo, pBottom: &MainView);
3166 Demo.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Demo);
3167 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Demo"), Size: 20.0f, Align: TEXTALIGN_ML);
3168 Label.VSplitMid(pLeft: nullptr, pRight: &Label, Spacing: 20.0f);
3169 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Ghost"), Size: 20.0f, Align: TEXTALIGN_ML);
3170
3171 Demo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Demo);
3172 Demo.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
3173
3174 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3175 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))
3176 {
3177 g_Config.m_ClAutoRaceRecord ^= 1;
3178 }
3179
3180 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3181 if(DoButton_CheckBox(pId: &g_Config.m_ClReplays, pText: Localize(pStr: "Enable replays"), Checked: g_Config.m_ClReplays, pRect: &Button))
3182 {
3183 g_Config.m_ClReplays ^= 1;
3184 Client()->DemoRecorder_UpdateReplayRecorder();
3185 }
3186
3187 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3188 if(g_Config.m_ClReplays)
3189 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);
3190
3191 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3192 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhost, pText: Localize(pStr: "Enable ghost"), Checked: g_Config.m_ClRaceGhost, pRect: &Button))
3193 {
3194 g_Config.m_ClRaceGhost ^= 1;
3195 }
3196 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"));
3197
3198 if(g_Config.m_ClRaceGhost)
3199 {
3200 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3201 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
3202 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceShowGhost, pText: Localize(pStr: "Show ghost"), Checked: g_Config.m_ClRaceShowGhost, pRect: &LeftLeft))
3203 {
3204 g_Config.m_ClRaceShowGhost ^= 1;
3205 }
3206 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: "%");
3207
3208 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3209 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceSaveGhost, pText: Localize(pStr: "Save ghost"), Checked: g_Config.m_ClRaceSaveGhost, pRect: &Button))
3210 {
3211 g_Config.m_ClRaceSaveGhost ^= 1;
3212 }
3213
3214 if(g_Config.m_ClRaceSaveGhost)
3215 {
3216 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3217 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhostSaveBest, pText: Localize(pStr: "Only save improvements"), Checked: g_Config.m_ClRaceGhostSaveBest, pRect: &Button))
3218 {
3219 g_Config.m_ClRaceGhostSaveBest ^= 1;
3220 }
3221 }
3222 }
3223
3224 // gameplay
3225 CUIRect Gameplay;
3226 MainView.HSplitTop(Cut: 150.0f, pTop: &Gameplay, pBottom: &MainView);
3227 Gameplay.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Gameplay);
3228 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Gameplay"), Size: 20.0f, Align: TEXTALIGN_ML);
3229 Gameplay.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Gameplay);
3230 Gameplay.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
3231
3232 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3233 Ui()->DoScrollbarOption(pId: &g_Config.m_ClOverlayEntities, pOption: &g_Config.m_ClOverlayEntities, pRect: &Button, pStr: Localize(pStr: "Overlay entities"), Min: 0, Max: 100);
3234
3235 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3236 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
3237
3238 if(DoButton_CheckBox(pId: &g_Config.m_ClTextEntities, pText: Localize(pStr: "Show text entities"), Checked: g_Config.m_ClTextEntities, pRect: &LeftLeft))
3239 g_Config.m_ClTextEntities ^= 1;
3240
3241 if(g_Config.m_ClTextEntities)
3242 {
3243 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClTextEntitiesSize, pOption: &g_Config.m_ClTextEntitiesSize, pRect: &Button, pStr: Localize(pStr: "Size"), Min: 0, Max: 100))
3244 m_pClient->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
3245 }
3246
3247 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3248 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
3249
3250 if(DoButton_CheckBox(pId: &g_Config.m_ClShowOthers, pText: Localize(pStr: "Show others"), Checked: g_Config.m_ClShowOthers == SHOW_OTHERS_ON, pRect: &LeftLeft))
3251 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF;
3252
3253 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: "%");
3254
3255 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 nameplates"));
3256
3257 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3258 static int s_ShowOwnTeamId = 0;
3259 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))
3260 {
3261 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ONLY_TEAM ? SHOW_OTHERS_ONLY_TEAM : SHOW_OTHERS_OFF;
3262 }
3263
3264 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
3265 if(DoButton_CheckBox(pId: &g_Config.m_ClShowQuads, pText: Localize(pStr: "Show quads"), Checked: g_Config.m_ClShowQuads, pRect: &Button))
3266 {
3267 g_Config.m_ClShowQuads ^= 1;
3268 }
3269 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowQuads, pNearRect: &Button, pText: Localize(pStr: "Quads are used for background decoration"));
3270
3271 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3272 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClDefaultZoom, pOption: &g_Config.m_ClDefaultZoom, pRect: &Button, pStr: Localize(pStr: "Default zoom"), Min: 0, Max: 20))
3273 m_pClient->m_Camera.SetZoom(Target: std::pow(x: CCamera::ZOOM_STEP, y: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime);
3274
3275 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3276 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPing, pText: Localize(pStr: "AntiPing"), Checked: g_Config.m_ClAntiPing, pRect: &Button))
3277 {
3278 g_Config.m_ClAntiPing ^= 1;
3279 }
3280 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"));
3281
3282 if(g_Config.m_ClAntiPing)
3283 {
3284 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3285 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingPlayers, pText: Localize(pStr: "AntiPing: predict other players"), Checked: g_Config.m_ClAntiPingPlayers, pRect: &Button))
3286 {
3287 g_Config.m_ClAntiPingPlayers ^= 1;
3288 }
3289
3290 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3291 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingWeapons, pText: Localize(pStr: "AntiPing: predict weapons"), Checked: g_Config.m_ClAntiPingWeapons, pRect: &Button))
3292 {
3293 g_Config.m_ClAntiPingWeapons ^= 1;
3294 }
3295
3296 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
3297 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingGrenade, pText: Localize(pStr: "AntiPing: predict grenade paths"), Checked: g_Config.m_ClAntiPingGrenade, pRect: &Button))
3298 {
3299 g_Config.m_ClAntiPingGrenade ^= 1;
3300 }
3301 }
3302
3303 CUIRect Background, Miscellaneous;
3304 MainView.VSplitMid(pLeft: &Background, pRight: &Miscellaneous, Spacing: 20.0f);
3305
3306 // background
3307 Background.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Background);
3308 Background.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Background);
3309 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Background"), Size: 20.0f, Align: TEXTALIGN_ML);
3310
3311 ColorRGBA GreyDefault(0.5f, 0.5f, 0.5f, 1);
3312
3313 static CButtonContainer s_ResetId1;
3314 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);
3315
3316 static CButtonContainer s_ResetId2;
3317 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);
3318
3319 CUIRect EditBox;
3320 Background.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Background);
3321 Background.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Background);
3322 Label.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &EditBox);
3323 EditBox.VSplitRight(Cut: 100.0f, pLeft: &EditBox, pRight: &Button);
3324 EditBox.VSplitRight(Cut: 5.0f, pLeft: &EditBox, pRight: nullptr);
3325
3326 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Map"), Size: 14.0f, Align: TEXTALIGN_ML);
3327
3328 static CLineInput s_BackgroundEntitiesInput(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities));
3329 Ui()->DoEditBox(pLineInput: &s_BackgroundEntitiesInput, pRect: &EditBox, FontSize: 14.0f);
3330
3331 static CButtonContainer s_BackgroundEntitiesReloadButton;
3332 if(DoButton_Menu(pButtonContainer: &s_BackgroundEntitiesReloadButton, pText: Localize(pStr: "Reload"), Checked: 0, pRect: &Button))
3333 {
3334 UpdateBackgroundEntities();
3335 }
3336
3337 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
3338 const bool UseCurrentMap = str_comp(a: g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0;
3339 static int s_UseCurrentMapId = 0;
3340 if(DoButton_CheckBox(pId: &s_UseCurrentMapId, pText: Localize(pStr: "Use current map as background"), Checked: UseCurrentMap, pRect: &Button))
3341 {
3342 if(UseCurrentMap)
3343 g_Config.m_ClBackgroundEntities[0] = '\0';
3344 else
3345 str_copy(dst&: g_Config.m_ClBackgroundEntities, CURRENT_MAP);
3346 m_pClient->m_Background.LoadBackground();
3347 }
3348
3349 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
3350 if(DoButton_CheckBox(pId: &g_Config.m_ClBackgroundShowTilesLayers, pText: Localize(pStr: "Show tiles layers from BG map"), Checked: g_Config.m_ClBackgroundShowTilesLayers, pRect: &Button))
3351 g_Config.m_ClBackgroundShowTilesLayers ^= 1;
3352
3353 // miscellaneous
3354 Miscellaneous.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Miscellaneous);
3355 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
3356
3357 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Miscellaneous"), Size: 20.0f, Align: TEXTALIGN_ML);
3358
3359 static CButtonContainer s_ButtonTimeout;
3360 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
3361 if(DoButton_Menu(pButtonContainer: &s_ButtonTimeout, pText: Localize(pStr: "New random timeout code"), Checked: 0, pRect: &Button))
3362 {
3363 Client()->GenerateTimeoutSeed();
3364 }
3365
3366 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
3367 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Miscellaneous);
3368 Miscellaneous.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Miscellaneous);
3369 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Run on join"), Size: 14.0f, Align: TEXTALIGN_ML);
3370 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
3371 static CLineInput s_RunOnJoinInput(g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin));
3372 s_RunOnJoinInput.SetEmptyText(Localize(pStr: "Chat command (e.g. showall 1)"));
3373 Ui()->DoEditBox(pLineInput: &s_RunOnJoinInput, pRect: &Button, FontSize: 14.0f);
3374
3375#if defined(CONF_FAMILY_WINDOWS)
3376 static CButtonContainer s_ButtonUnregisterShell;
3377 Miscellaneous.HSplitTop(10.0f, nullptr, &Miscellaneous);
3378 Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous);
3379 if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button))
3380 {
3381 Client()->ShellUnregister();
3382 }
3383#endif
3384
3385 // Updater
3386#if defined(CONF_AUTOUPDATE)
3387 {
3388 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
3389 IUpdater::EUpdaterState State = Updater()->GetCurrentState();
3390
3391 // Update Button
3392 char aBuf[256];
3393 if(NeedUpdate && State <= IUpdater::CLEAN)
3394 {
3395 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is available:"), Client()->LatestVersion());
3396 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
3397 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
3398 static CButtonContainer s_ButtonUpdate;
3399 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &Button))
3400 {
3401 Updater()->InitiateUpdate();
3402 }
3403 }
3404 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
3405 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Updating…"));
3406 else if(State == IUpdater::NEED_RESTART)
3407 {
3408 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet Client updated!"));
3409 m_NeedRestartUpdate = true;
3410 }
3411 else
3412 {
3413 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "No updates available"));
3414 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
3415 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
3416 static CButtonContainer s_ButtonUpdate;
3417 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Check now"), Checked: 0, pRect: &Button))
3418 {
3419 Client()->RequestDDNetInfo();
3420 }
3421 }
3422 Ui()->DoLabel(pRect: &UpdaterRect, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
3423 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
3424 }
3425#endif
3426}
3427