/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "maplayers.h" #include "menus.h" #include using namespace FontIcons; using namespace std::chrono_literals; int CMenus::DoButton_DemoPlayer(const void *pID, const char *pText, int Checked, const CUIRect *pRect) { pRect->Draw(ColorRGBA(1, 1, 1, (Checked ? 0.10f : 0.5f) * UI()->ButtonColorMul(pID)), IGraphics::CORNER_ALL, 5.0f); UI()->DoLabel(pRect, pText, 14.0f, TEXTALIGN_MC); return UI()->DoButtonLogic(pID, Checked, pRect); } int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, bool Enabled) { pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * UI()->ButtonColorMul(pButtonContainer)), Corners, 5.0f); TextRender()->SetCurFont(TextRender()->GetFont(TEXT_FONT_ICON_FONT)); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); TextRender()->TextColor(TextRender()->DefaultTextColor()); CUIRect Temp; pRect->HMargin(2.0f, &Temp); UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); if(!Enabled) { TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); TextRender()->TextOutlineColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f)); UI()->DoLabel(&Temp, FONT_ICON_SLASH, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); TextRender()->TextColor(TextRender()->DefaultTextColor()); } TextRender()->SetCurFont(nullptr); return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); } bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser) { bool DoFilterChat = *(bool *)pUser; if(!DoFilterChat) { return false; } CUnpacker Unpacker; Unpacker.Reset(pData, Size); int Msg = Unpacker.GetInt(); int Sys = Msg & 1; Msg >>= 1; return !Unpacker.Error() && !Sys && Msg == NETMSGTYPE_SV_CHAT; } void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek) { if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f) { m_pClient->m_Chat.Reset(); m_pClient->m_KillMessages.OnReset(); m_pClient->m_Particles.OnReset(); m_pClient->m_Sounds.OnReset(); m_pClient->m_Scoreboard.OnReset(); m_pClient->m_Statboard.OnReset(); m_pClient->m_SuppressEvents = true; if(TimeToSeek != 0.0f) DemoPlayer()->SeekTime(TimeToSeek); else DemoPlayer()->SeekPercent(PositionToSeek); m_pClient->m_SuppressEvents = false; m_pClient->m_MapLayersBackGround.EnvelopeUpdate(); m_pClient->m_MapLayersForeGround.EnvelopeUpdate(); } } void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset) { m_pClient->m_SuppressEvents = true; DemoPlayer()->SeekTick(TickOffset); m_pClient->m_SuppressEvents = false; DemoPlayer()->Pause(); m_pClient->m_MapLayersBackGround.EnvelopeUpdate(); m_pClient->m_MapLayersForeGround.EnvelopeUpdate(); } void CMenus::RenderDemoPlayer(CUIRect MainView) { const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); static int64_t s_LastSpeedChange = 0; // handle keyboard shortcuts independent of active menu float PositionToSeek = -1.0f; float TimeToSeek = 0.0f; if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts) { // increase/decrease speed if(!Input()->ShiftIsPressed()) { if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP) || Input()->KeyPress(KEY_UP)) { DemoPlayer()->SetSpeedIndex(+1); s_LastSpeedChange = time_get(); } else if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN) || Input()->KeyPress(KEY_DOWN)) { DemoPlayer()->SetSpeedIndex(-1); s_LastSpeedChange = time_get(); } } // pause/unpause if(Input()->KeyPress(KEY_SPACE) || Input()->KeyPress(KEY_RETURN) || Input()->KeyPress(KEY_K)) { if(pInfo->m_Paused) { DemoPlayer()->Unpause(); } else { DemoPlayer()->Pause(); } } // seek backward/forward 10/5 seconds if(Input()->KeyPress(KEY_J)) { TimeToSeek = -10.0f; } else if(Input()->KeyPress(KEY_L)) { TimeToSeek = 10.0f; } else if(Input()->KeyPress(KEY_LEFT)) { TimeToSeek = -5.0f; } else if(Input()->KeyPress(KEY_RIGHT)) { TimeToSeek = 5.0f; } // seek to 0-90% const int aSeekPercentKeys[] = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9}; for(unsigned i = 0; i < std::size(aSeekPercentKeys); i++) { if(Input()->KeyPress(aSeekPercentKeys[i])) { PositionToSeek = i * 0.1f; break; } } // seek to the beginning/end if(Input()->KeyPress(KEY_HOME)) { PositionToSeek = 0.0f; } else if(Input()->KeyPress(KEY_END)) { PositionToSeek = 1.0f; } // Advance single frame forward/backward with period/comma key const bool TickForwards = Input()->KeyPress(KEY_PERIOD); const bool TickBackwards = Input()->KeyPress(KEY_COMMA); if(TickForwards || TickBackwards) { DemoSeekTick(TickForwards ? IDemoPlayer::TICK_NEXT : IDemoPlayer::TICK_PREVIOUS); } } const float SeekBarHeight = 15.0f; const float ButtonbarHeight = 20.0f; const float NameBarHeight = 20.0f; const float Margins = 5.0f; const float TotalHeight = SeekBarHeight + ButtonbarHeight + NameBarHeight + Margins * 3; // render speed info if(g_Config.m_ClDemoShowSpeed && time_get() - s_LastSpeedChange < time_freq() * 1) { CUIRect Screen = *UI()->Screen(); char aSpeedBuf[256]; str_format(aSpeedBuf, sizeof(aSpeedBuf), "×%.2f", pInfo->m_Speed); TextRender()->Text(120.0f, Screen.y + Screen.h - 120.0f - TotalHeight, 60.0f, aSpeedBuf, -1.0f); } const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick; const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick; if(CurrentTick == TotalTicks) { DemoPlayer()->Pause(); PositionToSeek = 0.0f; } if(!m_MenuActive) { HandleDemoSeeking(PositionToSeek, TimeToSeek); return; } CUIRect DemoControls; MainView.HSplitBottom(TotalHeight, nullptr, &DemoControls); DemoControls.VSplitLeft(50.0f, nullptr, &DemoControls); DemoControls.VSplitLeft(600.0f, &DemoControls, nullptr); DemoControls.Draw(ms_ColorTabbarActive, IGraphics::CORNER_T, 10.0f); CUIRect SeekBar, ButtonBar, NameBar, SpeedBar; DemoControls.Margin(5.0f, &DemoControls); DemoControls.HSplitTop(SeekBarHeight, &SeekBar, &ButtonBar); ButtonBar.HSplitTop(Margins, nullptr, &ButtonBar); ButtonBar.HSplitBottom(NameBarHeight, &ButtonBar, &NameBar); NameBar.HSplitTop(4.0f, nullptr, &NameBar); // do seekbar { const float Rounding = 5.0f; static int s_SeekBarID = 0; void *pId = &s_SeekBarID; char aBuffer[128]; // draw seek bar SeekBar.Draw(ColorRGBA(0, 0, 0, 0.5f), IGraphics::CORNER_ALL, Rounding); // draw filled bar float Amount = CurrentTick / (float)TotalTicks; CUIRect FilledBar = SeekBar; FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount; FilledBar.Draw(ColorRGBA(1, 1, 1, 0.5f), IGraphics::CORNER_ALL, Rounding); // draw highlighting if(g_Config.m_ClDemoSliceBegin != -1 && g_Config.m_ClDemoSliceEnd != -1) { float RatioBegin = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks; float RatioEnd = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks; float Span = ((SeekBar.w - 2 * Rounding) * RatioEnd) - ((SeekBar.w - 2 * Rounding) * RatioBegin); Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 0.0f, 0.0f, 0.25f); IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * RatioBegin, SeekBar.y, Span, SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } // draw markers for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++) { float Ratio = (pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / (float)TotalTicks; Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } // draw slice markers // begin if(g_Config.m_ClDemoSliceBegin != -1) { float Ratio = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks; Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } // end if(g_Config.m_ClDemoSliceEnd != -1) { float Ratio = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks; Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } // draw time char aCurrentTime[32]; str_time((int64_t)CurrentTick / SERVER_TICK_SPEED * 100, TIME_HOURS, aCurrentTime, sizeof(aCurrentTime)); char aTotalTime[32]; str_time((int64_t)TotalTicks / SERVER_TICK_SPEED * 100, TIME_HOURS, aTotalTime, sizeof(aTotalTime)); str_format(aBuffer, sizeof(aBuffer), "%s / %s", aCurrentTime, aTotalTime); UI()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC); // do the logic const bool Inside = UI()->MouseInside(&SeekBar); if(UI()->CheckActiveItem(pId)) { if(!UI()->MouseButton(0)) UI()->SetActiveItem(nullptr); else { static float s_PrevAmount = 0.0f; float AmountSeek = clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f); if(Input()->ShiftIsPressed()) { AmountSeek = s_PrevAmount + (AmountSeek - s_PrevAmount) * 0.05f; if(AmountSeek > 0.0f && AmountSeek < 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.0001f) { PositionToSeek = AmountSeek; } } else { if(AmountSeek > 0.0f && AmountSeek < 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.001f) { s_PrevAmount = AmountSeek; PositionToSeek = AmountSeek; } } } } else if(UI()->HotItem() == pId) { if(UI()->MouseButton(0)) { UI()->SetActiveItem(pId); } else { const int HoveredTick = (int)(clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f) * TotalTicks); static char s_aHoveredTime[32]; str_time((int64_t)HoveredTick / SERVER_TICK_SPEED * 100, TIME_HOURS, s_aHoveredTime, sizeof(s_aHoveredTime)); GameClient()->m_Tooltips.DoToolTip(pId, &SeekBar, s_aHoveredTime); } } if(Inside) UI()->SetHotItem(pId); } bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false; // do buttons CUIRect Button; // combined play and pause button ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_PlayPauseButton; if(DoButton_FontIcon(&s_PlayPauseButton, pInfo->m_Paused ? FONT_ICON_PLAY : FONT_ICON_PAUSE, false, &Button, IGraphics::CORNER_ALL)) { if(pInfo->m_Paused) { DemoPlayer()->Unpause(); } else { DemoPlayer()->Pause(); } } GameClient()->m_Tooltips.DoToolTip(&s_PlayPauseButton, &Button, pInfo->m_Paused ? Localize("Play the current demo") : Localize("Pause the current demo")); // stop button ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_ResetButton; if(DoButton_FontIcon(&s_ResetButton, FONT_ICON_STOP, false, &Button, IGraphics::CORNER_ALL)) { DemoPlayer()->Pause(); PositionToSeek = 0.0f; } GameClient()->m_Tooltips.DoToolTip(&s_ResetButton, &Button, Localize("Stop the current demo")); // one tick back ButtonBar.VSplitLeft(Margins + 10.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_OneTickBackButton; if(DoButton_FontIcon(&s_OneTickBackButton, FONT_ICON_CHEVRON_LEFT, 0, &Button, IGraphics::CORNER_ALL)) DemoSeekTick(IDemoPlayer::TICK_PREVIOUS); GameClient()->m_Tooltips.DoToolTip(&s_OneTickBackButton, &Button, Localize("Go back one tick")); // one tick forward ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_OneTickForwardButton; if(DoButton_FontIcon(&s_OneTickForwardButton, FONT_ICON_CHEVRON_RIGHT, 0, &Button, IGraphics::CORNER_ALL)) DemoSeekTick(IDemoPlayer::TICK_NEXT); GameClient()->m_Tooltips.DoToolTip(&s_OneTickForwardButton, &Button, Localize("Go forward one tick")); // slowdown ButtonBar.VSplitLeft(Margins + 10.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_SlowDownButton; if(DoButton_FontIcon(&s_SlowDownButton, FONT_ICON_BACKWARD, 0, &Button, IGraphics::CORNER_ALL)) DecreaseDemoSpeed = true; GameClient()->m_Tooltips.DoToolTip(&s_SlowDownButton, &Button, Localize("Slow down the demo")); // fastforward ButtonBar.VSplitLeft(Margins, 0, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_FastForwardButton; if(DoButton_FontIcon(&s_FastForwardButton, FONT_ICON_FORWARD, 0, &Button, IGraphics::CORNER_ALL)) IncreaseDemoSpeed = true; GameClient()->m_Tooltips.DoToolTip(&s_FastForwardButton, &Button, Localize("Speed up the demo")); // speed meter ButtonBar.VSplitLeft(Margins * 12, &SpeedBar, &ButtonBar); char aBuffer[64]; str_format(aBuffer, sizeof(aBuffer), "×%g", pInfo->m_Speed); UI()->DoLabel(&SpeedBar, aBuffer, Button.h * 0.7f, TEXTALIGN_MC); // slice begin button ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_SliceBeginButton; if(DoButton_FontIcon(&s_SliceBeginButton, FONT_ICON_RIGHT_FROM_BRACKET, 0, &Button, IGraphics::CORNER_ALL)) { Client()->DemoSliceBegin(); if(CurrentTick > (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick)) g_Config.m_ClDemoSliceEnd = -1; } GameClient()->m_Tooltips.DoToolTip(&s_SliceBeginButton, &Button, Localize("Mark the beginning of a cut")); // slice end button ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_SliceEndButton; if(DoButton_FontIcon(&s_SliceEndButton, FONT_ICON_RIGHT_TO_BRACKET, 0, &Button, IGraphics::CORNER_ALL)) { Client()->DemoSliceEnd(); if(CurrentTick < (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick)) g_Config.m_ClDemoSliceBegin = -1; } GameClient()->m_Tooltips.DoToolTip(&s_SliceEndButton, &Button, Localize("Mark the end of a cut")); // slice save button ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_SliceSaveButton; if(DoButton_FontIcon(&s_SliceSaveButton, FONT_ICON_ARROW_UP_RIGHT_FROM_SQUARE, 0, &Button, IGraphics::CORNER_ALL)) { char aDemoName[IO_MAX_PATH_LENGTH]; DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName)); m_DemoSliceInput.Set(aDemoName); m_DemoSliceInput.Append(".demo"); UI()->SetActiveItem(&m_DemoSliceInput); m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE; } GameClient()->m_Tooltips.DoToolTip(&s_SliceSaveButton, &Button, Localize("Export cut as a separate demo")); // threshold value, accounts for slight inaccuracy when setting demo position const int Threshold = 10; // one marker back ButtonBar.VSplitLeft(Margins + 20.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_OneMarkerBackButton; if(DoButton_FontIcon(&s_OneMarkerBackButton, FONT_ICON_BACKWARD_STEP, 0, &Button, IGraphics::CORNER_ALL)) for(int i = pInfo->m_NumTimelineMarkers - 1; i >= 0; i--) { if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) < CurrentTick && absolute(((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold) { PositionToSeek = (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks; break; } PositionToSeek = 0.0f; } GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerBackButton, &Button, Localize("Go back one marker")); // one marker forward ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar); ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); static CButtonContainer s_OneMarkerForwardButton; if(DoButton_FontIcon(&s_OneMarkerForwardButton, FONT_ICON_FORWARD_STEP, 0, &Button, IGraphics::CORNER_ALL)) for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++) { if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) > CurrentTick && absolute(((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold) { PositionToSeek = (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks; break; } PositionToSeek = 1.0f; } GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerForwardButton, &Button, Localize("Go forward one marker")); // close button ButtonBar.VSplitRight(ButtonbarHeight * 3, &ButtonBar, &Button); static int s_ExitButton = 0; if(DoButton_DemoPlayer(&s_ExitButton, Localize("Close"), 0, &Button) || (Input()->KeyPress(KEY_C) && m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE)) { Client()->Disconnect(); DemolistOnUpdate(false); } // toggle keyboard shortcuts button ButtonBar.VSplitRight(Margins, &ButtonBar, nullptr); ButtonBar.VSplitRight(ButtonbarHeight, &ButtonBar, &Button); static CButtonContainer s_KeyboardShortcutsButton; if(DoButton_FontIcon(&s_KeyboardShortcutsButton, FONT_ICON_KEYBOARD, 0, &Button, IGraphics::CORNER_ALL, g_Config.m_ClDemoKeyboardShortcuts != 0)) { g_Config.m_ClDemoKeyboardShortcuts ^= 1; } GameClient()->m_Tooltips.DoToolTip(&s_KeyboardShortcutsButton, &Button, Localize("Toggle keyboard shortcuts")); // demo name char aDemoName[IO_MAX_PATH_LENGTH]; DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName)); char aBuf[IO_MAX_PATH_LENGTH + 128]; str_format(aBuf, sizeof(aBuf), Localize("Demofile: %s"), aDemoName); CTextCursor Cursor; TextRender()->SetCursor(&Cursor, NameBar.x, NameBar.y + (NameBar.h - (Button.h * 0.5f)) / 2.f, Button.h * 0.5f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); Cursor.m_LineWidth = DemoControls.w; TextRender()->TextEx(&Cursor, aBuf, -1); if(IncreaseDemoSpeed) { DemoPlayer()->SetSpeedIndex(+1); s_LastSpeedChange = time_get(); } else if(DecreaseDemoSpeed) { DemoPlayer()->SetSpeedIndex(-1); s_LastSpeedChange = time_get(); } HandleDemoSeeking(PositionToSeek, TimeToSeek); // render popups if(m_DemoPlayerState != DEMOPLAYER_NONE) { // prevent element under the active popup from being activated UI()->SetHotItem(nullptr); } if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE) { RenderDemoPlayerSliceSavePopup(MainView); } UI()->RenderPopupMenus(); } void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) { const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); CUIRect Box; MainView.Margin(150.0f, &Box); // background Box.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); Box.Margin(24.0f, &Box); // title CUIRect Title; Box.HSplitTop(24.0f, &Title, &Box); Box.HSplitTop(20.0f, nullptr, &Box); UI()->DoLabel(&Title, Localize("Export demo cut"), 24.0f, TEXTALIGN_MC); // slice times CUIRect SliceTimesBar, SliceInterval, SliceLength; Box.HSplitTop(24.0f, &SliceTimesBar, &Box); SliceTimesBar.VSplitMid(&SliceInterval, &SliceLength, 40.0f); Box.HSplitTop(20.0f, nullptr, &Box); const int64_t RealSliceBegin = g_Config.m_ClDemoSliceBegin == -1 ? 0 : (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick); const int64_t RealSliceEnd = (g_Config.m_ClDemoSliceEnd == -1 ? pInfo->m_LastTick : g_Config.m_ClDemoSliceEnd) - pInfo->m_FirstTick; char aSliceBegin[32]; str_time(RealSliceBegin / SERVER_TICK_SPEED * 100, TIME_HOURS, aSliceBegin, sizeof(aSliceBegin)); char aSliceEnd[32]; str_time(RealSliceEnd / SERVER_TICK_SPEED * 100, TIME_HOURS, aSliceEnd, sizeof(aSliceEnd)); char aSliceLength[32]; str_time((RealSliceEnd - RealSliceBegin) / SERVER_TICK_SPEED * 100, TIME_HOURS, aSliceLength, sizeof(aSliceLength)); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "%s: %s – %s", Localize("Cut interval"), aSliceBegin, aSliceEnd); UI()->DoLabel(&SliceInterval, aBuf, 18.0f, TEXTALIGN_ML); str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Cut length"), aSliceLength); UI()->DoLabel(&SliceLength, aBuf, 18.0f, TEXTALIGN_ML); // file name CUIRect NameLabel, NameBox; Box.HSplitTop(24.0f, &NameLabel, &Box); Box.HSplitTop(20.0f, nullptr, &Box); NameLabel.VSplitLeft(150.0f, &NameLabel, &NameBox); NameBox.VSplitLeft(20.0f, nullptr, &NameBox); UI()->DoLabel(&NameLabel, Localize("New name:"), 18.0f, TEXTALIGN_ML); UI()->DoEditBox(&m_DemoSliceInput, &NameBox, 12.0f); // remove chat checkbox static int s_RemoveChat = 0; CUIRect RemoveChatCheckBox; Box.HSplitTop(24.0f, &RemoveChatCheckBox, &Box); Box.HSplitTop(20.0f, nullptr, &Box); if(DoButton_CheckBox(&s_RemoveChat, Localize("Remove chat"), s_RemoveChat, &RemoveChatCheckBox)) { s_RemoveChat ^= 1; } // buttons CUIRect ButtonBar, AbortButton, OkButton; Box.HSplitBottom(24.0f, &Box, &ButtonBar); ButtonBar.VSplitMid(&AbortButton, &OkButton, 40.0f); static CButtonContainer s_ButtonAbort; if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &AbortButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE))) m_DemoPlayerState = DEMOPLAYER_NONE; static CUI::SConfirmPopupContext s_ConfirmPopupContext; static CButtonContainer s_ButtonOk; if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &OkButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) { char aDemoName[IO_MAX_PATH_LENGTH]; DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName)); str_append(aDemoName, ".demo", sizeof(aDemoName)); if(!str_endswith(m_DemoSliceInput.GetString(), ".demo")) m_DemoSliceInput.Append(".demo"); if(str_comp(aDemoName, m_DemoSliceInput.GetString()) == 0) { static CUI::SMessagePopupContext s_MessagePopupContext; s_MessagePopupContext.ErrorColor(); str_copy(s_MessagePopupContext.m_aMessage, Localize("Please use a different name")); UI()->ShowPopupMessage(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_MessagePopupContext); } else { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "%s/%s", m_aCurrentDemoFolder, m_DemoSliceInput.GetString()); if(Storage()->FileExists(aPath, IStorage::TYPE_SAVE)) { s_ConfirmPopupContext.Reset(); s_ConfirmPopupContext.YesNoButtons(); str_copy(s_ConfirmPopupContext.m_aMessage, Localize("File already exists, do you want to overwrite it?")); UI()->ShowPopupConfirm(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_ConfirmPopupContext); } else s_ConfirmPopupContext.m_Result = CUI::SConfirmPopupContext::CONFIRMED; } } if(s_ConfirmPopupContext.m_Result == CUI::SConfirmPopupContext::CONFIRMED) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "%s/%s", m_aCurrentDemoFolder, m_DemoSliceInput.GetString()); m_DemoPlayerState = DEMOPLAYER_NONE; Client()->DemoSlice(aPath, CMenus::DemoFilterChat, &s_RemoveChat); DemolistPopulate(); DemolistOnUpdate(false); } if(s_ConfirmPopupContext.m_Result != CUI::SConfirmPopupContext::UNSET) { s_ConfirmPopupContext.Reset(); } } int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser) { CMenus *pSelf = (CMenus *)pUser; if(str_comp(pInfo->m_pName, ".") == 0 || (str_comp(pInfo->m_pName, "..") == 0 && str_comp(pSelf->m_aCurrentDemoFolder, "demos") == 0) || (!IsDir && !str_endswith(pInfo->m_pName, ".demo"))) { return 0; } CDemoItem Item; str_copy(Item.m_aFilename, pInfo->m_pName); if(IsDir) { str_format(Item.m_aName, sizeof(Item.m_aName), "%s/", pInfo->m_pName); Item.m_InfosLoaded = false; Item.m_Valid = false; Item.m_Date = 0; } else { str_truncate(Item.m_aName, sizeof(Item.m_aName), pInfo->m_pName, str_length(pInfo->m_pName) - str_length(".demo")); Item.m_InfosLoaded = false; Item.m_Date = pInfo->m_TimeModified; } Item.m_IsDir = IsDir != 0; Item.m_StorageType = StorageType; pSelf->m_vDemos.push_back(Item); if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms) { pSelf->GameClient()->m_Menus.RenderLoading(Localize("Loading demo files"), "", 0, false); } return 0; } void CMenus::DemolistPopulate() { m_vDemos.clear(); if(!str_comp(m_aCurrentDemoFolder, "demos")) m_DemolistStorageType = IStorage::TYPE_ALL; m_DemoPopulateStartTime = time_get_nanoseconds(); Storage()->ListDirectoryInfo(m_DemolistStorageType, m_aCurrentDemoFolder, DemolistFetchCallback, this); if(g_Config.m_BrDemoFetchInfo) FetchAllHeaders(); std::stable_sort(m_vDemos.begin(), m_vDemos.end()); } void CMenus::DemolistOnUpdate(bool Reset) { if(Reset) g_Config.m_UiDemoSelected[0] = '\0'; else { bool Found = false; int SelectedIndex = -1; // search for selected index for(auto &Item : m_vDemos) { SelectedIndex++; if(str_comp(g_Config.m_UiDemoSelected, Item.m_aName) == 0) { Found = true; break; } } if(Found) m_DemolistSelectedIndex = SelectedIndex; } m_DemolistSelectedIndex = Reset ? !m_vDemos.empty() ? 0 : -1 : m_DemolistSelectedIndex >= (int)m_vDemos.size() ? m_vDemos.size() - 1 : m_DemolistSelectedIndex; m_DemolistSelectedIsDir = m_DemolistSelectedIndex < 0 ? false : m_vDemos[m_DemolistSelectedIndex].m_IsDir; } bool CMenus::FetchHeader(CDemoItem &Item) { if(!Item.m_InfosLoaded) { char aBuffer[IO_MAX_PATH_LENGTH]; str_format(aBuffer, sizeof(aBuffer), "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename); Item.m_Valid = DemoPlayer()->GetDemoInfo(Storage(), aBuffer, Item.m_StorageType, &Item.m_Info, &Item.m_TimelineMarkers, &Item.m_MapInfo); Item.m_InfosLoaded = true; } return Item.m_Valid; } void CMenus::FetchAllHeaders() { for(auto &Item : m_vDemos) { FetchHeader(Item); } std::stable_sort(m_vDemos.begin(), m_vDemos.end()); } void CMenus::RenderDemoList(CUIRect MainView) { static int s_Inited = 0; if(!s_Inited) { DemolistPopulate(); DemolistOnUpdate(true); s_Inited = 1; } char aFooterLabel[128] = {0}; if(m_DemolistSelectedIndex >= 0) { CDemoItem &Item = m_vDemos[m_DemolistSelectedIndex]; if(str_comp(Item.m_aFilename, "..") == 0) str_copy(aFooterLabel, Localize("Parent Folder")); else if(m_DemolistSelectedIsDir) str_copy(aFooterLabel, Localize("Folder")); else if(!FetchHeader(Item)) str_copy(aFooterLabel, Localize("Invalid Demo")); else str_copy(aFooterLabel, Localize("Demo details")); } // render background MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); MainView.Margin(10.0f, &MainView); #if defined(CONF_VIDEORECORDER) CUIRect RenderRect; #endif CUIRect ButtonBar, RefreshRect, FetchRect, PlayRect, DeleteRect, RenameRect, LabelRect, ListBox; CUIRect ButtonBar2, DirectoryButton; MainView.HSplitBottom((ms_ButtonHeight + 5.0f) * 2.0f, &MainView, &ButtonBar2); ButtonBar2.HSplitTop(5.0f, 0, &ButtonBar2); ButtonBar2.HSplitTop(ms_ButtonHeight, &ButtonBar2, &ButtonBar); ButtonBar.HSplitTop(5.0f, 0, &ButtonBar); ButtonBar2.VSplitLeft(110.0f, &FetchRect, &ButtonBar2); ButtonBar2.VSplitLeft(10.0f, 0, &ButtonBar2); ButtonBar2.VSplitLeft(230.0f, &DirectoryButton, &ButtonBar2); ButtonBar2.VSplitLeft(10.0f, 0, &ButtonBar2); ButtonBar.VSplitRight(110.0f, &ButtonBar, &PlayRect); ButtonBar.VSplitLeft(110.0f, &RefreshRect, &ButtonBar); ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(110.0f, &DeleteRect, &ButtonBar); ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(110.0f, &RenameRect, &ButtonBar); ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar); #if defined(CONF_VIDEORECORDER) ButtonBar2.VSplitRight(110.0f, &ButtonBar2, &RenderRect); ButtonBar2.VSplitRight(10.0f, &ButtonBar2, 0); #endif ButtonBar.VSplitLeft(110.0f, &LabelRect, &ButtonBar); MainView.HSplitBottom(140.0f, &ListBox, &MainView); // render demo info MainView.VMargin(5.0f, &MainView); MainView.HSplitBottom(5.0f, &MainView, 0); MainView.Draw(ColorRGBA(0, 0, 0, 0.15f), IGraphics::CORNER_B, 4.0f); if(!m_DemolistSelectedIsDir && m_DemolistSelectedIndex >= 0 && m_vDemos[m_DemolistSelectedIndex].m_Valid) { CUIRect Left, Right, Labels; MainView.VMargin(20.0f, &MainView); MainView.HMargin(10.0f, &MainView); MainView.VSplitMid(&Labels, &MainView); // left side Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Created:"), 14.0f, TEXTALIGN_ML); char aTimestamp[256]; str_timestamp_ex(m_vDemos[m_DemolistSelectedIndex].m_Date, aTimestamp, sizeof(aTimestamp), FORMAT_SPACE); UI()->DoLabel(&Right, aTimestamp, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Type:"), 14.0f, TEXTALIGN_ML); UI()->DoLabel(&Right, m_vDemos[m_DemolistSelectedIndex].m_Info.m_aType, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Length:"), 14.0f, TEXTALIGN_ML); int Length = m_vDemos[m_DemolistSelectedIndex].Length(); char aBuf[64]; str_time((int64_t)Length * 100, TIME_HOURS, aBuf, sizeof(aBuf)); UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Version:"), 14.0f, TEXTALIGN_ML); str_format(aBuf, sizeof(aBuf), "%d", m_vDemos[m_DemolistSelectedIndex].m_Info.m_Version); UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Markers:"), 14.0f, TEXTALIGN_ML); str_format(aBuf, sizeof(aBuf), "%d", m_vDemos[m_DemolistSelectedIndex].NumMarkers()); UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML); // right side Labels = MainView; Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Map:"), 14.0f, TEXTALIGN_ML); UI()->DoLabel(&Right, m_vDemos[m_DemolistSelectedIndex].m_Info.m_aMapName, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Size:"), 14.0f, TEXTALIGN_ML); const float Size = m_vDemos[m_DemolistSelectedIndex].Size() / 1024.0f; if(Size > 1024) str_format(aBuf, sizeof(aBuf), Localize("%.2f MiB"), Size / 1024.0f); else str_format(aBuf, sizeof(aBuf), Localize("%.2f KiB"), Size); UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML); Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); if(m_vDemos[m_DemolistSelectedIndex].m_MapInfo.m_Sha256 != SHA256_ZEROED) { UI()->DoLabel(&Left, "SHA256:", 14.0f, TEXTALIGN_ML); char aSha[SHA256_MAXSTRSIZE]; sha256_str(m_vDemos[m_DemolistSelectedIndex].m_MapInfo.m_Sha256, aSha, sizeof(aSha) / 2); UI()->DoLabel(&Right, aSha, Right.w > 235 ? 14.0f : 11.0f, TEXTALIGN_ML); } else { UI()->DoLabel(&Left, Localize("Crc:"), 14.0f, TEXTALIGN_ML); str_format(aBuf, sizeof(aBuf), "%08x", m_vDemos[m_DemolistSelectedIndex].m_MapInfo.m_Crc); UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML); } Labels.HSplitTop(5.0f, 0, &Labels); Labels.HSplitTop(20.0f, &Left, &Labels); Left.VSplitLeft(150.0f, &Left, &Right); UI()->DoLabel(&Left, Localize("Netversion:"), 14.0f, TEXTALIGN_ML); UI()->DoLabel(&Right, m_vDemos[m_DemolistSelectedIndex].m_Info.m_aNetversion, 14.0f, TEXTALIGN_ML); } // demo list CUIRect Headers; ListBox.HSplitTop(ms_ListheaderHeight, &Headers, &ListBox); struct CColumn { int m_ID; int m_Sort; CLocConstString m_Caption; int m_Direction; float m_Width; CUIRect m_Rect; CUIRect m_Spacer; }; enum { COL_DEMONAME = 0, COL_MARKERS, COL_LENGTH, COL_DATE, }; static CColumn s_aCols[] = { {COL_DEMONAME, SORT_DEMONAME, Localizable("Demo"), 0, 0.0f, {0}, {0}}, {COL_MARKERS, SORT_MARKERS, Localizable("Markers"), 1, 75.0f, {0}, {0}}, {COL_LENGTH, SORT_LENGTH, Localizable("Length"), 1, 75.0f, {0}, {0}}, {COL_DATE, SORT_DATE, Localizable("Date"), 1, 160.0f, {0}, {0}}, }; Headers.Draw(ColorRGBA(0.0f, 0, 0, 0.15f), 0, 0); int NumCols = std::size(s_aCols); // do layout for(int i = 0; i < NumCols; i++) { if(s_aCols[i].m_Direction == -1) { Headers.VSplitLeft(s_aCols[i].m_Width, &s_aCols[i].m_Rect, &Headers); if(i + 1 < NumCols) { //Cols[i].flags |= SPACER; Headers.VSplitLeft(2, &s_aCols[i].m_Spacer, &Headers); } } } for(int i = NumCols - 1; i >= 0; i--) { if(s_aCols[i].m_Direction == 1) { Headers.VSplitRight(s_aCols[i].m_Width, &Headers, &s_aCols[i].m_Rect); Headers.VSplitRight(2, &Headers, &s_aCols[i].m_Spacer); } } for(int i = 0; i < NumCols; i++) { if(s_aCols[i].m_Direction == 0) s_aCols[i].m_Rect = Headers; } // do headers for(int i = 0; i < NumCols; i++) { if(DoButton_GridHeader(s_aCols[i].m_Caption, Localize(s_aCols[i].m_Caption), g_Config.m_BrDemoSort == s_aCols[i].m_Sort, &s_aCols[i].m_Rect)) { if(s_aCols[i].m_Sort != -1) { if(g_Config.m_BrDemoSort == s_aCols[i].m_Sort) g_Config.m_BrDemoSortOrder ^= 1; else g_Config.m_BrDemoSortOrder = 0; g_Config.m_BrDemoSort = s_aCols[i].m_Sort; } // Don't rescan in order to keep fetched headers, just resort std::stable_sort(m_vDemos.begin(), m_vDemos.end()); DemolistOnUpdate(false); } } static CListBox s_ListBox; s_ListBox.DoStart(ms_ListheaderHeight, m_vDemos.size(), 1, 3, m_DemolistSelectedIndex, &ListBox, false); int ItemIndex = -1; for(auto &Item : m_vDemos) { ItemIndex++; const CListboxItem ListItem = s_ListBox.DoNextItem(&Item, ItemIndex == m_DemolistSelectedIndex); if(!ListItem.m_Visible) continue; CUIRect Row = ListItem.m_Rect; CUIRect FileIcon; Row.VSplitLeft(Row.h, &FileIcon, &Row); Row.VSplitLeft(5.0f, 0, &Row); FileIcon.Margin(1.0f, &FileIcon); FileIcon.x += 2.0f; const char *pIconType; if(str_comp(Item.m_aFilename, "..") == 0) pIconType = FONT_ICON_FOLDER_TREE; else if(Item.m_IsDir) pIconType = FONT_ICON_FOLDER; else pIconType = FONT_ICON_FILM; ColorRGBA IconColor(1.0f, 1.0f, 1.0f, 1.0f); if(!Item.m_IsDir && (!Item.m_InfosLoaded || !Item.m_Valid)) IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded TextRender()->SetCurFont(TextRender()->GetFont(TEXT_FONT_ICON_FONT)); TextRender()->TextColor(IconColor); UI()->DoLabel(&FileIcon, pIconType, 12.0f, TEXTALIGN_ML); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->SetCurFont(nullptr); for(int c = 0; c < NumCols; c++) { CUIRect Button; Button.x = s_aCols[c].m_Rect.x; Button.y = Row.y; Button.h = Row.h; Button.w = s_aCols[c].m_Rect.w; int ID = s_aCols[c].m_ID; if(ID == COL_DEMONAME) { Button.x += FileIcon.w + 6.0f; CTextCursor Cursor; TextRender()->SetCursor(&Cursor, Button.x, Button.y + (Button.h - 12.0f) / 2.f, 12.0f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); Cursor.m_LineWidth = Button.w; TextRender()->TextEx(&Cursor, Item.m_aName, -1); } else if(ID == COL_MARKERS && !Item.m_IsDir && Item.m_InfosLoaded) { char aBuf[3]; str_format(aBuf, sizeof(aBuf), "%d", Item.NumMarkers()); Button.VMargin(4.0f, &Button); UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); } else if(ID == COL_LENGTH && !Item.m_IsDir && Item.m_InfosLoaded) { int Length = Item.Length(); char aBuf[32]; str_time((int64_t)Length * 100, TIME_HOURS, aBuf, sizeof(aBuf)); Button.VMargin(4.0f, &Button); UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); } else if(ID == COL_DATE && !Item.m_IsDir) { char aBuf[64]; str_timestamp_ex(Item.m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE); Button.VSplitRight(24.0f, &Button, 0); UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); } } } const int NewSelected = s_ListBox.DoEnd(); if(NewSelected != m_DemolistSelectedIndex) { m_DemolistSelectedIndex = NewSelected; if(m_DemolistSelectedIndex >= 0) str_copy(g_Config.m_UiDemoSelected, m_vDemos[m_DemolistSelectedIndex].m_aName); DemolistOnUpdate(false); } static CButtonContainer s_RefreshButton; if(DoButton_Menu(&s_RefreshButton, Localize("Refresh"), 0, &RefreshRect) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) { DemolistPopulate(); DemolistOnUpdate(false); } if(DoButton_CheckBox(&g_Config.m_BrDemoFetchInfo, Localize("Fetch Info"), g_Config.m_BrDemoFetchInfo, &FetchRect)) { g_Config.m_BrDemoFetchInfo ^= 1; if(g_Config.m_BrDemoFetchInfo) FetchAllHeaders(); } static CButtonContainer s_PlayButton; if(DoButton_Menu(&s_PlayButton, m_DemolistSelectedIsDir ? Localize("Open") : Localize("Play", "Demo browser"), 0, &PlayRect) || s_ListBox.WasItemActivated() || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE)) { if(m_DemolistSelectedIndex >= 0) { if(m_DemolistSelectedIsDir) // folder { if(str_comp(m_vDemos[m_DemolistSelectedIndex].m_aFilename, "..") == 0) // parent folder fs_parent_dir(m_aCurrentDemoFolder); else // sub folder { str_append(m_aCurrentDemoFolder, "/", sizeof(m_aCurrentDemoFolder)); str_append(m_aCurrentDemoFolder, m_vDemos[m_DemolistSelectedIndex].m_aFilename, sizeof(m_aCurrentDemoFolder)); m_DemolistStorageType = m_vDemos[m_DemolistSelectedIndex].m_StorageType; } DemolistPopulate(); DemolistOnUpdate(true); } else // file { char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "%s/%s", m_aCurrentDemoFolder, m_vDemos[m_DemolistSelectedIndex].m_aFilename); const char *pError = Client()->DemoPlayer_Play(aBuf, m_vDemos[m_DemolistSelectedIndex].m_StorageType); if(pError) PopupMessage(Localize("Error"), str_comp(pError, "error loading demo") ? pError : Localize("Error loading demo"), Localize("Ok")); else { UI()->SetActiveItem(nullptr); return; } } } } static CButtonContainer s_DirectoryButtonID; if(DoButton_Menu(&s_DirectoryButtonID, Localize("Demos directory"), 0, &DirectoryButton)) { char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(IStorage::TYPE_SAVE, "demos", aBuf, sizeof(aBuf)); Storage()->CreateFolder("demos", IStorage::TYPE_SAVE); if(!open_file(aBuf)) { dbg_msg("menus", "couldn't open file '%s'", aBuf); } } GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButtonID, &DirectoryButton, Localize("Open the directory that contains the demo files")); if(!m_DemolistSelectedIsDir) { static CButtonContainer s_DeleteButton; if(DoButton_Menu(&s_DeleteButton, Localize("Delete"), 0, &DeleteRect) || UI()->ConsumeHotkey(CUI::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed())) { if(m_DemolistSelectedIndex >= 0) { char aBuf[128 + IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), Localize("Are you sure that you want to delete the demo '%s'?"), m_vDemos[m_DemolistSelectedIndex].m_aFilename); PopupConfirm(Localize("Delete demo"), aBuf, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDeleteDemo); return; } } static CButtonContainer s_RenameButton; if(DoButton_Menu(&s_RenameButton, Localize("Rename"), 0, &RenameRect)) { if(m_DemolistSelectedIndex >= 0) { m_Popup = POPUP_RENAME_DEMO; m_DemoRenameInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); UI()->SetActiveItem(&m_DemoRenameInput); return; } } #if defined(CONF_VIDEORECORDER) static CButtonContainer s_RenderButton; if(DoButton_Menu(&s_RenderButton, Localize("Render"), 0, &RenderRect) || (Input()->KeyPress(KEY_R) && m_pClient->m_GameConsole.IsClosed())) { if(m_DemolistSelectedIndex >= 0) { m_Popup = POPUP_RENDER_DEMO; m_DemoRenderInput.Set(m_vDemos[m_DemolistSelectedIndex].m_aFilename); UI()->SetActiveItem(&m_DemoRenderInput); return; } } #endif } UI()->DoLabel(&LabelRect, aFooterLabel, 14.0f, TEXTALIGN_ML); } void CMenus::PopupConfirmDeleteDemo() { char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "%s/%s", m_aCurrentDemoFolder, m_vDemos[m_DemolistSelectedIndex].m_aFilename); if(Storage()->RemoveFile(aBuf, m_vDemos[m_DemolistSelectedIndex].m_StorageType)) { DemolistPopulate(); DemolistOnUpdate(false); } else { char aError[128 + IO_MAX_PATH_LENGTH]; str_format(aError, sizeof(aError), Localize("Unable to delete the demo '%s'"), m_vDemos[m_DemolistSelectedIndex].m_aFilename); PopupMessage(Localize("Error"), aError, Localize("Ok")); } }