diff --git a/CMakeLists.txt b/CMakeLists.txt
index 597d57eaf..3b1f8006a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2823,6 +2823,8 @@ if(CLIENT)
components/tclient/tclient.h
components/tclient/trails.cpp
components/tclient/trails.h
+ components/tclient/hud_watch.cpp
+ components/tclient/hud_watch.h
components/bestclient/3d_particles.cpp
components/bestclient/3d_particles.h
components/bestclient/admin_panel.cpp
diff --git a/src/engine/shared/config_variables_tclient.h b/src/engine/shared/config_variables_tclient.h
index dfc8c4c0c..0c46e3a47 100644
--- a/src/engine/shared/config_variables_tclient.h
+++ b/src/engine/shared/config_variables_tclient.h
@@ -290,4 +290,8 @@ MACRO_CONFIG_INT(TcUiCompactList, tc_ui_compact_list, 0, 0, 1, CFGFLAG_CLIENT |
// Dummy Info
MACRO_CONFIG_INT(TcShowhudDummyPosition, tc_showhud_dummy_position, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Dummy Position)")
MACRO_CONFIG_INT(TcShowhudDummySpeed, tc_showhud_dummy_speed, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Dummy Speed)")
+
+// Hud Watch - config variable watcher displayed on screen
+MACRO_CONFIG_INT(TcHudWatchEnable, tc_hud_watch_enable, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable HUD watch overlay")
+MACRO_CONFIG_STR(TcHudWatchData, tc_hud_watch_data, 2048, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Serialized HUD watch data (var|label|hexcolor;...)")
MACRO_CONFIG_INT(TcShowhudDummyAngle, tc_showhud_dummy_angle, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Dummy Aim Angle)")
diff --git a/src/game/client/components/bestclient/hud_editor.cpp b/src/game/client/components/bestclient/hud_editor.cpp
index 42f7efbdb..b4b448718 100644
--- a/src/game/client/components/bestclient/hud_editor.cpp
+++ b/src/game/client/components/bestclient/hud_editor.cpp
@@ -224,7 +224,8 @@ namespace
Module == HudLayout::MODULE_FINISH_PREDICTION ||
Module == HudLayout::MODULE_VOICE_TALKERS ||
Module == HudLayout::MODULE_VOICE_STATUS ||
- Module == HudLayout::MODULE_MUSIC_PLAYER;
+ Module == HudLayout::MODULE_MUSIC_PLAYER ||
+ Module == HudLayout::MODULE_HUD_WATCH;
}
bool PointInRect(vec2 Point, const CUIRect &Rect)
@@ -572,6 +573,10 @@ CHudEditor::SModuleVisual CHudEditor::GetModuleVisual(HudLayout::EModule Module)
Visual.m_Rect = GameClient()->m_VoiceChat.GetHudMuteStatusIndicatorRect(Width, Height, true);
Visual.m_Rounding = 2.3f;
break;
+ case HudLayout::MODULE_HUD_WATCH:
+ Visual.m_Rect = GameClient()->m_HudWatch.GetHudEditorRect();
+ Visual.m_Rounding = 4.0f;
+ break;
default:
Visual.m_Rect = GetFallbackModuleRect(Module);
Visual.m_Rounding = 4.0f;
@@ -619,6 +624,7 @@ void CHudEditor::CollectModuleVisuals(SModuleVisual *pOut, int &Count) const
AddModule(HudLayout::MODULE_MUSIC_PLAYER);
AddModule(HudLayout::MODULE_VOICE_TALKERS);
AddModule(HudLayout::MODULE_VOICE_STATUS);
+ AddModule(HudLayout::MODULE_HUD_WATCH);
}
HudLayout::EModule CHudEditor::HitTestModule(vec2 MousePos) const
diff --git a/src/game/client/components/hud_layout.cpp b/src/game/client/components/hud_layout.cpp
index 12657fd8a..e67384a70 100644
--- a/src/game/client/components/hud_layout.cpp
+++ b/src/game/client/components/hud_layout.cpp
@@ -48,6 +48,7 @@ namespace HudLayout
// when the keyboard preset changes.
{84.5f, 152.0f, 100, 0, true, false, 0x66000000U}, // KEYSTROKES_MOUSE
{484.0f, 172.0f, 100, 0, true, true, 0x66000000U}, // DUMMY_ACTIONS
+ {5.0f, 60.0f, 100, 0, true, false, 0x66000000U}, // HUD_WATCH
};
static const char *gs_apModuleNames[MODULE_COUNT] = {
@@ -73,6 +74,7 @@ namespace HudLayout
"Keyboard",
"Mouse",
"Dummy Actions",
+ "Config Watch",
};
static SModuleLayout gs_aRuntimeModuleLayouts[MODULE_COUNT];
diff --git a/src/game/client/components/hud_layout.h b/src/game/client/components/hud_layout.h
index 115420e9c..bfb41c0e7 100644
--- a/src/game/client/components/hud_layout.h
+++ b/src/game/client/components/hud_layout.h
@@ -39,6 +39,7 @@ namespace HudLayout
MODULE_KEYSTROKES_KEYBOARD,
MODULE_KEYSTROKES_MOUSE,
MODULE_DUMMY_ACTIONS,
+ MODULE_HUD_WATCH,
MODULE_COUNT,
};
diff --git a/src/game/client/components/tclient/hud_watch.cpp b/src/game/client/components/tclient/hud_watch.cpp
new file mode 100644
index 000000000..3206f4341
--- /dev/null
+++ b/src/game/client/components/tclient/hud_watch.cpp
@@ -0,0 +1,338 @@
+#include "hud_watch.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+
+CHudWatch::CHudWatch() :
+ m_Dirty(false)
+{
+}
+
+void CHudWatch::OnInit()
+{
+ LoadFromConfig();
+}
+
+void CHudWatch::LoadFromConfig()
+{
+ m_vWatches.clear();
+ const char *pData = g_Config.m_TcHudWatchData;
+ if(!pData || pData[0] == '\0')
+ return;
+
+ char aData[2048];
+ str_copy(aData, pData);
+ char *pEntry = aData;
+ while(pEntry && pEntry[0])
+ {
+ const char *pSemi_const = str_find(pEntry, ";");
+ char *pSemi = const_cast(pSemi_const);
+ if(pSemi)
+ *pSemi = '\0';
+
+ char *pParts[3] = {};
+ int NumParts = 0;
+ char *pTok = pEntry;
+ for(int i = 0; i < 3; i++)
+ {
+ const char *pPipe_const = str_find(pTok, "|");
+ char *pPipe = const_cast(pPipe_const);
+ if(pPipe)
+ {
+ *pPipe = '\0';
+ pParts[i] = pTok;
+ pTok = pPipe + 1;
+ NumParts++;
+ }
+ else
+ {
+ pParts[i] = pTok;
+ NumParts++;
+ break;
+ }
+ }
+ if(NumParts >= 1 && pParts[0] && pParts[0][0])
+ {
+ const char *pVarName = pParts[0];
+ const char *pLabel = (NumParts >= 2 && pParts[1]) ? pParts[1] : pParts[0];
+ ColorRGBA Color(1.0f, 1.0f, 1.0f, 1.0f);
+ std::unordered_map ValueColors;
+ if(NumParts >= 3 && pParts[2] && pParts[2][0])
+ {
+ const char *pColorPart = pParts[2];
+ const char *pComma = str_find(pColorPart, ",");
+ if(pComma)
+ {
+ char aDefColor[16];
+ str_copy(aDefColor, pColorPart, (int)(pComma - pColorPart) + 1);
+ unsigned char aDefHex[4] = {};
+ if(str_hex_decode(aDefHex, sizeof(aDefHex), aDefColor) == 0)
+ {
+ Color.r = aDefHex[0] / 255.0f;
+ Color.g = aDefHex[1] / 255.0f;
+ Color.b = aDefHex[2] / 255.0f;
+ Color.a = aDefHex[3] / 255.0f;
+ }
+ const char *pCond = pComma + 1;
+ while(pCond && pCond[0])
+ {
+ const char *pComma2 = str_find(pCond, ",");
+ char aCond[64];
+ if(pComma2)
+ {
+ str_copy(aCond, pCond, (int)(pComma2 - pCond) + 1);
+ pCond = pComma2 + 1;
+ }
+ else
+ {
+ str_copy(aCond, pCond);
+ pCond = nullptr;
+ }
+ const char *pColon = str_find(aCond, ":");
+ if(pColon)
+ {
+ int Val = str_toint(aCond);
+ unsigned char aHex[4] = {};
+ if(str_hex_decode(aHex, sizeof(aHex), pColon + 1) == 0)
+ {
+ ValueColors[Val] = ColorRGBA(
+ aHex[0] / 255.0f,
+ aHex[1] / 255.0f,
+ aHex[2] / 255.0f,
+ aHex[3] / 255.0f);
+ }
+ }
+ }
+ }
+ else
+ {
+ unsigned char aHex[4] = {};
+ if(str_hex_decode(aHex, sizeof(aHex), pColorPart) == 0)
+ {
+ Color.r = aHex[0] / 255.0f;
+ Color.g = aHex[1] / 255.0f;
+ Color.b = aHex[2] / 255.0f;
+ Color.a = aHex[3] / 255.0f;
+ }
+ }
+ }
+
+ struct SFindData
+ {
+ const char *m_pName;
+ const SConfigVariable *m_pResult;
+ };
+ SFindData FindData = {pVarName, nullptr};
+ auto Finder = [](const SConfigVariable *pVar, void *pUserData) {
+ auto *pData = static_cast(pUserData);
+ if(str_comp(pVar->m_pScriptName, pData->m_pName) == 0)
+ pData->m_pResult = pVar;
+ };
+ GameClient()->ConfigManager()->PossibleConfigVariables("", CFGFLAG_CLIENT, Finder, &FindData);
+ if(FindData.m_pResult)
+ {
+ SHudWatchItem Item;
+ Item.m_pVar = FindData.m_pResult;
+ str_copy(Item.m_aLabel, pLabel);
+ Item.m_Color = Color;
+ Item.m_vValueColors = std::move(ValueColors);
+ m_vWatches.push_back(Item);
+ }
+ }
+
+ if(pSemi)
+ pEntry = pSemi + 1;
+ else
+ break;
+ }
+}
+
+void CHudWatch::SaveToConfig()
+{
+ char aData[2048] = "";
+ int Offset = 0;
+ for(size_t i = 0; i < m_vWatches.size(); i++)
+ {
+ if(Offset > 0 && Offset < (int)sizeof(aData) - 2)
+ {
+ aData[Offset] = ';';
+ Offset++;
+ }
+ const SHudWatchItem &Item = m_vWatches[i];
+ char aColorBuf[512];
+ int ColorOff = str_format(aColorBuf, sizeof(aColorBuf), "%02X%02X%02X%02X",
+ (int)(Item.m_Color.r * 255),
+ (int)(Item.m_Color.g * 255),
+ (int)(Item.m_Color.b * 255),
+ (int)(Item.m_Color.a * 255));
+ for(const auto &[Val, Col] : Item.m_vValueColors)
+ {
+ ColorOff += str_format(aColorBuf + ColorOff, (int)sizeof(aColorBuf) - ColorOff, ",%d:%02X%02X%02X%02X",
+ Val,
+ (int)(Col.r * 255),
+ (int)(Col.g * 255),
+ (int)(Col.b * 255),
+ (int)(Col.a * 255));
+ }
+ int Written = str_format(aData + Offset, (int)sizeof(aData) - Offset, "%s|%s|%s",
+ Item.m_pVar->m_pScriptName,
+ Item.m_aLabel,
+ aColorBuf);
+ if(Written > 0)
+ Offset += Written;
+ }
+ str_copy(g_Config.m_TcHudWatchData, aData);
+ GameClient()->ConfigManager()->Save();
+}
+
+void CHudWatch::OnRender()
+{
+ if(!g_Config.m_TcHudWatchEnable || m_vWatches.empty())
+ return;
+
+ float Width = 300.0f * Graphics()->ScreenAspect();
+ float Height = 300.0f;
+ Graphics()->MapScreen(0.0f, 0.0f, Width, Height);
+
+ const auto Layout = HudLayout::Get(HudLayout::MODULE_HUD_WATCH, Width, Height);
+ const float Scale = std::clamp(Layout.m_Scale / 100.0f, 0.25f, 3.0f);
+ const float FontSize = 6.0f * Scale;
+
+ float X = Layout.m_X;
+ float Y = Layout.m_Y;
+ float LineHeight = FontSize + 2.0f * Scale;
+
+ for(const SHudWatchItem &Item : m_vWatches)
+ {
+ ITextRender *pTextRender = TextRender();
+
+ int CurrentValue = 0;
+ char aValue[256] = "";
+ if(Item.m_pVar->m_Type == SConfigVariable::VAR_INT)
+ {
+ const SIntConfigVariable *pInt = static_cast(Item.m_pVar);
+ CurrentValue = *pInt->m_pVariable;
+ str_format(aValue, sizeof(aValue), "%d", CurrentValue);
+ }
+ else if(Item.m_pVar->m_Type == SConfigVariable::VAR_STRING)
+ {
+ const SStringConfigVariable *pStr = static_cast(Item.m_pVar);
+ str_copy(aValue, pStr->m_pStr);
+ }
+ else if(Item.m_pVar->m_Type == SConfigVariable::VAR_COLOR)
+ {
+ const SColorConfigVariable *pCol = static_cast(Item.m_pVar);
+ unsigned ColVal = *pCol->m_pVariable;
+ if(pCol->m_Alpha)
+ str_format(aValue, sizeof(aValue), "#%08X", ColVal);
+ else
+ str_format(aValue, sizeof(aValue), "#%06X", ColVal & 0xFFFFFF);
+ }
+
+ auto it = Item.m_vValueColors.find(CurrentValue);
+ if(!Item.m_vValueColors.empty() && it == Item.m_vValueColors.end())
+ {
+ Y += LineHeight;
+ continue;
+ }
+ ColorRGBA UseColor = (it != Item.m_vValueColors.end()) ? it->second : Item.m_Color;
+
+ char aBuf[512];
+ str_format(aBuf, sizeof(aBuf), "%s: %s", Item.m_aLabel, aValue);
+
+ pTextRender->TextColor(UseColor);
+ pTextRender->Text(X, Y, FontSize, aBuf, -1.0f);
+
+ Y += LineHeight;
+ }
+
+ TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f);
+}
+
+CUIRect CHudWatch::GetHudEditorRect(bool ForcePreview) const
+{
+ if(m_vWatches.empty() && !ForcePreview)
+ return {0.0f, 0.0f, 0.0f, 0.0f};
+
+ float Width = 300.0f * Graphics()->ScreenAspect();
+ float Height = 300.0f;
+
+ const auto Layout = HudLayout::Get(HudLayout::MODULE_HUD_WATCH, Width, Height);
+ const float Scale = std::clamp(Layout.m_Scale / 100.0f, 0.25f, 3.0f);
+ const float FontSize = 6.0f * Scale;
+ const float LineHeight = FontSize + 2.0f * Scale;
+
+ size_t NumItems = m_vWatches.empty() ? 1 : m_vWatches.size();
+ float MaxWidth = 80.0f * Scale;
+ for(size_t i = 0; i < m_vWatches.size(); i++)
+ {
+ char aBuf[128];
+ str_format(aBuf, sizeof(aBuf), "%s: 000", m_vWatches[i].m_aLabel);
+ float TextWidth = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f);
+ if(TextWidth > MaxWidth)
+ MaxWidth = TextWidth;
+ }
+
+ CUIRect Rect;
+ Rect.x = Layout.m_X;
+ Rect.y = Layout.m_Y;
+ Rect.w = MaxWidth + 4.0f;
+ Rect.h = NumItems * LineHeight + 2.0f;
+
+ Rect.x = std::clamp(Rect.x, 0.0f, std::max(0.0f, Width - Rect.w));
+ Rect.y = std::clamp(Rect.y, 0.0f, std::max(0.0f, Height - Rect.h));
+ return Rect;
+}
+
+bool CHudWatch::IsWatched(const SConfigVariable *pVar) const
+{
+ for(const auto &Item : m_vWatches)
+ if(Item.m_pVar == pVar)
+ return true;
+ return false;
+}
+
+void CHudWatch::SetWatch(const SConfigVariable *pVar, const char *pLabel, ColorRGBA Color, std::unordered_map ValueColors)
+{
+ for(auto &Item : m_vWatches)
+ {
+ if(Item.m_pVar == pVar)
+ {
+ str_copy(Item.m_aLabel, pLabel);
+ Item.m_Color = Color;
+ Item.m_vValueColors = std::move(ValueColors);
+ SaveToConfig();
+ return;
+ }
+ }
+ SHudWatchItem Item;
+ Item.m_pVar = pVar;
+ str_copy(Item.m_aLabel, pLabel);
+ Item.m_Color = Color;
+ Item.m_vValueColors = std::move(ValueColors);
+ m_vWatches.push_back(Item);
+ SaveToConfig();
+}
+
+void CHudWatch::RemoveWatch(const SConfigVariable *pVar)
+{
+ for(auto it = m_vWatches.begin(); it != m_vWatches.end(); ++it)
+ {
+ if(it->m_pVar == pVar)
+ {
+ m_vWatches.erase(it);
+ SaveToConfig();
+ return;
+ }
+ }
+}
diff --git a/src/game/client/components/tclient/hud_watch.h b/src/game/client/components/tclient/hud_watch.h
new file mode 100644
index 000000000..9f7ef22d0
--- /dev/null
+++ b/src/game/client/components/tclient/hud_watch.h
@@ -0,0 +1,45 @@
+#ifndef GAME_CLIENT_COMPONENTS_TCLIENT_HUD_WATCH_H
+#define GAME_CLIENT_COMPONENTS_TCLIENT_HUD_WATCH_H
+
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+
+struct SHudWatchItem
+{
+ const SConfigVariable *m_pVar;
+ char m_aLabel[64];
+ ColorRGBA m_Color;
+ std::unordered_map m_vValueColors;
+};
+
+class CHudWatch : public CComponent
+{
+ std::vector m_vWatches;
+ bool m_Dirty;
+
+ void LoadFromConfig();
+ void SaveToConfig();
+
+public:
+ CHudWatch();
+ int Sizeof() const override { return sizeof(*this); }
+ void OnInit() override;
+ void OnRender() override;
+ void OnMessage(int MsgType, void *pRawMsg) override {}
+
+ bool IsWatched(const SConfigVariable *pVar) const;
+ void SetWatch(const SConfigVariable *pVar, const char *pLabel, ColorRGBA Color, std::unordered_map ValueColors = {});
+ void RemoveWatch(const SConfigVariable *pVar);
+
+ const std::vector &GetWatches() const { return m_vWatches; }
+
+ CUIRect GetHudEditorRect(bool ForcePreview = false) const;
+};
+
+#endif
diff --git a/src/game/client/components/tclient/menus_tclient.cpp b/src/game/client/components/tclient/menus_tclient.cpp
index 422d1f59a..9b55422c3 100644
--- a/src/game/client/components/tclient/menus_tclient.cpp
+++ b/src/game/client/components/tclient/menus_tclient.cpp
@@ -5,6 +5,7 @@
#include
#include
+#include
#include
#include
#include
@@ -34,6 +35,7 @@
#include
#include
+#include
#include
#include
#include
@@ -2607,6 +2609,157 @@ void CMenus::RenderSettingsTClientProfiles(CUIRect MainView)
s_SelectedProfile = s_ListBox.DoEnd();
}
+struct SWatchEditPopupContext : public SPopupMenuId
+{
+ CGameClient *m_pGameClient;
+ CUi *m_pUI;
+ const SConfigVariable *m_pVar;
+ CLineInputBuffered<64> m_LabelInput;
+ unsigned m_ColorValue;
+ ColorHSLA m_DisplayHsla;
+ CUi::SColorPickerPopupContext m_ColorPickerContext;
+ bool m_NeedsReopen{false};
+
+ struct SCondition
+ {
+ CLineInputNumber m_ValueInput;
+ unsigned m_ColorValue{0xFFFFFFFF};
+ ColorHSLA m_DisplayHsla{0.0f, 0.0f, 1.0f, 1.0f};
+ CUi::SColorPickerPopupContext m_ColorPickerContext;
+ CButtonContainer m_RemoveBtnId;
+ };
+ std::list m_vConditions;
+};
+
+static CUi::EPopupMenuFunctionResult WatchEditPopupFunc(void *pContext, CUIRect View, bool Active)
+{
+ SWatchEditPopupContext *pCtx = static_cast(pContext);
+ CGameClient *pGameClient = pCtx->m_pGameClient;
+ CUi *pUI = pCtx->m_pUI;
+
+ const float RowHeight = 16.0f;
+ const float Margin = 3.0f;
+ const float Padding = 5.0f;
+
+ // Label row
+ CUIRect LabelRow;
+ View.HSplitTop(Padding, nullptr, &View);
+ View.HSplitTop(RowHeight, &LabelRow, &View);
+ LabelRow.VSplitLeft(35.0f, nullptr, &LabelRow);
+ pUI->DoLabel(&LabelRow, "Имя:", RowHeight * 0.8f, TEXTALIGN_ML);
+
+ CUIRect LabelInput;
+ LabelRow.VSplitRight(0.0f, &LabelRow, &LabelInput);
+ LabelRow.VSplitLeft(45.0f, nullptr, &LabelInput);
+ pUI->DoEditBox(&pCtx->m_LabelInput, &LabelInput, 12.0f);
+
+ // Value→Color conditions header
+ CUIRect CondHeader;
+ View.HSplitTop(Margin + 1.0f, nullptr, &View);
+ View.HSplitTop(RowHeight, &CondHeader, &View);
+ pUI->DoLabel(&CondHeader, "Цвета значений:", RowHeight * 0.8f, TEXTALIGN_ML);
+
+ // Condition rows
+ for(auto it = pCtx->m_vConditions.begin(); it != pCtx->m_vConditions.end(); )
+ {
+ auto &Cond = *it;
+
+ CUIRect CondRow, ValInput, ColSquare, RemoveBtn;
+ View.HSplitTop(Margin, nullptr, &View);
+ View.HSplitTop(RowHeight, &CondRow, &View);
+
+ CondRow.VSplitLeft(40.0f, &ValInput, &CondRow);
+ pUI->DoEditBox(&Cond.m_ValueInput, &ValInput, 10.0f);
+
+ CondRow.VSplitLeft(Margin, nullptr, &CondRow);
+ CondRow.VSplitLeft(22.0f, &ColSquare, &CondRow);
+ ColSquare.h = RowHeight - 2.0f;
+ ColSquare.y += 1.0f;
+ {
+ ColorRGBA Outline(1.0f, 1.0f, 1.0f, 0.25f);
+ ColSquare.Draw(Outline, IGraphics::CORNER_ALL, 3.0f);
+ CUIRect Inner;
+ ColSquare.Margin(1.5f, &Inner);
+ ColorRGBA ColPrev = color_cast(Cond.m_DisplayHsla);
+ Inner.Draw(ColPrev, IGraphics::CORNER_ALL, 2.0f);
+ }
+
+ if(Active && pUI->DoButtonLogic(&Cond.m_ColorValue, 0, &ColSquare, BUTTONFLAG_LEFT))
+ {
+ Cond.m_ColorPickerContext.m_pHslaColor = &Cond.m_ColorValue;
+ Cond.m_ColorPickerContext.m_HslaColor = Cond.m_DisplayHsla;
+ Cond.m_ColorPickerContext.m_HsvaColor = color_cast(Cond.m_DisplayHsla);
+ Cond.m_ColorPickerContext.m_RgbaColor = color_cast(Cond.m_DisplayHsla);
+ Cond.m_ColorPickerContext.m_Alpha = true;
+ pUI->ShowPopupColorPicker(pUI->MouseX(), pUI->MouseY(), &Cond.m_ColorPickerContext);
+ }
+
+ if(pUI->IsPopupOpen(&Cond.m_ColorPickerContext) && Cond.m_ColorPickerContext.m_pHslaColor == &Cond.m_ColorValue)
+ {
+ Cond.m_DisplayHsla = color_cast(Cond.m_ColorPickerContext.m_HsvaColor);
+ }
+
+ CondRow.VSplitRight(18.0f, &CondRow, &RemoveBtn);
+ if(pUI->DoButton_FontIcon(&Cond.m_RemoveBtnId, FontIcon::XMARK, 0, &RemoveBtn, BUTTONFLAG_LEFT, IGraphics::CORNER_ALL, true, std::nullopt))
+ {
+ if(Active)
+ {
+ it = pCtx->m_vConditions.erase(it);
+ pCtx->m_NeedsReopen = true;
+ return CUi::POPUP_CLOSE_CURRENT;
+ }
+ }
+ ++it;
+ }
+
+ // Add condition button
+ CUIRect AddCondRow;
+ View.HSplitTop(Margin, nullptr, &View);
+ View.HSplitTop(RowHeight, &AddCondRow, &View);
+ static CButtonContainer s_AddCondBtn;
+ if(pGameClient->m_Menus.DoButton_Menu(&s_AddCondBtn, "+ Добавить условие", 0, &AddCondRow))
+ {
+ pCtx->m_vConditions.emplace_back();
+ pCtx->m_NeedsReopen = true;
+ return CUi::POPUP_CLOSE_CURRENT;
+ }
+
+ // Button row
+ CUIRect ButtonRow;
+ View.HSplitTop(Margin + 5.0f, nullptr, &View);
+ float ButtonRowHeight = 18.0f;
+ View.HSplitTop(ButtonRowHeight, &ButtonRow, &View);
+
+ CUIRect SaveBtn, RemoveBtnB;
+ CUIRect TmpRow = ButtonRow;
+ TmpRow.VSplitLeft((TmpRow.w - Margin) / 2.0f, &SaveBtn, &TmpRow);
+ TmpRow.VSplitLeft(Margin, nullptr, &TmpRow);
+ RemoveBtnB = TmpRow;
+
+ static CButtonContainer s_SaveBtn, s_RemoveBtn;
+ if(pGameClient->m_Menus.DoButton_Menu(&s_SaveBtn, "Сохранить", 0, &SaveBtn))
+ {
+ ColorRGBA FinalColor(1.0f, 1.0f, 1.0f, 1.0f);
+ std::unordered_map ValueColors;
+ for(const auto &Cond : pCtx->m_vConditions)
+ {
+ int Val = Cond.m_ValueInput.GetInteger();
+ ValueColors[Val] = color_cast(Cond.m_DisplayHsla);
+ }
+ pGameClient->m_HudWatch.SetWatch(pCtx->m_pVar, pCtx->m_LabelInput.GetString(), FinalColor, ValueColors);
+ return CUi::POPUP_CLOSE_CURRENT;
+ }
+ if(pGameClient->m_Menus.DoButton_Menu(&s_RemoveBtn, "Удалить", 0, &RemoveBtnB))
+ {
+ pGameClient->m_HudWatch.RemoveWatch(pCtx->m_pVar);
+ return CUi::POPUP_CLOSE_CURRENT;
+ }
+
+ View.HSplitTop(4.0f, nullptr, &View);
+
+ return CUi::POPUP_KEEP_OPEN;
+}
+
void CMenus::RenderSettingsTClientConfigs(CUIRect MainView)
{
// hi hello, this is a relatively self contained mess, sorry if you're forking or need to modify this -Tater
@@ -2908,11 +3061,86 @@ void CMenus::RenderSettingsTClientConfigs(CUIRect MainView)
RowContent.HSplitTop(LineSize, &TopLine, &Below);
}
CUIRect NameLine, Right;
- TopLine.VSplitRight(320.0f, &NameLine, &Right);
+ TopLine.VSplitRight(344.0f, &NameLine, &Right);
NameLine.VSplitLeft(10.0f, nullptr, &NameLine);
+ CUIRect EyeBtn;
+ NameLine.VSplitRight(24.0f, &NameLine, &EyeBtn);
+ Right.VSplitLeft(5.0f, nullptr, &Right);
+ EyeBtn.h = LineSize;
+ EyeBtn.y = TopLine.y + (TopLine.h - LineSize) / 2.0f;
+
Ui()->DoLabel(&NameLine, pVar->m_pScriptName, FontSize, TEXTALIGN_ML);
+ {
+ const bool Watched = GameClient()->m_HudWatch.IsWatched(pVar);
+ static std::unordered_map s_EyeBtns;
+ static std::unordered_map s_EditPopups;
+ CButtonContainer &EyeBtnId = s_EyeBtns[pVar];
+ bool EyeClicked = DoButton_Menu(&EyeBtnId, "", 0, &EyeBtn);
+ TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
+ TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
+ TextRender()->TextColor(Watched ? ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.35f));
+ Ui()->DoLabel(&EyeBtn, Watched ? FontIcon::EYE : FontIcon::EYE_SLASH, EyeBtn.h * 0.5f, TEXTALIGN_MC);
+ TextRender()->TextColor(TextRender()->DefaultTextColor());
+ TextRender()->SetRenderFlags(0);
+ TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
+ if(EyeClicked)
+ {
+ SWatchEditPopupContext &PopupCtx = s_EditPopups[pVar];
+ PopupCtx.m_pGameClient = GameClient();
+ PopupCtx.m_pUI = Ui();
+ PopupCtx.m_pVar = pVar;
+ PopupCtx.m_LabelInput.Clear();
+ PopupCtx.m_vConditions.clear();
+ PopupCtx.m_NeedsReopen = false;
+ bool Found = false;
+ for(const auto &W : GameClient()->m_HudWatch.GetWatches())
+ {
+ if(W.m_pVar == pVar)
+ {
+ PopupCtx.m_LabelInput.Set(W.m_aLabel);
+ PopupCtx.m_DisplayHsla = color_cast(W.m_Color);
+ ColorRGBA Col = W.m_Color;
+ PopupCtx.m_ColorValue = ((unsigned)(Col.a * 255) << 24) | ((unsigned)(Col.r * 255) << 16) | ((unsigned)(Col.g * 255) << 8) | (unsigned)(Col.b * 255);
+ for(const auto &[Val, CCol] : W.m_vValueColors)
+ {
+ PopupCtx.m_vConditions.emplace_back();
+ auto &Cond = PopupCtx.m_vConditions.back();
+ Cond.m_ValueInput.SetInteger(Val);
+ Cond.m_DisplayHsla = color_cast(CCol);
+ ColorRGBA TmpCol = CCol;
+ Cond.m_ColorValue = ((unsigned)(TmpCol.a * 255) << 24) | ((unsigned)(TmpCol.r * 255) << 16) | ((unsigned)(TmpCol.g * 255) << 8) | (unsigned)(TmpCol.b * 255);
+ }
+ Found = true;
+ break;
+ }
+ }
+ if(!Found)
+ {
+ PopupCtx.m_LabelInput.Set(pVar->m_pScriptName);
+ PopupCtx.m_DisplayHsla = ColorHSLA(0.0f, 0.0f, 1.0f, 1.0f);
+ PopupCtx.m_ColorValue = 0xFFFFFFFF;
+ }
+ PopupCtx.m_ColorPickerContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_UNSET;
+ PopupCtx.m_ColorPickerContext.m_Alpha = false;
+ PopupCtx.m_ColorPickerContext.m_pHslaColor = nullptr;
+ PopupCtx.m_ColorPickerContext.m_State = EEditState::NONE;
+ float PopupHeight = 100.0f + PopupCtx.m_vConditions.size() * 19.0f;
+ Ui()->DoPopupMenu(&PopupCtx, EyeBtn.x, EyeBtn.y, 240.0f, PopupHeight, &PopupCtx, WatchEditPopupFunc);
+ }
+ else
+ {
+ auto it = s_EditPopups.find(pVar);
+ if(it != s_EditPopups.end() && it->second.m_NeedsReopen)
+ {
+ it->second.m_NeedsReopen = false;
+ float PopupHeight = 100.0f + it->second.m_vConditions.size() * 19.0f;
+ Ui()->DoPopupMenu(&it->second, EyeBtn.x, EyeBtn.y, 240.0f, PopupHeight, &it->second, WatchEditPopupFunc);
+ }
+ }
+ }
+
CUIRect Controls, ResetRect;
Right.VSplitRight(120.0f, &Controls, &ResetRect);
Controls.h = LineSize;
diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp
index 5f2868d84..8d0f584ce 100644
--- a/src/game/client/gameclient.cpp
+++ b/src/game/client/gameclient.cpp
@@ -332,6 +332,7 @@ void CGameClient::OnConsoleInit()
&m_CustomCommunities, // TClient
&m_MusicPlayer, // BestClient
&m_Hud,
+ &m_HudWatch,
&m_Spectator,
&m_Emoticon,
&m_BindChat, // TClient
diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h
index a6932345a..6e54500aa 100644
--- a/src/game/client/gameclient.h
+++ b/src/game/client/gameclient.h
@@ -81,6 +81,7 @@
#include "components/tclient/skinprofiles.h"
#include "components/tclient/statusbar.h"
#include "components/tclient/tclient.h"
+#include "components/tclient/hud_watch.h"
#include "components/tclient/trails.h"
#include "components/bestclient/3d_particles.h"
#include "components/bestclient/admin_panel.h"
@@ -202,6 +203,7 @@ class CGameClient : public IGameClient
CCountryFlags m_CountryFlags;
CFlow m_Flow;
CHud m_Hud;
+ CHudWatch m_HudWatch;
CImportantAlert m_ImportantAlert;
CDebugHud m_DebugHud;
CControls m_Controls;