From 8a5d9dcef8e1ac30c3a42efda4793e734b77ccb0 Mon Sep 17 00:00:00 2001 From: UncraftedName Date: Tue, 17 Sep 2024 04:23:53 -0700 Subject: [PATCH] Add spt_hud_ent_info to imgui text hud tab --- spt/features/ent_props.cpp | 168 ++++++++++++++++++ spt/features/ent_props.hpp | 2 + .../visualizations/imgui/imgui_interface.hpp | 10 +- .../imgui/spt_imgui_widgets.cpp | 6 +- spt/utils/ent_utils.cpp | 29 +-- 5 files changed, 185 insertions(+), 30 deletions(-) diff --git a/spt/features/ent_props.cpp b/spt/features/ent_props.cpp index 0a90a3a66..72182edf3 100644 --- a/spt/features/ent_props.cpp +++ b/spt/features/ent_props.cpp @@ -11,9 +11,12 @@ #include "spt\utils\portal_utils.hpp" #include "SPTLib\patterns.hpp" #include "visualizations/imgui/imgui_interface.hpp" +#include "thirdparty/imgui/imgui_internal.h" #include #include +#include +#include ConVar y_spt_hud_portal_bubble("y_spt_hud_portal_bubble", "0", FCVAR_CHEAT, "Turns on portal bubble index hud.\n"); ConVar y_spt_hud_ent_info( @@ -492,9 +495,174 @@ void EntProps::LoadFeature() if (result) { InitConcommandBase(y_spt_hud_ent_info); + SptImGui::RegisterHudCvarCallback(y_spt_hud_ent_info, ImGuiEntInfoCvarCallback, true); } } +void EntProps::ImGuiEntInfoCvarCallback(ConVar& var) +{ + SptImGui::CvarValue(var, SptImGui::CVF_ALWAYS_QUOTE); + + const char* oldVal = var.GetString(); + static ImGuiTextBuffer newVal; + newVal.Buf.resize(0); + newVal.append(""); // explicit null terminator + bool updateCvar = false; + + int nEnts = 0; + int nRegexesForEnt = 0; + + int i = 0; + int len = strlen(oldVal); + + if (whiteSpacesOnly(oldVal)) + i = len; + + /* + * Goal: + * + * - Parse the cvar value. The logic for this is intentially meant to be a duplicate of + * utils::FillInfoArray(). The exact flow control is slightly different in order to handle + * some ImGui state, but the parsed entity indices & regex should be the same. + * + * - As we parse, reconstruct the cvar value from left to right. If the user interacts with the + * GUI, edit the reconstructed value on the fly (e.g. add an extra regex, drop an ent index, + * etc.) and set the cvar value at the end. + */ + + // go up to len so that we can cleanup the stack ID, indent, etc. w/o duplicating code + while (i <= len && nEnts < MAX_ENTRIES) + { + bool atEnd = i >= len; + bool readEntityIndex = !atEnd && (i == 0 || oldVal[i] == ENT_SEPARATOR); + i += i > 0 || nEnts > 0; + int nextSepIdx = i; + + // find the next separator + while (nextSepIdx < len && oldVal[nextSepIdx] != PROP_SEPARATOR && oldVal[nextSepIdx] != ENT_SEPARATOR) + ++nextSepIdx; + + // new entity index, we're at the end, or parse error - cleanup ID, indent, etc. + if ((readEntityIndex || atEnd) && nEnts > 0) + { + if (ImGui::SmallButton("+")) + { + newVal.append(",.*"); + updateCvar = true; + } + ImGui::SetItemTooltip("add regex"); + ImGui::PopID(); // pop nEnts ID + ImGui::Unindent(); + SptImGui::EndBordered(); + } + + if (atEnd) + break; + + // read an index or a regex + + if (readEntityIndex) + { + SptImGui::BeginBordered(); + + long long entIndex = atoll(oldVal + i); + IClientEntity* ent = utils::GetClientEntity(entIndex); + nRegexesForEnt = 0; + + if (SptImGui::InputTextInteger("entity index,", "", entIndex, 10)) + updateCvar = true; + + ImGui::SameLine(); + if (ent) + ImGui::Text("(class name: %s)", ent->GetClientClass()->GetName()); + else + ImGui::TextColored(SPT_IMGUI_WARN_COLOR_YELLOW, "(invalid entity)"); + + ImGui::PushID(nEnts++); + + ImGui::SameLine(); + bool includeEntInNewVal = !ImGui::SmallButton("-"); + ImGui::SetItemTooltip("remove entity"); + if (includeEntInNewVal) + { + // newVal.empty() instead of e.g. (i == 0) or (nEnts == 0) prevents weird parsing edge cases + newVal.appendf("%s%" PRId64, newVal.empty() ? "" : ";", entIndex); + } + else + { + /* + * Jump ahead to the next entity. Note that we still entered the ID & indent scope + * for this entity - that will be popped in the next loop. This jump means that the + * newVal will not get any data (including the index) associated with this entity. + * There does cause one broken frame because the button for the ent deletion must + * exist but I don't draw the regexes for that entity. This is fixable but I cbf. + */ + while (nextSepIdx < len && oldVal[nextSepIdx] != ENT_SEPARATOR) + ++nextSepIdx; + updateCvar = true; + } + ImGui::Indent(); + } + else + { + ImGui::PushID(nRegexesForEnt++); + + const char* regexStart = oldVal + i; + int regexLen = nextSepIdx - i; + char regexBuf[INFO_BUFFER_SIZE]; + strncpy_s(regexBuf, regexStart, regexLen); + // don't allow the characters: ;," + if (ImGui::InputTextEx( + "##regex", + "enter regex", + regexBuf, + sizeof regexBuf, + ImVec2{0, 0}, + ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CallbackCharFilter, + [](ImGuiInputTextCallbackData* data) -> int + { return data->EventChar >= 256 || strchr(";,\"", (char)data->EventChar); }, + nullptr)) + { + updateCvar = true; + } + if (ImGui::IsItemHovered() && regexBuf[0] != '\0') + ImGui::SetItemTooltip("enter regex"); + ImGui::SameLine(); + if (ImGui::SmallButton("-")) + updateCvar = true; + else + newVal.appendf(",%s", regexBuf); + ImGui::SetItemTooltip("remove regex"); + + // test if the regex is valid, report error if not + try + { + std::regex re{regexBuf}; + } + catch (const std::regex_error& exp) + { + ImGui::SameLine(); + ImGui::TextColored(SPT_IMGUI_WARN_COLOR_YELLOW, "(invalid regex)"); + ImGui::SetItemTooltip("%s", exp.what()); + } + + ImGui::PopID(); + } + + i = nextSepIdx; + } + + if (ImGui::SmallButton("+")) + { + newVal.appendf("%s-1,.*", nEnts == 0 ? "" : ";"); + updateCvar = true; + } + ImGui::SetItemTooltip("add entity index"); + + if (updateCvar) + var.SetValue(newVal.c_str()); +} + void** _InternalPlayerField::GetServerPtr() const { auto serverplayer = reinterpret_cast(spt_entprops.GetPlayer(true)); diff --git a/spt/features/ent_props.hpp b/spt/features/ent_props.hpp index ef2fbea73..c4b92faee 100644 --- a/spt/features/ent_props.hpp +++ b/spt/features/ent_props.hpp @@ -83,6 +83,8 @@ class EntProps : public FeatureWrapper std::vector clientPatterns; std::vector wrappers; std::unordered_map nameToMapWrapper; + + static void ImGuiEntInfoCvarCallback(ConVar& var); }; extern EntProps spt_entprops; diff --git a/spt/features/visualizations/imgui/imgui_interface.hpp b/spt/features/visualizations/imgui/imgui_interface.hpp index 57873bd00..834637c30 100644 --- a/spt/features/visualizations/imgui/imgui_interface.hpp +++ b/spt/features/visualizations/imgui/imgui_interface.hpp @@ -175,7 +175,6 @@ namespace SptImGuiGroup // features that access game state inline Tab GameIo{"Game IO", &Root}; - inline Tab GameIo_EntProps{"Ent props", &GameIo}; inline Tab GameIo_ISG{"ISG", &GameIo}; // hud cvars - use the RegisterHudCvarXXX functions below to add cvars @@ -264,10 +263,17 @@ class SptImGui // SPT-related widgets + enum CvarValueFlags + { + CVF_NONE = 0, + CVF_ALWAYS_QUOTE = 1, + // TODO: copy/reset widgets + }; + // a button for a ConCommand with no args, does NOT invoke the command (because of OE compat) static bool CmdButton(const char* label, ConCommand& cmd); // display cvar name & value, surrounds the value in quotes if it has a space (if the value already has quotes, too bad!) - static void CvarValue(const ConVar& c); + static void CvarValue(const ConVar& c, CvarValueFlags flags = CVF_NONE); // a checkbox for a boolean cvar, returns value of cvar static bool CvarCheckbox(ConVar& c, const char* label); // a combo/dropdown box for a cvar with multiple integer options (does not use clipper - not optimized for huge lists), returns value of cvar diff --git a/spt/features/visualizations/imgui/spt_imgui_widgets.cpp b/spt/features/visualizations/imgui/spt_imgui_widgets.cpp index 08730cde0..5e48d309c 100644 --- a/spt/features/visualizations/imgui/spt_imgui_widgets.cpp +++ b/spt/features/visualizations/imgui/spt_imgui_widgets.cpp @@ -13,12 +13,12 @@ bool SptImGui::CmdButton(const char* label, ConCommand& cmd) return ret; } -void SptImGui::CvarValue(const ConVar& c) +void SptImGui::CvarValue(const ConVar& c, CvarValueFlags flags) { const char* v = c.GetString(); const char* surround = ""; - if (strchr(v, ' ')) - surround = " "; + if ((flags & CVF_ALWAYS_QUOTE) || !*v || strchr(v, ' ')) + surround = "\""; ImGui::TextDisabled("(%s %s%s%s)", WrangleLegacyCommandName(c.GetName(), true, nullptr), surround, v, surround); ImGui::SameLine(); HelpMarker("%s", c.GetHelpText()); diff --git a/spt/utils/ent_utils.cpp b/spt/utils/ent_utils.cpp index 732672ab7..46140dda2 100644 --- a/spt/utils/ent_utils.cpp +++ b/spt/utils/ent_utils.cpp @@ -36,6 +36,8 @@ namespace utils { IClientEntity* GetClientEntity(int index) { + if (index >= MAX_EDICTS) + return nullptr; return interfaces::entList->GetClientEntity(index + 1); } @@ -318,31 +320,8 @@ namespace utils while (i < argSize && entries < maxEntries) { - bool readEntityIndex = false; - - // Read entity index - if (args[i] == entSep) - { - ++i; - readEntityIndex = true; - } - // Also read if at the start of the string. Note: does not increment the index since we're at the start of the string. - else if (i == 0) - { - readEntityIndex = true; - } - else if (args[i] == sep) - { - ++i; - } - else // error occurred - { - i = argSize; - arr = (wchar_t*)L"error occurred in parsing"; - entries = 1; - break; - } - + bool readEntityIndex = i == 0 || args[i] == entSep; + i += i > 0 || entries > 0; // i is at a separtor from the previous loop, go to next char int endIndex = i; // Go to the next separator