diff --git a/Mods/Editor/Src/Components/EntityProperties.cpp b/Mods/Editor/Src/Components/EntityProperties.cpp index 148cb3c7..694a3de1 100644 --- a/Mods/Editor/Src/Components/EntityProperties.cpp +++ b/Mods/Editor/Src/Components/EntityProperties.cpp @@ -439,11 +439,34 @@ void Editor::DrawEntityProperties() { } } + auto s_InputPins = GetPins(s_SelectedEntity, false); + ImGui::SameLine(0, 5); - if (ImGui::InputText( - "Input Pin", s_InputPinName, IM_ARRAYSIZE(s_InputPinName), ImGuiInputTextFlags_EnterReturnsTrue - )) { + Util::ImGuiUtils::InputWithAutocomplete( + "Input Pin##InputPinsPopup", + s_InputPinName, + sizeof(s_InputPinName), + s_InputPins, + [](const PinInfo& pin) -> std::string { + return pin.name; + }, + [](const PinInfo& pin) -> std::string { + if (pin.description.empty()) { + return pin.name; + } + + return pin.name + " - " + pin.description; + }, + [](const std::string&, const std::string&, const std::string& value) { + strcpy_s(s_InputPinName, value.c_str()); + }, + [](const PinInfo& pin) -> std::string { + return pin.name; + } + ); + + if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Enter)) { ZObjectRef s_ObjectRef; if (m_InputPinData) { @@ -551,11 +574,34 @@ void Editor::DrawEntityProperties() { } } + auto s_OutputPins = GetPins(s_SelectedEntity, true); + ImGui::SameLine(0, 5); - if (ImGui::InputText( - "Output Pin", s_OutputPinName, IM_ARRAYSIZE(s_OutputPinName), ImGuiInputTextFlags_EnterReturnsTrue - )) { + Util::ImGuiUtils::InputWithAutocomplete( + "Output Pin##OutputPinsPopup", + s_OutputPinName, + sizeof(s_OutputPinName), + s_OutputPins, + [](const PinInfo& pin) -> std::string { + return pin.name; + }, + [](const PinInfo& pin) -> std::string { + if (pin.description.empty()) { + return pin.name; + } + + return pin.name + " - " + pin.description; + }, + [](const std::string&, const std::string&, const std::string& value) { + strcpy_s(s_OutputPinName, value.c_str()); + }, + [](const PinInfo& pin) -> std::string { + return pin.name; + } + ); + + if (ImGui::IsItemActive() && ImGui::IsKeyPressed(ImGuiKey_Enter)) { ZObjectRef s_ObjectRef; if (m_OutputPinData) { diff --git a/Mods/Editor/Src/Editor.cpp b/Mods/Editor/Src/Editor.cpp index 6bcc3cd2..689b88fa 100644 --- a/Mods/Editor/Src/Editor.cpp +++ b/Mods/Editor/Src/Editor.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "Hooks.h" #include "Logging.h" @@ -31,6 +32,7 @@ #include #include "Util/StringUtils.h" +#include "Util/HttpUtils.h" Editor::Editor() { // Disable ZTemplateEntityBlueprintFactory freeing its associated data. @@ -499,6 +501,38 @@ void Editor::OnFrameUpdate(const SGameUpdateEvent& p_UpdateEvent) { m_ItemToRemove = {}; m_RemoveItemFromInventory = false; } + + static std::future> s_DownloadFuture; + static bool s_DownloadStarted = false; + static bool s_DownloadCompleted = false; + + if (m_ClassToInputAndOutputPins.empty() && !s_DownloadStarted) { + const std::string s_PinsUrl = + "https://raw.githubusercontent.com/glacier-modding/glaciermodding.org" + "/refs/heads/main/docs/modding/hitman/guides/pins.json"; + + s_DownloadFuture = std::async( + std::launch::async, [this, s_PinsUrl]() { + std::string jsonContent = Util::HttpUtils::DownloadFromUrl(s_PinsUrl); + + if (!jsonContent.empty()) { + return ParsePinsJson(jsonContent); + } + + return std::map(); + } + ); + + s_DownloadStarted = true; + } + + if (s_DownloadStarted && !s_DownloadCompleted) { + if (s_DownloadFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + m_ClassToInputAndOutputPins = s_DownloadFuture.get(); + s_DownloadCompleted = true; + Logger::Debug("Pin list download complete! Loaded {} classes.", m_ClassToInputAndOutputPins.size()); + } + } } void Editor::ProcessTasks() { @@ -522,6 +556,132 @@ void Editor::QueueTask(std::function p_Task) { m_TaskQueue.push_back(std::move(p_Task)); } +std::vector Editor::GetPins(ZEntityRef p_EntityRef, bool outputPins) { + std::vector s_Result; + + if (!p_EntityRef || !p_EntityRef->GetType()->m_pInterfaceData) { + return s_Result; + } + + TArray* s_Interfaces = p_EntityRef->GetType()->m_pInterfaceData; + + std::unordered_set s_PinNames; + + for (const SInterfaceData& s_InterfaceData : *s_Interfaces) + { + const IType* s_TypeInfo = s_InterfaceData.m_Type->GetTypeInfo(); + + if (!s_TypeInfo) { + continue; + } + + const std::string s_ClassName = Util::StringUtils::ToLowerCase(s_TypeInfo->pszTypeName); + + auto s_Iterator = m_ClassToInputAndOutputPins.find(s_ClassName); + + if (s_Iterator == m_ClassToInputAndOutputPins.end()) { + continue; + } + + const auto& s_Pins = outputPins ? s_Iterator->second.outputPins : s_Iterator->second.inputPins; + + for (const auto& s_Pin : s_Pins) { + if (s_PinNames.insert(s_Pin.name).second) { + s_Result.push_back(s_Pin); + } + } + } + + std::sort( + s_Result.begin(), + s_Result.end(), + [](const PinInfo& p_A, const PinInfo& p_B) { + return p_A.name < p_B.name; + } + ); + + return s_Result; +} + +std::map Editor::ParsePinsJson(const std::string& p_PinsJson) { + std::map result; + + simdjson::ondemand::parser parser; + simdjson::padded_string json = simdjson::padded_string(p_PinsJson); + + simdjson::ondemand::document doc = parser.iterate(json); + simdjson::ondemand::array entries = doc.get_array(); + + for (auto entry : entries) + { + std::string_view path = entry["path"].get_string(); + + // extract class name + std::string className; + { + size_t start = path.find('/'); + size_t end = path.find(".class"); + + if (start != std::string_view::npos && end != std::string_view::npos && end > start) + className = std::string(path.substr(start + 1, end - start - 1)); + } + + if (className.empty()) + continue; + + auto& pins = result[className]; + + // INPUT PINS + simdjson::ondemand::array inputPins = entry["in"].get_array(); + + for (auto pinEntry : inputPins) + { + std::string_view pinName = pinEntry["pin"].get_string(); + std::string_view description = pinEntry["description"].get_string(); + + pins.inputPins.push_back({ + std::string(pinName), + std::string(description) + }); + } + + // OUTPUT PINS + simdjson::ondemand::array outputPins = entry["out"].get_array(); + + for (auto pinEntry : outputPins) + { + std::string_view pinName = pinEntry["pin"].get_string(); + std::string_view description = pinEntry["description"].get_string(); + + pins.outputPins.push_back({ + std::string(pinName), + std::string(description) + }); + } + } + + for (auto& [className, pins] : result) + { + std::sort( + pins.inputPins.begin(), + pins.inputPins.end(), + [](const PinInfo& a, const PinInfo& b) { + return a.name < b.name; + } + ); + + std::sort( + pins.outputPins.begin(), + pins.outputPins.end(), + [](const PinInfo& a, const PinInfo& b) { + return a.name < b.name; + } + ); + } + + return result; +} + void Editor::OnMouseDown(SVector2 p_Pos, bool p_FirstClick) { SVector3 s_World; SVector3 s_Direction; diff --git a/Mods/Editor/Src/Editor.h b/Mods/Editor/Src/Editor.h index 3ced669e..cfc81f6f 100644 --- a/Mods/Editor/Src/Editor.h +++ b/Mods/Editor/Src/Editor.h @@ -77,6 +77,7 @@ class Editor : public IPluginInterface { static QneTransform MatrixToQneTransform(const SMatrix& p_Matrix); void QueueTask(std::function p_Task); + private: struct DebugEntity { std::string m_TypeName; @@ -92,6 +93,16 @@ class Editor : public IPluginInterface { SMatrix m_Transform; }; + struct PinInfo { + std::string name; + std::string description; + }; + + struct PinLists { + std::vector inputPins; + std::vector outputPins; + }; + void SpawnCameras(); void ActivateCamera(ZEntityRef* m_CameraEntity); void DeactivateCamera(); @@ -304,6 +315,10 @@ class Editor : public IPluginInterface { void ProcessTasks(); + std::vector GetPins(ZEntityRef p_EntityRef, bool outputPins); + + std::map ParsePinsJson(const std::string& p_PinsJson); + private: DECLARE_PLUGIN_DETOUR(Editor, bool, OnLoadScene, ZEntitySceneContext*, SSceneInitParameters&); DECLARE_PLUGIN_DETOUR(Editor, void, OnClearScene, ZEntitySceneContext* th, bool p_FullyUnloadScene); @@ -583,6 +598,7 @@ class Editor : public IPluginInterface { std::mutex m_TaskMutex; std::vector> m_TaskQueue; + std::map m_ClassToInputAndOutputPins; std::vector> m_PinDataTypes; STypeID* m_InputPinTypeID = nullptr; void* m_InputPinData = nullptr;