1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "menus.h"
4
5#include <base/dbg.h>
6#include <base/fs.h>
7#include <base/str.h>
8
9#include <engine/font_icons.h>
10#include <engine/graphics.h>
11#include <engine/shared/config.h>
12#include <engine/storage.h>
13#include <engine/textrender.h>
14#include <engine/updater.h>
15
16#include <game/client/components/background.h>
17#include <game/client/components/mapimages.h>
18#include <game/client/components/tooltips.h>
19#include <game/client/gameclient.h>
20#include <game/client/ui.h>
21#include <game/client/ui_listbox.h>
22#include <game/localization.h>
23
24#include <algorithm>
25
26void CMenus::RenderSettingsDDNet(CUIRect MainView)
27{
28 CUIRect Button, Left, Right, LeftLeft, Label;
29
30#if defined(CONF_AUTOUPDATE)
31 CUIRect UpdaterRect;
32 MainView.HSplitBottom(Cut: 20.0f, pTop: &MainView, pBottom: &UpdaterRect);
33 MainView.HSplitBottom(Cut: 5.0f, pTop: &MainView, pBottom: nullptr);
34#endif
35
36 // demo
37 CUIRect Demo;
38 MainView.HSplitTop(Cut: 110.0f, pTop: &Demo, pBottom: &MainView);
39 Demo.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Demo);
40 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Demo"), Size: 20.0f, Align: TEXTALIGN_ML);
41 Label.VSplitMid(pLeft: nullptr, pRight: &Label, Spacing: 20.0f);
42 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Ghost"), Size: 20.0f, Align: TEXTALIGN_ML);
43
44 Demo.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Demo);
45 Demo.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
46
47 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
48 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))
49 {
50 g_Config.m_ClAutoRaceRecord ^= 1;
51 }
52
53 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
54 if(DoButton_CheckBox(pId: &g_Config.m_ClReplays, pText: Localize(pStr: "Enable replays"), Checked: g_Config.m_ClReplays, pRect: &Button))
55 {
56 g_Config.m_ClReplays ^= 1;
57 if(Client()->State() == IClient::STATE_ONLINE)
58 {
59 Client()->DemoRecorder_UpdateReplayRecorder();
60 }
61 }
62
63 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
64 if(g_Config.m_ClReplays)
65 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);
66
67 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
68 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhost, pText: Localize(pStr: "Enable ghost"), Checked: g_Config.m_ClRaceGhost, pRect: &Button))
69 {
70 g_Config.m_ClRaceGhost ^= 1;
71 }
72 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"));
73
74 if(g_Config.m_ClRaceGhost)
75 {
76 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
77 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
78 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceShowGhost, pText: Localize(pStr: "Show ghost"), Checked: g_Config.m_ClRaceShowGhost, pRect: &LeftLeft))
79 {
80 g_Config.m_ClRaceShowGhost ^= 1;
81 }
82 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: "%");
83
84 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
85 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceSaveGhost, pText: Localize(pStr: "Save ghost"), Checked: g_Config.m_ClRaceSaveGhost, pRect: &Button))
86 {
87 g_Config.m_ClRaceSaveGhost ^= 1;
88 }
89
90 if(g_Config.m_ClRaceSaveGhost)
91 {
92 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
93 if(DoButton_CheckBox(pId: &g_Config.m_ClRaceGhostSaveBest, pText: Localize(pStr: "Only save improvements"), Checked: g_Config.m_ClRaceGhostSaveBest, pRect: &Button))
94 {
95 g_Config.m_ClRaceGhostSaveBest ^= 1;
96 }
97 }
98 }
99
100 // gameplay
101 CUIRect Gameplay;
102 MainView.HSplitTop(Cut: 190.0f, pTop: &Gameplay, pBottom: &MainView);
103 Gameplay.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Gameplay);
104 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Gameplay"), Size: 20.0f, Align: TEXTALIGN_ML);
105 Gameplay.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Gameplay);
106 Gameplay.VSplitMid(pLeft: &Left, pRight: &Right, Spacing: 20.0f);
107
108 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
109 Ui()->DoScrollbarOption(pId: &g_Config.m_ClOverlayEntities, pOption: &g_Config.m_ClOverlayEntities, pRect: &Button, pStr: Localize(pStr: "Overlay entities"), Min: 0, Max: 100);
110
111 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
112 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
113
114 if(DoButton_CheckBox(pId: &g_Config.m_ClTextEntities, pText: Localize(pStr: "Show text entities"), Checked: g_Config.m_ClTextEntities, pRect: &LeftLeft))
115 g_Config.m_ClTextEntities ^= 1;
116
117 if(g_Config.m_ClTextEntities)
118 {
119 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClTextEntitiesSize, pOption: &g_Config.m_ClTextEntitiesSize, pRect: &Button, pStr: Localize(pStr: "Size"), Min: 20, Max: 100, pScale: &CUi::ms_LinearScrollbarScale, Flags: CUi::SCROLLBAR_OPTION_DELAYUPDATE))
120 GameClient()->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize);
121 }
122
123 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
124 Button.VSplitMid(pLeft: &LeftLeft, pRight: &Button);
125
126 if(DoButton_CheckBox(pId: &g_Config.m_ClShowOthers, pText: Localize(pStr: "Show others"), Checked: g_Config.m_ClShowOthers == SHOW_OTHERS_ON, pRect: &LeftLeft))
127 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF;
128
129 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: "%");
130
131 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowOthersAlpha, pNearRect: &Button, pText: Localize(pStr: "Adjust the opacity of entities belonging to other teams, such as tees and name plates"));
132
133 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
134 static int s_ShowOwnTeamId = 0;
135 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))
136 {
137 g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ONLY_TEAM ? SHOW_OTHERS_ONLY_TEAM : SHOW_OTHERS_OFF;
138 }
139
140 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
141 if(DoButton_CheckBox(pId: &g_Config.m_ClShowQuads, pText: Localize(pStr: "Show background quads"), Checked: g_Config.m_ClShowQuads, pRect: &Button))
142 {
143 g_Config.m_ClShowQuads ^= 1;
144 }
145 GameClient()->m_Tooltips.DoToolTip(pId: &g_Config.m_ClShowQuads, pNearRect: &Button, pText: Localize(pStr: "Quads are used for background decoration"));
146
147 Left.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Left);
148 if(Ui()->DoScrollbarOption(pId: &g_Config.m_ClDefaultZoom, pOption: &g_Config.m_ClDefaultZoom, pRect: &Button, pStr: Localize(pStr: "Default zoom"), Min: 0, Max: 20))
149 GameClient()->m_Camera.SetZoom(Target: CCamera::ZoomStepsToValue(Steps: g_Config.m_ClDefaultZoom - 10), Smoothness: g_Config.m_ClSmoothZoomTime, IsUser: true);
150
151 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
152 Ui()->DoScrollbarOption(pId: &g_Config.m_ClPredictionMargin, pOption: &g_Config.m_ClPredictionMargin, pRect: &Button, pStr: Localize(pStr: "Prediction margin"), Min: 1, Max: 300);
153
154 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
155 if(DoButton_CheckBox(pId: &g_Config.m_ClPredictEvents, pText: Localize(pStr: "Predict events (experimental)"), Checked: g_Config.m_ClPredictEvents, pRect: &Button))
156 {
157 g_Config.m_ClPredictEvents ^= 1;
158 }
159
160 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
161 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPing, pText: Localize(pStr: "AntiPing"), Checked: g_Config.m_ClAntiPing, pRect: &Button))
162 {
163 g_Config.m_ClAntiPing ^= 1;
164 }
165 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"));
166
167 if(g_Config.m_ClAntiPing)
168 {
169 // AntiPing: players
170 {
171 // Base state
172 {
173 int Pressed = std::min(a: g_Config.m_ClAntiPingPlayers, b: 2);
174 const int Prev = Pressed;
175 DoLine_RadioMenu(View&: Right, pLabel: Localize(pStr: "AntiPing: predict other players"),
176 vButtonContainers&: m_vButtonContainersAntiPingPlayers,
177 vLabels: {Localize(pStr: "Off", pContext: "AntiPing: predict other players"), Localize(pStr: "All", pContext: "AntiPing: predict other players"), Localize(pStr: "Interact", pContext: "AntiPing: predict other players")},
178 vValues: {0, 1, 2},
179 Value&: Pressed);
180 if(Pressed != Prev)
181 g_Config.m_ClAntiPingPlayers = Pressed;
182 }
183
184 // Interaction state
185 if(g_Config.m_ClAntiPingPlayers >= 2)
186 {
187 bool PrevChecked = g_Config.m_ClAntiPingPlayers == 3;
188 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
189 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingPlayers, pText: Localize(pStr: "Always predict frozen players"), Checked: PrevChecked, pRect: &Button))
190 {
191 g_Config.m_ClAntiPingPlayers = PrevChecked ? 2 : 3;
192 }
193 }
194 }
195
196 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
197 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingWeapons, pText: Localize(pStr: "AntiPing: predict weapons"), Checked: g_Config.m_ClAntiPingWeapons, pRect: &Button))
198 {
199 g_Config.m_ClAntiPingWeapons ^= 1;
200 }
201
202 Right.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Right);
203 if(DoButton_CheckBox(pId: &g_Config.m_ClAntiPingGrenade, pText: Localize(pStr: "AntiPing: predict grenade paths"), Checked: g_Config.m_ClAntiPingGrenade, pRect: &Button))
204 {
205 g_Config.m_ClAntiPingGrenade ^= 1;
206 }
207 }
208
209 CUIRect Background, Miscellaneous;
210 MainView.VSplitMid(pLeft: &Background, pRight: &Miscellaneous, Spacing: 20.0f);
211
212 // background
213 Background.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Background);
214 Background.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Background);
215 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Background"), Size: 20.0f, Align: TEXTALIGN_ML);
216
217 ColorRGBA GreyDefault(0.5f, 0.5f, 0.5f, 1);
218
219 static CButtonContainer s_ResetId1;
220 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);
221
222 static CButtonContainer s_ResetId2;
223 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);
224
225 CUIRect EditBox, ReloadButton;
226 Background.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Background);
227 Background.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Background);
228 Label.VSplitLeft(Cut: 100.0f, pLeft: &Label, pRight: &EditBox);
229 EditBox.VSplitRight(Cut: 60.0f, pLeft: &EditBox, pRight: &Button);
230 Button.VSplitMid(pLeft: &ReloadButton, pRight: &Button, Spacing: 5.0f);
231 EditBox.VSplitRight(Cut: 5.0f, pLeft: &EditBox, pRight: nullptr);
232
233 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Map"), Size: 14.0f, Align: TEXTALIGN_ML);
234
235 static CLineInput s_BackgroundEntitiesInput(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities));
236 Ui()->DoEditBox(pLineInput: &s_BackgroundEntitiesInput, pRect: &EditBox, FontSize: 14.0f);
237
238 static CButtonContainer s_BackgroundEntitiesMapPicker, s_BackgroundEntitiesReload;
239
240 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesReload, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &ReloadButton, Flags: BUTTONFLAG_LEFT))
241 {
242 GameClient()->m_Background.LoadBackground();
243 }
244
245 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_BackgroundEntitiesMapPicker, pText: FontIcon::FOLDER, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
246 {
247 static SPopupMenuId s_PopupMapPickerId;
248 static CPopupMapPickerContext s_PopupMapPickerContext;
249 s_PopupMapPickerContext.m_pMenus = this;
250 s_PopupMapPickerContext.MapListPopulate();
251 Ui()->DoPopupMenu(pId: &s_PopupMapPickerId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 300.0f, Height: 250.0f, pContext: &s_PopupMapPickerContext, pfnFunc: PopupMapPicker);
252 }
253
254 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
255 const bool UseCurrentMap = str_comp(a: g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0;
256 static int s_UseCurrentMapId = 0;
257 if(DoButton_CheckBox(pId: &s_UseCurrentMapId, pText: Localize(pStr: "Use current map as background"), Checked: UseCurrentMap, pRect: &Button))
258 {
259 if(UseCurrentMap)
260 g_Config.m_ClBackgroundEntities[0] = '\0';
261 else
262 str_copy(dst&: g_Config.m_ClBackgroundEntities, CURRENT_MAP);
263 GameClient()->m_Background.LoadBackground();
264 }
265
266 Background.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Background);
267 if(DoButton_CheckBox(pId: &g_Config.m_ClBackgroundShowTilesLayers, pText: Localize(pStr: "Show tiles layers from BG map"), Checked: g_Config.m_ClBackgroundShowTilesLayers, pRect: &Button))
268 g_Config.m_ClBackgroundShowTilesLayers ^= 1;
269
270 // miscellaneous
271 Miscellaneous.HSplitTop(Cut: 30.0f, pTop: &Label, pBottom: &Miscellaneous);
272 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
273
274 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Miscellaneous"), Size: 20.0f, Align: TEXTALIGN_ML);
275
276 static CButtonContainer s_ButtonTimeout;
277 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
278 if(DoButton_Menu(pButtonContainer: &s_ButtonTimeout, pText: Localize(pStr: "New random timeout code"), Checked: 0, pRect: &Button))
279 {
280 Client()->GenerateTimeoutSeed();
281 }
282
283 Miscellaneous.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &Miscellaneous);
284 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Label, pBottom: &Miscellaneous);
285 Miscellaneous.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &Miscellaneous);
286 Ui()->DoLabel(pRect: &Label, pText: Localize(pStr: "Run on join"), Size: 14.0f, Align: TEXTALIGN_ML);
287 Miscellaneous.HSplitTop(Cut: 20.0f, pTop: &Button, pBottom: &Miscellaneous);
288 static CLineInput s_RunOnJoinInput(g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin));
289 s_RunOnJoinInput.SetEmptyText(Localize(pStr: "Chat command (e.g. showall 1)"));
290 Ui()->DoEditBox(pLineInput: &s_RunOnJoinInput, pRect: &Button, FontSize: 14.0f);
291
292#if defined(CONF_FAMILY_WINDOWS)
293 static CButtonContainer s_ButtonUnregisterShell;
294 Miscellaneous.HSplitTop(10.0f, nullptr, &Miscellaneous);
295 Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous);
296 if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button))
297 {
298 Client()->ShellUnregister();
299 }
300#endif
301
302 // Updater
303#if defined(CONF_AUTOUPDATE)
304 {
305 bool NeedUpdate = str_comp(a: Client()->LatestVersion(), b: "0");
306 IUpdater::EUpdaterState State = Updater()->GetCurrentState();
307
308 // Update Button
309 char aBuf[256];
310 if(NeedUpdate && State <= IUpdater::CLEAN)
311 {
312 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "DDNet %s is available:"), Client()->LatestVersion());
313 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
314 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
315 static CButtonContainer s_ButtonUpdate;
316 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Update now"), Checked: 0, pRect: &Button))
317 {
318 Updater()->InitiateUpdate();
319 }
320 }
321 else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART)
322 str_copy(dst&: aBuf, src: Localize(pStr: "Updating…"));
323 else if(State == IUpdater::NEED_RESTART)
324 {
325 str_copy(dst&: aBuf, src: Localize(pStr: "DDNet Client updated!"));
326 m_NeedRestartUpdate = true;
327 }
328 else
329 {
330 str_copy(dst&: aBuf, src: Localize(pStr: "No updates available"));
331 UpdaterRect.VSplitLeft(Cut: TextRender()->TextWidth(Size: 14.0f, pText: aBuf, StrLength: -1, LineWidth: -1.0f) + 10.0f, pLeft: &UpdaterRect, pRight: &Button);
332 Button.VSplitLeft(Cut: 100.0f, pLeft: &Button, pRight: nullptr);
333 static CButtonContainer s_ButtonUpdate;
334 if(DoButton_Menu(pButtonContainer: &s_ButtonUpdate, pText: Localize(pStr: "Check now"), Checked: 0, pRect: &Button))
335 {
336 Client()->RequestDDNetInfo();
337 }
338 }
339 Ui()->DoLabel(pRect: &UpdaterRect, pText: aBuf, Size: 14.0f, Align: TEXTALIGN_ML);
340 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
341 }
342#endif
343}
344
345CUi::EPopupMenuFunctionResult CMenus::PopupMapPicker(void *pContext, CUIRect View, bool Active)
346{
347 CPopupMapPickerContext *pPopupContext = static_cast<CPopupMapPickerContext *>(pContext);
348 CMenus *pMenus = pPopupContext->m_pMenus;
349
350 static CListBox s_ListBox;
351 s_ListBox.SetActive(Active);
352 s_ListBox.DoStart(RowHeight: 20.0f, NumItems: pPopupContext->m_vMaps.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: -1, pRect: &View, Background: false);
353
354 int MapIndex = 0;
355 for(auto &Map : pPopupContext->m_vMaps)
356 {
357 MapIndex++;
358 const CListboxItem Item = s_ListBox.DoNextItem(pId: &Map, Selected: MapIndex == pPopupContext->m_Selection);
359 if(!Item.m_Visible)
360 continue;
361
362 CUIRect Label, Icon;
363 Item.m_Rect.VSplitLeft(Cut: 20.0f, pLeft: &Icon, pRight: &Label);
364
365 char aLabelText[IO_MAX_PATH_LENGTH];
366 str_copy(dst&: aLabelText, src: Map.m_aFilename);
367 if(Map.m_IsDirectory)
368 str_append(dst&: aLabelText, src: "/");
369
370 const char *pIconType;
371 if(!Map.m_IsDirectory)
372 {
373 pIconType = FontIcon::MAP;
374 }
375 else
376 {
377 if(!str_comp(a: Map.m_aFilename, b: ".."))
378 pIconType = FontIcon::FOLDER_TREE;
379 else
380 pIconType = FontIcon::FOLDER;
381 }
382
383 pMenus->TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
384 pMenus->TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
385 pMenus->Ui()->DoLabel(pRect: &Icon, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
386 pMenus->TextRender()->SetRenderFlags(0);
387 pMenus->TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
388
389 pMenus->Ui()->DoLabel(pRect: &Label, pText: aLabelText, Size: 10.0f, Align: TEXTALIGN_ML);
390 }
391
392 const int NewSelected = s_ListBox.DoEnd();
393 pPopupContext->m_Selection = NewSelected >= 0 ? NewSelected : -1;
394 if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated())
395 {
396 const CMapListItem &SelectedItem = pPopupContext->m_vMaps[pPopupContext->m_Selection];
397
398 if(SelectedItem.m_IsDirectory)
399 {
400 if(!str_comp(a: SelectedItem.m_aFilename, b: ".."))
401 {
402 dbg_assert(fs_parent_dir(pPopupContext->m_aCurrentMapFolder) == 0, "Parent folder item selected but there is no parent folder");
403 }
404 else
405 {
406 str_append(dst&: pPopupContext->m_aCurrentMapFolder, src: "/");
407 str_append(dst&: pPopupContext->m_aCurrentMapFolder, src: SelectedItem.m_aFilename);
408 }
409 pPopupContext->MapListPopulate();
410 }
411 else
412 {
413 str_format(buffer: g_Config.m_ClBackgroundEntities, buffer_size: sizeof(g_Config.m_ClBackgroundEntities), format: "%s/%s", pPopupContext->m_aCurrentMapFolder, SelectedItem.m_aFilename);
414 pMenus->GameClient()->m_Background.LoadBackground();
415 return CUi::POPUP_CLOSE_CURRENT;
416 }
417 }
418
419 return CUi::POPUP_KEEP_OPEN;
420}
421
422void CMenus::CPopupMapPickerContext::MapListPopulate()
423{
424 m_vMaps.clear();
425 char aTemp[IO_MAX_PATH_LENGTH];
426 str_format(buffer: aTemp, buffer_size: sizeof(aTemp), format: "maps/%s", m_aCurrentMapFolder);
427 m_pMenus->Storage()->ListDirectoryInfo(Type: IStorage::TYPE_ALL, pPath: aTemp, pfnCallback: MapListFetchCallback, pUser: this);
428 std::stable_sort(first: m_vMaps.begin(), last: m_vMaps.end(), comp: CompareFilenameAscending);
429 m_Selection = -1;
430}
431
432int CMenus::CPopupMapPickerContext::MapListFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
433{
434 CPopupMapPickerContext *pRealUser = (CPopupMapPickerContext *)pUser;
435 if((!IsDir && !str_endswith(str: pInfo->m_pName, suffix: ".map")) || !str_comp(a: pInfo->m_pName, b: ".") || (!str_comp(a: pInfo->m_pName, b: "..") && (!str_comp(a: pRealUser->m_aCurrentMapFolder, b: ""))))
436 return 0;
437
438 CMapListItem Item;
439 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
440 Item.m_IsDirectory = IsDir;
441
442 pRealUser->m_vMaps.emplace_back(args&: Item);
443
444 return 0;
445}
446