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
4#include "maplayers.h"
5#include "menus.h"
6
7#include <base/fs.h>
8#include <base/hash.h>
9#include <base/io.h>
10#include <base/math.h>
11#include <base/str.h>
12#include <base/time.h>
13
14#include <engine/client.h>
15#include <engine/demo.h>
16#include <engine/font_icons.h>
17#include <engine/graphics.h>
18#include <engine/keys.h>
19#include <engine/shared/localization.h>
20#include <engine/storage.h>
21#include <engine/textrender.h>
22
23#include <generated/client_data.h>
24
25#include <game/client/components/console.h>
26#include <game/client/gameclient.h>
27#include <game/client/ui.h>
28#include <game/client/ui_listbox.h>
29#include <game/localization.h>
30
31#include <chrono>
32
33using namespace std::chrono_literals;
34
35bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser)
36{
37 bool DoFilterChat = *(bool *)pUser;
38 if(!DoFilterChat)
39 {
40 return false;
41 }
42
43 CUnpacker Unpacker;
44 Unpacker.Reset(pData, Size);
45
46 int Msg = Unpacker.GetInt();
47 int Sys = Msg & 1;
48 Msg >>= 1;
49
50 return !Unpacker.Error() && !Sys && Msg == NETMSGTYPE_SV_CHAT;
51}
52
53void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek)
54{
55 if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f)
56 {
57 GameClient()->m_Chat.Reset();
58 GameClient()->m_DamageInd.OnReset();
59 GameClient()->m_InfoMessages.OnReset();
60 GameClient()->m_Particles.OnReset();
61 GameClient()->m_Sounds.OnReset();
62 GameClient()->m_Scoreboard.OnReset();
63 GameClient()->m_Statboard.OnReset();
64 GameClient()->m_SuppressEvents = true;
65 if(TimeToSeek != 0.0f)
66 DemoPlayer()->SeekTime(Seconds: TimeToSeek);
67 else
68 DemoPlayer()->SeekPercent(Percent: PositionToSeek);
69 GameClient()->m_SuppressEvents = false;
70
71 if(!DemoPlayer()->BaseInfo()->m_Paused &&
72 !DemoPlayer()->BaseInfo()->m_LiveDemo &&
73 PositionToSeek == 1.0f)
74 {
75 DemoPlayer()->Pause();
76 }
77 }
78}
79
80void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset)
81{
82 GameClient()->m_SuppressEvents = true;
83 DemoPlayer()->SeekTick(TickOffset);
84 GameClient()->m_SuppressEvents = false;
85 DemoPlayer()->Pause();
86}
87
88void CMenus::RenderDemoPlayer(CUIRect MainView)
89{
90 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
91 const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick;
92 const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick;
93
94 // When rendering a demo and starting paused, render the pause indicator permanently.
95#if defined(CONF_VIDEORECORDER)
96 const bool VideoRendering = IVideo::Current() != nullptr;
97 bool InitialVideoPause = VideoRendering && m_LastPauseChange < 0.0f && pInfo->m_Paused;
98#else
99 const bool VideoRendering = false;
100 bool InitialVideoPause = false;
101#endif
102
103 const auto &&UpdateLastPauseChange = [&]() {
104 // Immediately hide the pause indicator when unpausing the initial pause when rendering a demo.
105 m_LastPauseChange = InitialVideoPause ? 0.0f : Client()->GlobalTime();
106 InitialVideoPause = false;
107 };
108 const auto &&UpdateLastSpeedChange = [&]() {
109 m_LastSpeedChange = Client()->GlobalTime();
110 };
111
112 // threshold value, accounts for slight inaccuracy when setting demo position
113 constexpr int Threshold = 10;
114 const auto &&FindPreviousMarkerPosition = [&]() {
115 for(int i = pInfo->m_NumTimelineMarkers - 1; i >= 0; i--)
116 {
117 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) < CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
118 {
119 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
120 }
121 }
122 return 0.0f;
123 };
124 const auto &&FindNextMarkerPosition = [&]() {
125 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
126 {
127 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) > CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
128 {
129 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
130 }
131 }
132 return 1.0f;
133 };
134
135 static constexpr float SKIP_DURATIONS_SECONDS[] = {0.1f, 0.5f, 1.0f, 5.0f, 10.0f, 30.0f, 60.0f, 5.0f * 60.0f, 10.0f * 60.0f};
136 static constexpr const char *SKIP_DURATIONS_STRINGS[] = {"0.1", "0.5", "1", "5", "10", "30", "1", "5", "10"};
137 static_assert(SKIP_DURATIONS_SECONDS[DEFAULT_SKIP_DURATION_INDEX] == 5.0f);
138 static_assert(std::size(SKIP_DURATIONS_SECONDS) == std::size(SKIP_DURATIONS_STRINGS));
139
140 const float DemoLengthSeconds = TotalTicks / static_cast<float>(Client()->GameTickSpeed());
141 int NumDurationLabels = 0;
142 for(size_t i = 0; i < std::size(SKIP_DURATIONS_SECONDS); ++i)
143 {
144 if(SKIP_DURATIONS_SECONDS[i] >= DemoLengthSeconds)
145 break;
146 NumDurationLabels = i + 1;
147 }
148 if(NumDurationLabels < 2)
149 {
150 m_SkipDurationIndex = 0;
151 }
152 else if(m_SkipDurationIndex >= NumDurationLabels)
153 {
154 m_SkipDurationIndex = NumDurationLabels - 1;
155 }
156
157 // handle keyboard shortcuts independent of active menu
158 float PositionToSeek = -1.0f;
159 float TimeToSeek = 0.0f;
160 if(!GameClient()->m_GameConsole.IsActive() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !Ui()->IsPopupOpen())
161 {
162 // increase/decrease speed
163 if(!Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed())
164 {
165 if(Input()->KeyPress(Key: KEY_UP) || (m_MenuActive && Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP)))
166 {
167 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
168 UpdateLastSpeedChange();
169 }
170 else if(Input()->KeyPress(Key: KEY_DOWN) || (m_MenuActive && Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN)))
171 {
172 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
173 UpdateLastSpeedChange();
174 }
175 }
176
177 // pause/unpause
178 if(Input()->KeyPress(Key: KEY_SPACE) || Input()->KeyPress(Key: KEY_RETURN) || Input()->KeyPress(Key: KEY_KP_ENTER) || Input()->KeyPress(Key: KEY_K))
179 {
180 if(pInfo->m_Paused)
181 {
182 DemoPlayer()->Unpause();
183 }
184 else
185 {
186 DemoPlayer()->Pause();
187 }
188 UpdateLastPauseChange();
189 }
190
191 // seek backward/forward configured time
192 if(Input()->KeyPress(Key: KEY_LEFT) || Input()->KeyPress(Key: KEY_J))
193 {
194 if(Input()->ModifierIsPressed())
195 {
196 PositionToSeek = FindPreviousMarkerPosition();
197 }
198 else if(Input()->ShiftIsPressed())
199 {
200 if(m_SkipDurationIndex > 0)
201 {
202 --m_SkipDurationIndex;
203 }
204 }
205 else
206 {
207 TimeToSeek = -SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
208 }
209 }
210 else if(Input()->KeyPress(Key: KEY_RIGHT) || Input()->KeyPress(Key: KEY_L))
211 {
212 if(Input()->ModifierIsPressed())
213 {
214 PositionToSeek = FindNextMarkerPosition();
215 }
216 else if(Input()->ShiftIsPressed())
217 {
218 if(m_SkipDurationIndex < NumDurationLabels - 1)
219 {
220 ++m_SkipDurationIndex;
221 }
222 }
223 else
224 {
225 TimeToSeek = SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
226 }
227 }
228
229 // seek to 0-90%
230 const int aSeekPercentKeys[] = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9};
231 for(unsigned i = 0; i < std::size(aSeekPercentKeys); i++)
232 {
233 if(Input()->KeyPress(Key: aSeekPercentKeys[i]))
234 {
235 PositionToSeek = i * 0.1f;
236 break;
237 }
238 }
239
240 // seek to the beginning/end
241 if(Input()->KeyPress(Key: KEY_HOME))
242 {
243 PositionToSeek = 0.0f;
244 }
245 else if(Input()->KeyPress(Key: KEY_END))
246 {
247 PositionToSeek = 1.0f;
248 }
249
250 // Advance single frame forward/backward with period/comma key
251 if(Input()->KeyPress(Key: KEY_PERIOD))
252 {
253 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
254 }
255 else if(Input()->KeyPress(Key: KEY_COMMA))
256 {
257 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
258 }
259 }
260
261 const float SeekBarHeight = 15.0f;
262 const float ButtonbarHeight = 20.0f;
263 const float NameBarHeight = 20.0f;
264 const float Margins = 5.0f;
265 const float TotalHeight = SeekBarHeight + ButtonbarHeight + NameBarHeight + Margins * 3;
266
267 if(!m_MenuActive)
268 {
269 // Render pause indicator
270 if(g_Config.m_ClDemoShowPause && (InitialVideoPause || (!VideoRendering && Client()->GlobalTime() - m_LastPauseChange < 0.5f)))
271 {
272 const float Time = InitialVideoPause ? 0.5f : ((Client()->GlobalTime() - m_LastPauseChange) / 0.5f);
273 const float Alpha = (Time < 0.5f ? Time : (1.0f - Time)) * 2.0f;
274 if(Alpha > 0.0f)
275 {
276 TextRender()->TextColor(Color: TextRender()->DefaultTextColor().WithMultipliedAlpha(alpha: Alpha));
277 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(alpha: Alpha));
278 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
279 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
280 Ui()->DoLabel(pRect: Ui()->Screen(), pText: pInfo->m_Paused ? FontIcon::PAUSE : FontIcon::PLAY, Size: 36.0f + Time * 12.0f, Align: TEXTALIGN_MC);
281 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
282 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
283 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
284 TextRender()->SetRenderFlags(0);
285 }
286 }
287
288 // Render speed info
289 if(g_Config.m_ClDemoShowSpeed && Client()->GlobalTime() - m_LastSpeedChange < 1.0f)
290 {
291 CUIRect Screen = *Ui()->Screen();
292
293 char aSpeedBuf[16];
294 str_format(buffer: aSpeedBuf, buffer_size: sizeof(aSpeedBuf), format: "×%.2f", pInfo->m_Speed);
295 TextRender()->Text(x: 120.0f, y: Screen.y + Screen.h - 120.0f - TotalHeight, Size: 60.0f, pText: aSpeedBuf, LineWidth: -1.0f);
296 }
297 }
298 else
299 {
300 if(m_LastPauseChange > 0.0f)
301 m_LastPauseChange = 0.0f;
302 if(m_LastSpeedChange > 0.0f)
303 m_LastSpeedChange = 0.0f;
304 }
305
306 if(CurrentTick == TotalTicks)
307 {
308 DemoPlayer()->Pause();
309 PositionToSeek = 0.0f;
310 UpdateLastPauseChange();
311 }
312
313 if(!m_MenuActive)
314 {
315 HandleDemoSeeking(PositionToSeek, TimeToSeek);
316 return;
317 }
318
319 CUIRect DemoControls;
320 MainView.HSplitBottom(Cut: TotalHeight, pTop: nullptr, pBottom: &DemoControls);
321 DemoControls.VSplitLeft(Cut: 50.0f, pLeft: nullptr, pRight: &DemoControls);
322 DemoControls.VSplitLeft(Cut: 600.0f, pLeft: &DemoControls, pRight: nullptr);
323 const CUIRect DemoControlsOriginal = DemoControls;
324 DemoControls.x += m_DemoControlsPositionOffset.x;
325 DemoControls.y += m_DemoControlsPositionOffset.y;
326 int Corners = IGraphics::CORNER_NONE;
327 if(DemoControls.x > 0.0f && DemoControls.y > 0.0f)
328 Corners |= IGraphics::CORNER_TL;
329 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y > 0.0f)
330 Corners |= IGraphics::CORNER_TR;
331 if(DemoControls.x > 0.0f && DemoControls.y < MainView.h - DemoControls.h)
332 Corners |= IGraphics::CORNER_BL;
333 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y < MainView.h - DemoControls.h)
334 Corners |= IGraphics::CORNER_BR;
335 DemoControls.Draw(Color: ms_ColorTabbarActive, Corners, Rounding: 10.0f);
336 const CUIRect DemoControlsDragRect = DemoControls;
337
338 CUIRect SeekBar, ButtonBar, NameBar, SpeedBar;
339 DemoControls.Margin(Cut: 5.0f, pOtherRect: &DemoControls);
340 DemoControls.HSplitTop(Cut: SeekBarHeight, pTop: &SeekBar, pBottom: &ButtonBar);
341 ButtonBar.HSplitTop(Cut: Margins, pTop: nullptr, pBottom: &ButtonBar);
342 ButtonBar.HSplitBottom(Cut: NameBarHeight, pTop: &ButtonBar, pBottom: &NameBar);
343 NameBar.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &NameBar);
344
345 // handle draggable demo controls
346 {
347 enum EDragOperation
348 {
349 OP_NONE,
350 OP_DRAGGING,
351 OP_CLICKED
352 };
353 static EDragOperation s_Operation = OP_NONE;
354 static vec2 s_InitialMouse = vec2(0.0f, 0.0f);
355
356 bool Clicked;
357 bool Abrupted;
358 if(int Result = Ui()->DoDraggableButtonLogic(pId: &s_Operation, Checked: 8, pRect: &DemoControlsDragRect, pClicked: &Clicked, pAbrupted: &Abrupted))
359 {
360 if(s_Operation == OP_NONE && Result == 1)
361 {
362 s_InitialMouse = Ui()->MousePos();
363 s_Operation = OP_CLICKED;
364 }
365
366 if(Clicked || Abrupted)
367 s_Operation = OP_NONE;
368
369 if(s_Operation == OP_CLICKED && length(a: Ui()->MousePos() - s_InitialMouse) > 5.0f)
370 {
371 s_Operation = OP_DRAGGING;
372 s_InitialMouse -= m_DemoControlsPositionOffset;
373 }
374
375 if(s_Operation == OP_DRAGGING)
376 {
377 m_DemoControlsPositionOffset = Ui()->MousePos() - s_InitialMouse;
378 m_DemoControlsPositionOffset.x = std::clamp(val: m_DemoControlsPositionOffset.x, lo: -DemoControlsOriginal.x, hi: MainView.w - DemoControlsDragRect.w - DemoControlsOriginal.x);
379 m_DemoControlsPositionOffset.y = std::clamp(val: m_DemoControlsPositionOffset.y, lo: -DemoControlsOriginal.y, hi: MainView.h - DemoControlsDragRect.h - DemoControlsOriginal.y);
380 }
381 }
382 }
383
384 // Live button
385 if(pInfo->m_LiveDemo)
386 {
387 CUIRect LiveButton;
388 SeekBar.VSplitRight(Cut: SeekBar.h, pLeft: &SeekBar, pRight: &LiveButton);
389 SeekBar.VSplitRight(Cut: 2.0f, pLeft: &SeekBar, pRight: nullptr);
390 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
391 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
392 TextRender()->TextColor(Color: pInfo->m_LivePlayback ? ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f) : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f));
393 Ui()->DoLabel(pRect: &LiveButton, pText: FontIcon::CIRCLE, Size: 24.0f, Align: TEXTALIGN_MC);
394 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
395 TextRender()->SetRenderFlags(0);
396 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
397 static char s_LiveButtonId;
398 if(Ui()->HotItem() == &s_LiveButtonId)
399 {
400 LiveButton.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
401 }
402 if(Ui()->DoButtonLogic(pId: &s_LiveButtonId, Checked: 0, pRect: &LiveButton, Flags: BUTTONFLAG_LEFT))
403 {
404 PositionToSeek = 1.0f;
405 DemoPlayer()->SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
406 UpdateLastSpeedChange();
407 }
408 GameClient()->m_Tooltips.DoToolTip(pId: &s_LiveButtonId, pNearRect: &LiveButton,
409 pText: pInfo->m_LivePlayback ? Localize(pStr: "Live", pContext: "Demo playback") : Localize(pStr: "Go to Live", pContext: "Demo playback"));
410 }
411
412 // do seekbar
413 {
414 // draw seek bar
415 const float Rounding = 5.0f;
416 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
417
418 // draw filled bar
419 float Amount = CurrentTick / (float)TotalTicks;
420 CUIRect FilledBar = SeekBar;
421 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
422 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
423
424 // draw highlighting
425 if(g_Config.m_ClDemoSliceBegin != -1 && g_Config.m_ClDemoSliceEnd != -1)
426 {
427 float RatioBegin = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
428 float RatioEnd = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
429 float Span = ((SeekBar.w - 2 * Rounding) * RatioEnd) - ((SeekBar.w - 2 * Rounding) * RatioBegin);
430 Graphics()->TextureClear();
431 Graphics()->QuadsBegin();
432 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 0.25f);
433 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * RatioBegin, SeekBar.y, Span, SeekBar.h);
434 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
435 Graphics()->QuadsEnd();
436 }
437
438 // draw markers
439 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
440 {
441 float Ratio = (pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / (float)TotalTicks;
442 Graphics()->TextureClear();
443 Graphics()->QuadsBegin();
444 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
445 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
446 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
447 Graphics()->QuadsEnd();
448 }
449
450 // draw slice markers
451 // begin
452 if(g_Config.m_ClDemoSliceBegin != -1)
453 {
454 float Ratio = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
455 Graphics()->TextureClear();
456 Graphics()->QuadsBegin();
457 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
458 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
459 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
460 Graphics()->QuadsEnd();
461 }
462
463 // end
464 if(g_Config.m_ClDemoSliceEnd != -1)
465 {
466 float Ratio = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
467 Graphics()->TextureClear();
468 Graphics()->QuadsBegin();
469 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
470 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
471 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
472 Graphics()->QuadsEnd();
473 }
474
475 // draw time
476 char aCurrentTime[32];
477 str_time(centisecs: (int64_t)CurrentTick / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
478 char aTotalTime[32];
479 str_time(centisecs: (int64_t)TotalTicks / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
480 char aSeekBarLabel[128];
481 str_format(buffer: aSeekBarLabel, buffer_size: sizeof(aSeekBarLabel), format: "%s / %s", aCurrentTime, aTotalTime);
482 Ui()->DoLabel(pRect: &SeekBar, pText: aSeekBarLabel, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
483
484 // do the logic
485 static char s_SeekBarId;
486 if(Ui()->CheckActiveItem(pId: &s_SeekBarId))
487 {
488 if(!Ui()->MouseButton(Index: 0))
489 {
490 if(!m_PausedBeforeSeeking)
491 {
492 DemoPlayer()->Unpause();
493 }
494 Ui()->SetActiveItem(nullptr);
495 }
496 else
497 {
498 float SeekAmount = std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
499 if(Input()->ShiftIsPressed())
500 {
501 Ui()->SetMouseSlow(true);
502 }
503 if(absolute(a: m_PrevSeekAmount - SeekAmount) >= 0.0001f)
504 {
505 PositionToSeek = m_PrevSeekAmount = SeekAmount;
506 }
507 }
508 }
509 else if(Ui()->HotItem() == &s_SeekBarId)
510 {
511 if(Ui()->MouseButton(Index: 0))
512 {
513 m_PrevSeekAmount = -1.0f;
514 m_PausedBeforeSeeking = pInfo->m_Paused;
515 if(!pInfo->m_Paused)
516 {
517 DemoPlayer()->Pause();
518 }
519 Ui()->SetActiveItem(&s_SeekBarId);
520 }
521 }
522
523 if(Ui()->MouseInside(pRect: &SeekBar) && !Ui()->MouseButton(Index: 0))
524 Ui()->SetHotItem(&s_SeekBarId);
525
526 if(Ui()->HotItem() == &s_SeekBarId)
527 {
528 const int HoveredTick = (int)(std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f) * TotalTicks);
529 static char s_aHoveredTime[32];
530 str_time(centisecs: (int64_t)HoveredTick / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: s_aHoveredTime, buffer_size: sizeof(s_aHoveredTime));
531 GameClient()->m_Tooltips.DoToolTip(pId: &s_SeekBarId, pNearRect: &SeekBar, pText: s_aHoveredTime);
532 }
533 }
534
535 bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false;
536
537 // do buttons
538 CUIRect Button;
539
540 // combined play and pause button
541 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
542 static CButtonContainer s_PlayPauseButton;
543 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_PlayPauseButton, pText: pInfo->m_Paused ? FontIcon::PLAY : FontIcon::PAUSE, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT))
544 {
545 if(pInfo->m_Paused)
546 {
547 DemoPlayer()->Unpause();
548 }
549 else
550 {
551 DemoPlayer()->Pause();
552 }
553 UpdateLastPauseChange();
554 }
555 GameClient()->m_Tooltips.DoToolTip(pId: &s_PlayPauseButton, pNearRect: &Button, pText: pInfo->m_Paused ? Localize(pStr: "Play the current demo") : Localize(pStr: "Pause the current demo"));
556
557 // stop button
558 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
559 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
560 static CButtonContainer s_ResetButton;
561 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ResetButton, pText: FontIcon::STOP, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT))
562 {
563 DemoPlayer()->Pause();
564 PositionToSeek = 0.0f;
565 }
566 GameClient()->m_Tooltips.DoToolTip(pId: &s_ResetButton, pNearRect: &Button, pText: Localize(pStr: "Stop the current demo"));
567
568 // skip time back
569 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
570 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
571 static CButtonContainer s_TimeBackButton;
572 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_TimeBackButton, pText: FontIcon::BACKWARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
573 {
574 TimeToSeek = -SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
575 }
576 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back the specified duration"));
577
578 // skip time dropdown
579 if(NumDurationLabels >= 2)
580 {
581 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
582 ButtonBar.VSplitLeft(Cut: 4 * ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
583
584 static std::vector<std::string> s_vDurationNames;
585 static std::vector<const char *> s_vpDurationNames;
586 s_vDurationNames.resize(sz: NumDurationLabels);
587 s_vpDurationNames.resize(sz: NumDurationLabels);
588
589 for(int i = 0; i < NumDurationLabels; ++i)
590 {
591 char aBuf[256];
592 if(SKIP_DURATIONS_SECONDS[i] >= 60)
593 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%s min.", pContext: "Demo player duration"), SKIP_DURATIONS_STRINGS[i]);
594 else
595 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%s sec.", pContext: "Demo player duration"), SKIP_DURATIONS_STRINGS[i]);
596 s_vDurationNames[i] = aBuf;
597 s_vpDurationNames[i] = s_vDurationNames[i].c_str();
598 }
599
600 static CUi::SDropDownState s_SkipDurationDropDownState;
601 static CScrollRegion s_SkipDurationDropDownScrollRegion;
602 s_SkipDurationDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_SkipDurationDropDownScrollRegion;
603 m_SkipDurationIndex = Ui()->DoDropDown(pRect: &Button, CurSelection: m_SkipDurationIndex, pStrs: s_vpDurationNames.data(), Num: NumDurationLabels, State&: s_SkipDurationDropDownState);
604 GameClient()->m_Tooltips.DoToolTip(pId: &s_SkipDurationDropDownState.m_ButtonContainer, pNearRect: &Button, pText: Localize(pStr: "Change the skip duration"));
605 }
606
607 // skip time forward
608 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
609 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
610 static CButtonContainer s_TimeForwardButton;
611 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_TimeForwardButton, pText: FontIcon::FORWARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
612 {
613 TimeToSeek = SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
614 }
615 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward the specified duration"));
616
617 // one tick back
618 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
619 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
620 static CButtonContainer s_OneTickBackButton;
621 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneTickBackButton, pText: FontIcon::BACKWARD_STEP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
622 {
623 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
624 }
625 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one tick"));
626
627 // one tick forward
628 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
629 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
630 static CButtonContainer s_OneTickForwardButton;
631 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneTickForwardButton, pText: FontIcon::FORWARD_STEP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
632 {
633 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
634 }
635 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one tick"));
636
637 // one marker back
638 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
639 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
640 static CButtonContainer s_OneMarkerBackButton;
641 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneMarkerBackButton, pText: FontIcon::BACKWARD_FAST, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
642 {
643 PositionToSeek = FindPreviousMarkerPosition();
644 }
645 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one marker"));
646
647 // one marker forward
648 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
649 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
650 static CButtonContainer s_OneMarkerForwardButton;
651 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneMarkerForwardButton, pText: FontIcon::FORWARD_FAST, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
652 {
653 PositionToSeek = FindNextMarkerPosition();
654 }
655 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one marker"));
656
657 // slowdown
658 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
659 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
660 static CButtonContainer s_SlowDownButton;
661 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SlowDownButton, pText: FontIcon::CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
662 DecreaseDemoSpeed = true;
663 GameClient()->m_Tooltips.DoToolTip(pId: &s_SlowDownButton, pNearRect: &Button, pText: Localize(pStr: "Slow down the demo"));
664
665 // fastforward
666 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
667 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
668 static CButtonContainer s_SpeedUpButton;
669 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SpeedUpButton, pText: FontIcon::CHEVRON_UP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
670 IncreaseDemoSpeed = true;
671 GameClient()->m_Tooltips.DoToolTip(pId: &s_SpeedUpButton, pNearRect: &Button, pText: Localize(pStr: "Speed up the demo"));
672
673 // speed meter
674 ButtonBar.VSplitLeft(Cut: Margins * 12, pLeft: &SpeedBar, pRight: &ButtonBar);
675 char aBuffer[64];
676 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "×%g", pInfo->m_Speed);
677 Ui()->DoLabel(pRect: &SpeedBar, pText: aBuffer, Size: Button.h * 0.7f, Align: TEXTALIGN_MC);
678
679 // slice begin button
680 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
681 static CButtonContainer s_SliceBeginButton;
682 const int SliceBeginButtonResult = Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceBeginButton, pText: FontIcon::RIGHT_FROM_BRACKET, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
683 if(SliceBeginButtonResult == 1)
684 {
685 Client()->DemoSliceBegin();
686 if(CurrentTick > (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick))
687 g_Config.m_ClDemoSliceEnd = -1;
688 }
689 else if(SliceBeginButtonResult == 2)
690 {
691 g_Config.m_ClDemoSliceBegin = -1;
692 }
693 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceBeginButton, pNearRect: &Button, pText: Localize(pStr: "Mark the beginning of a cut (right click to reset)"));
694
695 // slice end button
696 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
697 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
698 static CButtonContainer s_SliceEndButton;
699 const int SliceEndButtonResult = Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceEndButton, pText: FontIcon::RIGHT_TO_BRACKET, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
700 if(SliceEndButtonResult == 1)
701 {
702 Client()->DemoSliceEnd();
703 if(CurrentTick < (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick))
704 g_Config.m_ClDemoSliceBegin = -1;
705 }
706 else if(SliceEndButtonResult == 2)
707 {
708 g_Config.m_ClDemoSliceEnd = -1;
709 }
710 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceEndButton, pNearRect: &Button, pText: Localize(pStr: "Mark the end of a cut (right click to reset)"));
711
712 // slice save button
713#if defined(CONF_VIDEORECORDER)
714 const bool SliceEnabled = IVideo::Current() == nullptr;
715#else
716 const bool SliceEnabled = true;
717#endif
718 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
719 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
720 static CButtonContainer s_SliceSaveButton;
721 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceSaveButton, pText: FontIcon::ARROW_UP_RIGHT_FROM_SQUARE, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: SliceEnabled) && SliceEnabled)
722 {
723 char aDemoName[IO_MAX_PATH_LENGTH];
724 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
725 m_DemoSliceInput.Set(aDemoName);
726 Ui()->SetActiveItem(&m_DemoSliceInput);
727 m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE;
728 }
729 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceSaveButton, pNearRect: &Button, pText: Localize(pStr: "Export cut as a separate demo"));
730
731 // close button
732 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
733 static CButtonContainer s_ExitButton;
734 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ExitButton, pText: FontIcon::XMARK, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT) || (Input()->KeyPress(Key: KEY_C) && !GameClient()->m_GameConsole.IsActive() && m_DemoPlayerState == DEMOPLAYER_NONE))
735 {
736 Client()->Disconnect();
737 SetMenuPage(PAGE_DEMOS);
738 DemolistOnUpdate(Reset: false);
739 }
740 GameClient()->m_Tooltips.DoToolTip(pId: &s_ExitButton, pNearRect: &Button, pText: Localize(pStr: "Close the demo player"));
741
742 // toggle keyboard shortcuts button
743 ButtonBar.VSplitRight(Cut: Margins, pLeft: &ButtonBar, pRight: nullptr);
744 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
745 static CButtonContainer s_KeyboardShortcutsButton;
746 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_KeyboardShortcutsButton, pText: FontIcon::KEYBOARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: g_Config.m_ClDemoKeyboardShortcuts != 0))
747 {
748 g_Config.m_ClDemoKeyboardShortcuts ^= 1;
749 }
750 GameClient()->m_Tooltips.DoToolTip(pId: &s_KeyboardShortcutsButton, pNearRect: &Button, pText: Localize(pStr: "Toggle keyboard shortcuts"));
751
752 // auto camera button (only available when it is possible to use)
753 if(GameClient()->m_Camera.CanUseAutoSpecCamera())
754 {
755 ButtonBar.VSplitRight(Cut: Margins, pLeft: &ButtonBar, pRight: nullptr);
756 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
757 static CButtonContainer s_AutoCameraButton;
758 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_AutoCameraButton, pText: FontIcon::CAMERA, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: GameClient()->m_Camera.m_AutoSpecCamera))
759 {
760 GameClient()->m_Camera.m_AutoSpecCamera = !GameClient()->m_Camera.m_AutoSpecCamera;
761 }
762 GameClient()->m_Tooltips.DoToolTip(pId: &s_AutoCameraButton, pNearRect: &Button, pText: Localize(pStr: "Toggle auto camera"));
763 }
764
765 // demo name
766 char aDemoName[IO_MAX_PATH_LENGTH];
767 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
768 char aBuf[IO_MAX_PATH_LENGTH + 128];
769 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Demofile: %s"), aDemoName);
770 SLabelProperties Props;
771 Props.m_MaxWidth = NameBar.w;
772 Props.m_EllipsisAtEnd = true;
773 Props.m_EnableWidthCheck = false;
774 Ui()->DoLabel(pRect: &NameBar, pText: aBuf, Size: Button.h * 0.5f, Align: TEXTALIGN_ML, LabelProps: Props);
775
776 if(IncreaseDemoSpeed)
777 {
778 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
779 UpdateLastSpeedChange();
780 }
781 else if(DecreaseDemoSpeed)
782 {
783 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
784 UpdateLastSpeedChange();
785 }
786
787 HandleDemoSeeking(PositionToSeek, TimeToSeek);
788
789 // render popups
790 if(m_DemoPlayerState != DEMOPLAYER_NONE)
791 {
792 // prevent element under the active popup from being activated
793 Ui()->SetHotItem(nullptr);
794 }
795 if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE)
796 {
797 RenderDemoPlayerSliceSavePopup(MainView);
798 }
799}
800
801void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView)
802{
803 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
804
805 CUIRect Box;
806 MainView.Margin(Cut: 150.0f, pOtherRect: &Box);
807
808 // background
809 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
810 Box.Margin(Cut: 24.0f, pOtherRect: &Box);
811
812 // title
813 CUIRect Title;
814 Box.HSplitTop(Cut: 24.0f, pTop: &Title, pBottom: &Box);
815 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
816 Ui()->DoLabel(pRect: &Title, pText: Localize(pStr: "Export demo cut"), Size: 24.0f, Align: TEXTALIGN_MC);
817
818 // slice times
819 CUIRect SliceTimesBar, SliceInterval, SliceLength;
820 Box.HSplitTop(Cut: 24.0f, pTop: &SliceTimesBar, pBottom: &Box);
821 SliceTimesBar.VSplitMid(pLeft: &SliceInterval, pRight: &SliceLength, Spacing: 40.0f);
822 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
823 const int64_t RealSliceBegin = g_Config.m_ClDemoSliceBegin == -1 ? 0 : (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick);
824 const int64_t RealSliceEnd = (g_Config.m_ClDemoSliceEnd == -1 ? pInfo->m_LastTick : g_Config.m_ClDemoSliceEnd) - pInfo->m_FirstTick;
825 char aSliceBegin[32];
826 str_time(centisecs: RealSliceBegin / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: aSliceBegin, buffer_size: sizeof(aSliceBegin));
827 char aSliceEnd[32];
828 str_time(centisecs: RealSliceEnd / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: aSliceEnd, buffer_size: sizeof(aSliceEnd));
829 char aSliceLength[32];
830 str_time(centisecs: (RealSliceEnd - RealSliceBegin) / Client()->GameTickSpeed() * 100, format: ETimeFormat::HOURS, buffer: aSliceLength, buffer_size: sizeof(aSliceLength));
831 char aBuf[256];
832 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s – %s", Localize(pStr: "Cut interval"), aSliceBegin, aSliceEnd);
833 Ui()->DoLabel(pRect: &SliceInterval, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
834 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Cut length"), aSliceLength);
835 Ui()->DoLabel(pRect: &SliceLength, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
836
837 // file name
838 CUIRect NameLabel, NameBox;
839 Box.HSplitTop(Cut: 24.0f, pTop: &NameLabel, pBottom: &Box);
840 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
841 NameLabel.VSplitLeft(Cut: 150.0f, pLeft: &NameLabel, pRight: &NameBox);
842 NameBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &NameBox);
843 Ui()->DoLabel(pRect: &NameLabel, pText: Localize(pStr: "New name:"), Size: 18.0f, Align: TEXTALIGN_ML);
844 Ui()->DoEditBox(pLineInput: &m_DemoSliceInput, pRect: &NameBox, FontSize: 12.0f);
845
846 // remove chat checkbox
847 static int s_RemoveChat = 0;
848
849 CUIRect CheckBoxBar, RemoveChatCheckBox, RenderCutCheckBox;
850 Box.HSplitTop(Cut: 24.0f, pTop: &CheckBoxBar, pBottom: &Box);
851 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
852 CheckBoxBar.VSplitMid(pLeft: &RemoveChatCheckBox, pRight: &RenderCutCheckBox, Spacing: 40.0f);
853 if(DoButton_CheckBox(pId: &s_RemoveChat, pText: Localize(pStr: "Remove chat"), Checked: s_RemoveChat, pRect: &RemoveChatCheckBox))
854 {
855 s_RemoveChat ^= 1;
856 }
857#if defined(CONF_VIDEORECORDER)
858 static int s_RenderCut = 0;
859 if(DoButton_CheckBox(pId: &s_RenderCut, pText: Localize(pStr: "Render cut to video"), Checked: s_RenderCut, pRect: &RenderCutCheckBox))
860 {
861 s_RenderCut ^= 1;
862 }
863#endif
864
865 // buttons
866 CUIRect ButtonBar, AbortButton, OkButton;
867 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
868 ButtonBar.VSplitMid(pLeft: &AbortButton, pRight: &OkButton, Spacing: 40.0f);
869
870 static CButtonContainer s_ButtonAbort;
871 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &AbortButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
872 m_DemoPlayerState = DEMOPLAYER_NONE;
873
874 static CUi::SConfirmPopupContext s_ConfirmPopupContext;
875 static CButtonContainer s_ButtonOk;
876 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &OkButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
877 {
878 if(str_endswith(str: m_DemoSliceInput.GetString(), suffix: ".demo"))
879 {
880 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
881 fs_split_file_extension(filename: m_DemoSliceInput.GetString(), name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
882 m_DemoSliceInput.Set(aNameWithoutExt);
883 }
884
885 static CUi::SMessagePopupContext s_MessagePopupContext;
886 char aDemoName[IO_MAX_PATH_LENGTH];
887 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
888 if(str_comp(a: aDemoName, b: m_DemoSliceInput.GetString()) == 0)
889 {
890 s_MessagePopupContext.ErrorColor();
891 str_copy(dst&: s_MessagePopupContext.m_aMessage, src: Localize(pStr: "Please use a different filename"));
892 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_MessagePopupContext);
893 }
894 else if(!str_valid_filename(str: m_DemoSliceInput.GetString()))
895 {
896 s_MessagePopupContext.ErrorColor();
897 str_copy(dst&: s_MessagePopupContext.m_aMessage, src: Localize(pStr: "This name cannot be used for files and folders"));
898 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_MessagePopupContext);
899 }
900 else
901 {
902 char aPath[IO_MAX_PATH_LENGTH];
903 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
904 if(Storage()->FileExists(pFilename: aPath, Type: IStorage::TYPE_SAVE))
905 {
906 s_ConfirmPopupContext.Reset();
907 s_ConfirmPopupContext.YesNoButtons();
908 str_copy(dst&: s_ConfirmPopupContext.m_aMessage, src: Localize(pStr: "File already exists, do you want to overwrite it?"));
909 Ui()->ShowPopupConfirm(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_ConfirmPopupContext);
910 }
911 else
912 s_ConfirmPopupContext.m_Result = CUi::SConfirmPopupContext::CONFIRMED;
913 }
914 }
915
916 if(s_ConfirmPopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED)
917 {
918 char aPath[IO_MAX_PATH_LENGTH];
919 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
920 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_DemoSliceInput.GetString());
921 if(str_endswith(str: m_aCurrentDemoSelectionName, suffix: ".demo"))
922 m_aCurrentDemoSelectionName[str_length(str: m_aCurrentDemoSelectionName) - str_length(str: ".demo")] = '\0';
923
924 Client()->DemoSlice(pDstPath: aPath, pfnFilter: CMenus::DemoFilterChat, pUser: &s_RemoveChat);
925 DemolistPopulate();
926 DemolistOnUpdate(Reset: false);
927 m_DemoPlayerState = DEMOPLAYER_NONE;
928#if defined(CONF_VIDEORECORDER)
929 if(s_RenderCut)
930 {
931 m_Popup = POPUP_RENDER_DEMO;
932 m_StartPaused = false;
933 m_DemoRenderInput.Set(m_aCurrentDemoSelectionName);
934 Ui()->SetActiveItem(&m_DemoRenderInput);
935 if(m_DemolistStorageType != IStorage::TYPE_ALL && m_DemolistStorageType != IStorage::TYPE_SAVE)
936 m_DemolistStorageType = IStorage::TYPE_ALL; // Select a storage type containing the sliced demo
937 }
938#endif
939 }
940 if(s_ConfirmPopupContext.m_Result != CUi::SConfirmPopupContext::UNSET)
941 {
942 s_ConfirmPopupContext.Reset();
943 }
944}
945
946int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
947{
948 CMenus *pSelf = (CMenus *)pUser;
949 if(str_comp(a: pInfo->m_pName, b: ".") == 0 ||
950 (str_comp(a: pInfo->m_pName, b: "..") == 0 && (pSelf->m_aCurrentDemoFolder[0] == '\0' || (!pSelf->m_DemolistMultipleStorages && str_comp(a: pSelf->m_aCurrentDemoFolder, b: "demos") == 0))) ||
951 (!IsDir && !str_endswith(str: pInfo->m_pName, suffix: ".demo")))
952 {
953 return 0;
954 }
955
956 CDemoItem Item;
957 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
958 if(IsDir)
959 {
960 str_format(buffer: Item.m_aName, buffer_size: sizeof(Item.m_aName), format: "%s/", pInfo->m_pName);
961 Item.m_Date = 0;
962 }
963 else
964 {
965 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - str_length(str: ".demo"));
966 Item.m_Date = pInfo->m_TimeModified;
967 }
968 Item.m_InfosLoaded = false;
969 Item.m_Valid = false;
970 Item.m_IsDir = IsDir != 0;
971 Item.m_IsLink = false;
972 Item.m_StorageType = StorageType;
973 pSelf->m_vDemos.push_back(x: Item);
974
975 if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms)
976 {
977 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading demo files"), pContent: "", IncreaseCounter: 0);
978 }
979
980 return 0;
981}
982
983void CMenus::DemolistPopulate()
984{
985 m_vDemos.clear();
986
987 int NumStoragesWithDemos = 0;
988 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
989 {
990 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
991 {
992 NumStoragesWithDemos++;
993 }
994 }
995 m_DemolistMultipleStorages = NumStoragesWithDemos > 1;
996
997 if(m_aCurrentDemoFolder[0] == '\0')
998 {
999 {
1000 CDemoItem Item;
1001 str_copy(dst&: Item.m_aFilename, src: "demos");
1002 str_copy(dst&: Item.m_aName, src: Localize(pStr: "All combined"));
1003 Item.m_InfosLoaded = false;
1004 Item.m_Valid = false;
1005 Item.m_Date = 0;
1006 Item.m_IsDir = true;
1007 Item.m_IsLink = true;
1008 Item.m_StorageType = IStorage::TYPE_ALL;
1009 m_vDemos.push_back(x: Item);
1010 }
1011
1012 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
1013 {
1014 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
1015 {
1016 CDemoItem Item;
1017 str_copy(dst&: Item.m_aFilename, src: "demos");
1018 Storage()->GetCompletePath(Type: StorageType, pDir: "demos", pBuffer: Item.m_aName, BufferSize: sizeof(Item.m_aName));
1019 str_append(dst&: Item.m_aName, src: "/");
1020 Item.m_InfosLoaded = false;
1021 Item.m_Valid = false;
1022 Item.m_Date = 0;
1023 Item.m_IsDir = true;
1024 Item.m_IsLink = true;
1025 Item.m_StorageType = StorageType;
1026 m_vDemos.push_back(x: Item);
1027 }
1028 }
1029 }
1030 else
1031 {
1032 m_DemoPopulateStartTime = time_get_nanoseconds();
1033 Storage()->ListDirectoryInfo(Type: m_DemolistStorageType, pPath: m_aCurrentDemoFolder, pfnCallback: DemolistFetchCallback, pUser: this);
1034
1035 // Make sure there is a demo item to navigate back to the parent folder, if the folder contents could not be enumerated.
1036 if(m_vDemos.empty())
1037 {
1038 CDemoItem Item;
1039 str_copy(dst&: Item.m_aFilename, src: "..");
1040 str_copy(dst&: Item.m_aName, src: "../");
1041 Item.m_Date = 0;
1042 Item.m_InfosLoaded = false;
1043 Item.m_Valid = false;
1044 Item.m_IsDir = true;
1045 Item.m_IsLink = false;
1046 Item.m_StorageType = m_DemolistStorageType;
1047 m_vDemos.push_back(x: Item);
1048 }
1049
1050 if(g_Config.m_BrDemoFetchInfo)
1051 FetchAllHeaders();
1052
1053 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1054 }
1055 RefreshFilteredDemos();
1056}
1057
1058void CMenus::RefreshFilteredDemos()
1059{
1060 m_vpFilteredDemos.clear();
1061 for(auto &Demo : m_vDemos)
1062 {
1063 if(str_find_nocase(haystack: Demo.m_aFilename, needle: m_DemoSearchInput.GetString()))
1064 {
1065 m_vpFilteredDemos.push_back(x: &Demo);
1066 }
1067 }
1068}
1069
1070void CMenus::DemolistOnUpdate(bool Reset)
1071{
1072 if(Reset)
1073 {
1074 if(m_vpFilteredDemos.empty())
1075 {
1076 m_DemolistSelectedIndex = -1;
1077 m_aCurrentDemoSelectionName[0] = '\0';
1078 }
1079 else
1080 {
1081 m_DemolistSelectedIndex = 0;
1082 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1083 }
1084 }
1085 else
1086 {
1087 RefreshFilteredDemos();
1088
1089 // search for selected index
1090 m_DemolistSelectedIndex = -1;
1091 int SelectedIndex = -1;
1092 for(const auto &pItem : m_vpFilteredDemos)
1093 {
1094 SelectedIndex++;
1095 if(str_comp(a: m_aCurrentDemoSelectionName, b: pItem->m_aName) == 0)
1096 {
1097 m_DemolistSelectedIndex = SelectedIndex;
1098 break;
1099 }
1100 }
1101 }
1102
1103 if(m_DemolistSelectedIndex >= 0)
1104 m_DemolistSelectedReveal = true;
1105}
1106
1107bool CMenus::FetchHeader(CDemoItem &Item)
1108{
1109 if(!Item.m_InfosLoaded)
1110 {
1111 char aBuffer[IO_MAX_PATH_LENGTH];
1112 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename);
1113 IOHANDLE File;
1114 Item.m_Valid = DemoPlayer()->GetDemoInfo(pStorage: Storage(), pConsole: nullptr, pFilename: aBuffer, StorageType: Item.m_StorageType, pDemoHeader: &Item.m_Info, pTimelineMarkers: &Item.m_TimelineMarkers, pMapInfo: &Item.m_MapInfo, pFile: &File);
1115 Item.m_InfosLoaded = true;
1116
1117 if(Item.m_Valid && File)
1118 {
1119 Item.m_Size = io_length(io: File);
1120 io_close(io: File);
1121 }
1122 }
1123 return Item.m_Valid;
1124}
1125
1126void CMenus::FetchAllHeaders()
1127{
1128 for(auto &Item : m_vDemos)
1129 {
1130 FetchHeader(Item);
1131 }
1132 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1133}
1134
1135void CMenus::RenderDemoBrowser(CUIRect MainView)
1136{
1137 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_DEMOS);
1138
1139 CUIRect ListView, DetailsView, ButtonsView;
1140 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1141 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1142 MainView.HSplitBottom(Cut: 22.0f * 2.0f + 5.0f, pTop: &ListView, pBottom: &ButtonsView);
1143 ListView.VSplitRight(Cut: 205.0f, pLeft: &ListView, pRight: &DetailsView);
1144 ListView.VSplitRight(Cut: 5.0f, pLeft: &ListView, pRight: nullptr);
1145
1146 bool WasListboxItemActivated;
1147 RenderDemoBrowserList(ListView, WasListboxItemActivated);
1148 RenderDemoBrowserDetails(DetailsView);
1149 RenderDemoBrowserButtons(ButtonsView, WasListboxItemActivated);
1150}
1151
1152void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivated)
1153{
1154 if(!m_DemoBrowserListInitialized)
1155 {
1156 DemolistPopulate();
1157 DemolistOnUpdate(Reset: true);
1158 m_DemoBrowserListInitialized = true;
1159 }
1160
1161#if defined(CONF_VIDEORECORDER)
1162 if(!m_DemoRenderInput.IsEmpty())
1163 {
1164 if(DemoPlayer()->ErrorMessage()[0] == '\0')
1165 {
1166 m_Popup = POPUP_RENDER_DONE;
1167 }
1168 else
1169 {
1170 m_DemoRenderInput.Clear();
1171 }
1172 }
1173#endif
1174
1175 class CColumn
1176 {
1177 public:
1178 int m_Id;
1179 int m_Sort;
1180 const char *m_pCaption;
1181 int m_Direction;
1182 bool m_FontIcon;
1183 float m_Width;
1184 CUIRect m_Rect;
1185 const char *m_pTooltip;
1186 };
1187
1188 enum
1189 {
1190 COL_ICON = 0,
1191 COL_DEMONAME,
1192 COL_MARKERS,
1193 COL_LENGTH,
1194 COL_DATE,
1195 };
1196
1197 static CListBox s_ListBox;
1198 static CColumn s_aCols[] = {
1199 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1200 {.m_Id: COL_ICON, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: ms_ListheaderHeight, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1201 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1202 {.m_Id: COL_DEMONAME, .m_Sort: SORT_DEMONAME, .m_pCaption: Localizable(pStr: "Demo"), .m_Direction: 0, .m_FontIcon: false, .m_Width: 0.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1203 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1204 {.m_Id: COL_MARKERS, .m_Sort: SORT_MARKERS, .m_pCaption: FontIcon::BOOKMARK, .m_Direction: 1, .m_FontIcon: true, .m_Width: 30.0f, .m_Rect: {.x: 0}, .m_pTooltip: Localizable(pStr: "Markers")},
1205 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1206 {.m_Id: COL_LENGTH, .m_Sort: SORT_LENGTH, .m_pCaption: Localizable(pStr: "Length"), .m_Direction: 1, .m_FontIcon: false, .m_Width: 75.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1207 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1208 {.m_Id: COL_DATE, .m_Sort: SORT_DATE, .m_pCaption: Localizable(pStr: "Date"), .m_Direction: 1, .m_FontIcon: false, .m_Width: 150.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1209 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: s_ListBox.ScrollbarWidthMax(), .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1210 };
1211
1212 CUIRect Headers, ListBox;
1213 ListView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &ListBox);
1214 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1215 ListBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1216
1217 for(auto &Col : s_aCols)
1218 {
1219 if(Col.m_Direction == -1)
1220 {
1221 Headers.VSplitLeft(Cut: Col.m_Width, pLeft: &Col.m_Rect, pRight: &Headers);
1222 }
1223 }
1224
1225 for(int i = std::size(s_aCols) - 1; i >= 0; i--)
1226 {
1227 if(s_aCols[i].m_Direction == 1)
1228 {
1229 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
1230 }
1231 }
1232
1233 for(auto &Col : s_aCols)
1234 {
1235 if(Col.m_Direction == 0)
1236 Col.m_Rect = Headers;
1237 }
1238
1239 for(auto &Col : s_aCols)
1240 {
1241 if(Col.m_pCaption[0] != '\0' && Col.m_Sort != -1)
1242 {
1243 if(Col.m_FontIcon)
1244 {
1245 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1246 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1247 }
1248 const int ButtonPressed = DoButton_GridHeader(pId: &Col.m_Id, pText: Col.m_FontIcon ? Col.m_pCaption : Localize(pStr: Col.m_pCaption), Checked: g_Config.m_BrDemoSort == Col.m_Sort, pRect: &Col.m_Rect, Align: Col.m_FontIcon ? TEXTALIGN_MC : TEXTALIGN_ML);
1249 if(Col.m_pTooltip != nullptr)
1250 {
1251 GameClient()->m_Tooltips.DoToolTip(pId: &Col.m_Id, pNearRect: &Col.m_Rect, pText: Localize(pStr: Col.m_pTooltip));
1252 }
1253 if(Col.m_FontIcon)
1254 {
1255 TextRender()->SetRenderFlags(0);
1256 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1257 }
1258 if(ButtonPressed)
1259 {
1260 if(g_Config.m_BrDemoSort == Col.m_Sort)
1261 g_Config.m_BrDemoSortOrder ^= 1;
1262 else
1263 g_Config.m_BrDemoSortOrder = 0;
1264 g_Config.m_BrDemoSort = Col.m_Sort;
1265 // Don't rescan in order to keep fetched headers, just resort
1266 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1267 DemolistOnUpdate(Reset: false);
1268 }
1269 }
1270 }
1271
1272 if(m_DemolistSelectedReveal)
1273 {
1274 s_ListBox.ScrollToSelected();
1275 m_DemolistSelectedReveal = false;
1276 }
1277
1278 s_ListBox.DoStart(RowHeight: ms_ListheaderHeight, NumItems: m_vpFilteredDemos.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: m_DemolistSelectedIndex, pRect: &ListBox, Background: false, BackgroundCorners: IGraphics::CORNER_ALL, ForceShowScrollbar: true);
1279
1280 char aBuf[64];
1281 int ItemIndex = -1;
1282 for(auto &pItem : m_vpFilteredDemos)
1283 {
1284 ItemIndex++;
1285
1286 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: ItemIndex == m_DemolistSelectedIndex);
1287 if(!ListItem.m_Visible)
1288 continue;
1289
1290 for(const auto &Col : s_aCols)
1291 {
1292 CUIRect Button;
1293 Button.x = Col.m_Rect.x;
1294 Button.y = ListItem.m_Rect.y;
1295 Button.h = ListItem.m_Rect.h;
1296 Button.w = Col.m_Rect.w;
1297
1298 if(Col.m_Id == COL_ICON)
1299 {
1300 Button.Margin(Cut: 1.0f, pOtherRect: &Button);
1301
1302 const char *pIconType;
1303 if(pItem->m_IsLink || str_comp(a: pItem->m_aFilename, b: "..") == 0)
1304 pIconType = FontIcon::FOLDER_TREE;
1305 else if(pItem->m_IsDir)
1306 pIconType = FontIcon::FOLDER;
1307 else
1308 pIconType = FontIcon::FILM;
1309
1310 ColorRGBA IconColor;
1311 if(!pItem->m_IsDir && (!pItem->m_InfosLoaded || !pItem->m_Valid))
1312 IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded
1313 else
1314 IconColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
1315
1316 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1317 TextRender()->TextColor(Color: IconColor);
1318 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1319 Ui()->DoLabel(pRect: &Button, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
1320 TextRender()->SetRenderFlags(0);
1321 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1322 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1323 }
1324 else if(Col.m_Id == COL_DEMONAME)
1325 {
1326 SLabelProperties Props;
1327 Props.m_MaxWidth = Button.w;
1328 Props.m_EllipsisAtEnd = true;
1329 Props.m_EnableWidthCheck = false;
1330 Ui()->DoLabel(pRect: &Button, pText: pItem->m_aName, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1331 }
1332 else if(Col.m_Id == COL_MARKERS && !pItem->m_IsDir && pItem->m_Valid)
1333 {
1334 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1335 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1336 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1337 }
1338 else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid)
1339 {
1340 str_time(centisecs: (int64_t)pItem->Length() * 100, format: ETimeFormat::HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1341 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1342 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1343 }
1344 else if(Col.m_Id == COL_DATE && !pItem->m_IsDir)
1345 {
1346 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), format: TimestampFormat::SPACE);
1347 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1348 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1349 }
1350 }
1351 }
1352
1353 const int NewSelected = s_ListBox.DoEnd();
1354 if(NewSelected != m_DemolistSelectedIndex)
1355 {
1356 m_DemolistSelectedIndex = NewSelected;
1357 if(m_DemolistSelectedIndex >= 0)
1358 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1359 DemolistOnUpdate(Reset: false);
1360 }
1361
1362 WasListboxItemActivated = s_ListBox.WasItemActivated();
1363}
1364
1365void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView)
1366{
1367 CUIRect Contents, Header;
1368 DetailsView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &Contents);
1369 Contents.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1370 Contents.Margin(Cut: 5.0f, pOtherRect: &Contents);
1371
1372 const float FontSize = 12.0f;
1373 CDemoItem *pItem = m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex] : nullptr;
1374
1375 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1376 const char *pHeaderLabel;
1377 if(pItem == nullptr)
1378 pHeaderLabel = Localize(pStr: "No demo selected");
1379 else if(str_comp(a: pItem->m_aFilename, b: "..") == 0)
1380 pHeaderLabel = Localize(pStr: "Parent Folder");
1381 else if(pItem->m_IsLink)
1382 pHeaderLabel = Localize(pStr: "Folder Link");
1383 else if(pItem->m_IsDir)
1384 pHeaderLabel = Localize(pStr: "Folder");
1385 else if(!FetchHeader(Item&: *pItem))
1386 pHeaderLabel = Localize(pStr: "Invalid Demo");
1387 else
1388 pHeaderLabel = Localize(pStr: "Demo");
1389 Ui()->DoLabel(pRect: &Header, pText: pHeaderLabel, Size: FontSize + 2.0f, Align: TEXTALIGN_MC);
1390
1391 if(pItem == nullptr || pItem->m_IsDir)
1392 return;
1393
1394 char aBuf[256];
1395 CUIRect Left, Right;
1396
1397 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1398 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1399 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Created"), Size: FontSize, Align: TEXTALIGN_ML);
1400 if(pItem->m_Valid)
1401 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Size"), Size: FontSize, Align: TEXTALIGN_ML);
1402 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), format: TimestampFormat::SPACE);
1403 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1404 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1405 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1406
1407 if(!pItem->m_Valid)
1408 return;
1409
1410 const float DemoSize = pItem->m_Size / 1024.0f;
1411 if(DemoSize > 1024)
1412 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), DemoSize / 1024.0f);
1413 else
1414 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), DemoSize);
1415 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1416 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1417
1418 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1419 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1420 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Type"), Size: FontSize, Align: TEXTALIGN_ML);
1421 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1422 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1423 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1424 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aType, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1425 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->m_Info.m_Version);
1426 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1427 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1428
1429 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1430 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1431 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Length"), Size: FontSize, Align: TEXTALIGN_ML);
1432 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Markers"), Size: FontSize, Align: TEXTALIGN_ML);
1433 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1434 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1435 str_time(centisecs: (int64_t)pItem->Length() * 100, format: ETimeFormat::HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1436 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1437 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1438 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1439 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1440
1441 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1442 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Netversion"), Size: FontSize, Align: TEXTALIGN_ML);
1443 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1444 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aNetversion, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1445 Contents.HSplitTop(Cut: 16.0f, pTop: nullptr, pBottom: &Contents);
1446
1447 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1448 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map"), Size: FontSize, Align: TEXTALIGN_ML);
1449 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1450 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aMapName, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1451 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1452
1453 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1454 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map size"), Size: FontSize, Align: TEXTALIGN_ML);
1455 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1456 const float MapSize = pItem->MapSize() / 1024.0f;
1457 if(MapSize == 0.0f)
1458 str_copy(dst&: aBuf, src: Localize(pStr: "map not included", pContext: "Demo details"));
1459 else if(MapSize > 1024)
1460 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), MapSize / 1024.0f);
1461 else
1462 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), MapSize);
1463 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1464 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1465
1466 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1467 if(pItem->m_MapInfo.m_Sha256.has_value())
1468 {
1469 Ui()->DoLabel(pRect: &Left, pText: "SHA256", Size: FontSize, Align: TEXTALIGN_ML);
1470 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1471 char aSha[SHA256_MAXSTRSIZE];
1472 sha256_str(digest: pItem->m_MapInfo.m_Sha256.value(), str: aSha, max_len: sizeof(aSha));
1473 SLabelProperties Props;
1474 Props.m_MaxWidth = Left.w;
1475 Props.m_EllipsisAtEnd = true;
1476 Props.m_EnableWidthCheck = false;
1477 Ui()->DoLabel(pRect: &Left, pText: aSha, Size: FontSize - 1.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1478 }
1479 else
1480 {
1481 Ui()->DoLabel(pRect: &Left, pText: "CRC32", Size: FontSize, Align: TEXTALIGN_ML);
1482 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1483 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%08x", pItem->m_MapInfo.m_Crc);
1484 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1485 }
1486 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1487}
1488
1489void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemActivated)
1490{
1491 const auto &&SetIconMode = [&](bool Enable) {
1492 if(Enable)
1493 {
1494 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1495 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1496 }
1497 else
1498 {
1499 TextRender()->SetRenderFlags(0);
1500 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1501 }
1502 };
1503
1504 CUIRect ButtonBarTop, ButtonBarBottom;
1505 ButtonsView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ButtonsView);
1506 ButtonsView.HSplitMid(pTop: &ButtonBarTop, pBottom: &ButtonBarBottom, Spacing: 5.0f);
1507
1508 // quick search
1509 {
1510 CUIRect DemoSearch;
1511 ButtonBarTop.VSplitLeft(Cut: ButtonBarBottom.h * 21.0f, pLeft: &DemoSearch, pRight: &ButtonBarTop);
1512 ButtonBarTop.VSplitLeft(Cut: ButtonBarTop.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarTop);
1513 if(Ui()->DoEditBox_Search(pLineInput: &m_DemoSearchInput, pRect: &DemoSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
1514 {
1515 RefreshFilteredDemos();
1516 DemolistOnUpdate(Reset: false);
1517 }
1518 }
1519
1520 // refresh button
1521 {
1522 CUIRect RefreshButton;
1523 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 3.0f, pLeft: &RefreshButton, pRight: &ButtonBarBottom);
1524 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1525 SetIconMode(true);
1526 static CButtonContainer s_RefreshButton;
1527 if(DoButton_Menu(pButtonContainer: &s_RefreshButton, pText: FontIcon::ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
1528 {
1529 SetIconMode(false);
1530 DemolistPopulate();
1531 DemolistOnUpdate(Reset: false);
1532 }
1533 SetIconMode(false);
1534 GameClient()->m_Tooltips.DoToolTip(pId: &s_RefreshButton, pNearRect: &RefreshButton, pText: Localize(pStr: "Refresh the demo list"));
1535 }
1536
1537 // fetch info checkbox
1538 {
1539 CUIRect FetchInfo;
1540 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 7.0f, pLeft: &FetchInfo, pRight: &ButtonBarBottom);
1541 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1542 if(DoButton_CheckBox(pId: &g_Config.m_BrDemoFetchInfo, pText: Localize(pStr: "Fetch Info"), Checked: g_Config.m_BrDemoFetchInfo, pRect: &FetchInfo))
1543 {
1544 g_Config.m_BrDemoFetchInfo ^= 1;
1545 if(g_Config.m_BrDemoFetchInfo)
1546 FetchAllHeaders();
1547 }
1548 }
1549
1550 // demos directory button
1551 if(m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
1552 {
1553 CUIRect DemosDirectoryButton;
1554 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 10.0f, pLeft: &DemosDirectoryButton, pRight: &ButtonBarBottom);
1555 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1556 static CButtonContainer s_DemosDirectoryButton;
1557 if(DoButton_Menu(pButtonContainer: &s_DemosDirectoryButton, pText: Localize(pStr: "Demos directory"), Checked: 0, pRect: &DemosDirectoryButton))
1558 {
1559 char aBuf[IO_MAX_PATH_LENGTH];
1560 Storage()->GetCompletePath(Type: m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, pDir: m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1561 Client()->ViewFile(pFilename: aBuf);
1562 }
1563 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemosDirectoryButton, pNearRect: &DemosDirectoryButton, pText: Localize(pStr: "Open the directory that contains the demo files"));
1564 }
1565
1566 // play/open button
1567 if(m_DemolistSelectedIndex >= 0)
1568 {
1569 CUIRect PlayButton;
1570 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &PlayButton);
1571 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarBottom, pRight: nullptr);
1572 SetIconMode(true);
1573 static CButtonContainer s_PlayButton;
1574 const bool ActivateSelectedItem = DoButton_Menu(pButtonContainer: &s_PlayButton, pText: (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FontIcon::FOLDER_OPEN : FontIcon::PLAY, Checked: 0, pRect: &PlayButton) || WasListboxItemActivated ||
1575 Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) ||
1576 (Input()->KeyPress(Key: KEY_P) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive());
1577 SetIconMode(false);
1578 const char *pPlayTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Open the selected folder") : Localize(pStr: "Play the selected demo");
1579 GameClient()->m_Tooltips.DoToolTip(pId: &s_PlayButton, pNearRect: &PlayButton, pText: pPlayTooltip);
1580
1581 if(ActivateSelectedItem)
1582 {
1583 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
1584 {
1585 m_DemoSearchInput.Clear();
1586 const bool ParentFolder = str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") == 0;
1587 if(ParentFolder) // parent folder
1588 {
1589 str_copy(dst&: m_aCurrentDemoSelectionName, src: fs_filename(path: m_aCurrentDemoFolder));
1590 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1591 if(fs_parent_dir(path: m_aCurrentDemoFolder))
1592 {
1593 m_aCurrentDemoFolder[0] = '\0';
1594 if(m_DemolistStorageType == IStorage::TYPE_ALL)
1595 {
1596 m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item
1597 }
1598 else
1599 {
1600 Storage()->GetCompletePath(Type: m_DemolistStorageType, pDir: "demos", pBuffer: m_aCurrentDemoSelectionName, BufferSize: sizeof(m_aCurrentDemoSelectionName));
1601 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1602 }
1603 }
1604 }
1605 else // sub folder
1606 {
1607 if(m_aCurrentDemoFolder[0] != '\0')
1608 str_append(dst&: m_aCurrentDemoFolder, src: "/");
1609 else
1610 m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
1611 str_append(dst&: m_aCurrentDemoFolder, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1612 }
1613 DemolistPopulate();
1614 DemolistOnUpdate(Reset: !ParentFolder);
1615 }
1616 else // file
1617 {
1618 if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
1619 PopupConfirm(pTitle: Localize(pStr: "Disconnect"), pMessage: Localize(pStr: "Are you sure that you want to disconnect and play this demo?"), pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmPlayDemo);
1620 else
1621 CMenus::PopupConfirmPlayDemo();
1622 return;
1623 }
1624 }
1625 }
1626 // Check again if a demo is selected, because it is possible that no demo is selected when the
1627 // list is refreshed after navigating to the parent folder of a folder that has been deleted.
1628 if(m_DemolistSelectedIndex >= 0)
1629 {
1630 if(m_aCurrentDemoFolder[0] != '\0')
1631 {
1632 if(str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
1633 {
1634 // rename button
1635 CUIRect RenameButton;
1636 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &RenameButton);
1637 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1638 SetIconMode(true);
1639 static CButtonContainer s_RenameButton;
1640 if(DoButton_Menu(pButtonContainer: &s_RenameButton, pText: FontIcon::PENCIL, Checked: 0, pRect: &RenameButton))
1641 {
1642 SetIconMode(false);
1643 m_Popup = POPUP_RENAME_DEMO;
1644 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1645 {
1646 m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1647 }
1648 else
1649 {
1650 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1651 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1652 m_DemoRenameInput.Set(aNameWithoutExt);
1653 }
1654 Ui()->SetActiveItem(&m_DemoRenameInput);
1655 return;
1656 }
1657 const char *pRenameTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Rename folder") : Localize(pStr: "Rename demo");
1658 GameClient()->m_Tooltips.DoToolTip(pId: &s_RenameButton, pNearRect: &RenameButton, pText: pRenameTooltip);
1659
1660 // delete button
1661 static CButtonContainer s_DeleteButton;
1662 CUIRect DeleteButton;
1663 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &DeleteButton);
1664 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1665 if(DoButton_Menu(pButtonContainer: &s_DeleteButton, pText: FontIcon::TRASH, Checked: 0, pRect: &DeleteButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE) || (Input()->KeyPress(Key: KEY_D) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1666 {
1667 SetIconMode(false);
1668 char aBuf[128 + IO_MAX_PATH_LENGTH];
1669 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Are you sure that you want to delete the folder '%s'?") : Localize(pStr: "Are you sure that you want to delete the demo '%s'?"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1670 PopupConfirm(pTitle: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Delete folder") : Localize(pStr: "Delete demo"), pMessage: aBuf, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? &CMenus::PopupConfirmDeleteFolder : &CMenus::PopupConfirmDeleteDemo);
1671 return;
1672 }
1673 const char *pDeleteTooltip = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Delete folder") : Localize(pStr: "Delete demo");
1674 GameClient()->m_Tooltips.DoToolTip(pId: &s_DeleteButton, pNearRect: &DeleteButton, pText: pDeleteTooltip);
1675 SetIconMode(false);
1676 }
1677
1678#if defined(CONF_VIDEORECORDER)
1679 // render demo button
1680 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1681 {
1682 CUIRect RenderButton;
1683 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarTop, pRight: &RenderButton);
1684 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarTop, pRight: nullptr);
1685 SetIconMode(true);
1686 static CButtonContainer s_RenderButton;
1687 if(DoButton_Menu(pButtonContainer: &s_RenderButton, pText: FontIcon::VIDEO, Checked: 0, pRect: &RenderButton) || (Input()->KeyPress(Key: KEY_R) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1688 {
1689 SetIconMode(false);
1690 m_Popup = POPUP_RENDER_DEMO;
1691 m_StartPaused = false;
1692 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1693 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1694 m_DemoRenderInput.Set(aNameWithoutExt);
1695 Ui()->SetActiveItem(&m_DemoRenderInput);
1696 return;
1697 }
1698 SetIconMode(false);
1699 GameClient()->m_Tooltips.DoToolTip(pId: &s_RenderButton, pNearRect: &RenderButton, pText: Localize(pStr: "Render demo"));
1700 }
1701#endif
1702 }
1703 }
1704}
1705
1706void CMenus::PopupConfirmPlayDemo()
1707{
1708 char aBuf[IO_MAX_PATH_LENGTH];
1709 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1710 const char *pError = Client()->DemoPlayer_Play(pFilename: aBuf, StorageType: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType);
1711 m_LastPauseChange = -1.0f;
1712 m_LastSpeedChange = -1.0f;
1713 if(pError)
1714 {
1715 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
1716 }
1717 else
1718 {
1719 Ui()->SetActiveItem(nullptr);
1720 return;
1721 }
1722}
1723
1724void CMenus::PopupConfirmDeleteDemo()
1725{
1726 char aBuf[IO_MAX_PATH_LENGTH];
1727 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1728 if(Storage()->RemoveFile(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1729 {
1730 DemolistPopulate();
1731 DemolistOnUpdate(Reset: false);
1732 }
1733 else
1734 {
1735 char aError[128 + IO_MAX_PATH_LENGTH];
1736 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the demo '%s'"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1737 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1738 }
1739}
1740
1741void CMenus::PopupConfirmDeleteFolder()
1742{
1743 char aBuf[IO_MAX_PATH_LENGTH];
1744 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1745 if(Storage()->RemoveFolder(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1746 {
1747 DemolistPopulate();
1748 DemolistOnUpdate(Reset: false);
1749 }
1750 else
1751 {
1752 char aError[128 + IO_MAX_PATH_LENGTH];
1753 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the folder '%s'. Make sure it's empty first."), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1754 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1755 }
1756}
1757
1758void CMenus::ConchainDemoPlay(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1759{
1760 CMenus *pThis = static_cast<CMenus *>(pUserData);
1761 pThis->m_LastPauseChange = pThis->Client()->GlobalTime();
1762 pfnCallback(pResult, pCallbackUserData);
1763}
1764
1765void CMenus::ConchainDemoSpeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1766{
1767 CMenus *pThis = static_cast<CMenus *>(pUserData);
1768 if(pResult->NumArguments() == 1)
1769 {
1770 pThis->m_LastSpeedChange = pThis->Client()->GlobalTime();
1771 }
1772 pfnCallback(pResult, pCallbackUserData);
1773}
1774