From 8575c746e87014bd19613f35033a0b0cd3bfa219 Mon Sep 17 00:00:00 2001 From: Petr Ohlidal Date: Tue, 23 Apr 2024 02:29:37 +0200 Subject: [PATCH] Tuning: proof-of-concept addonpart conflict markers. This implements proactive scanning for addonpart conflicts when the menu is refreshed. This incurs a big lag when opening the menu and likely a lot of spam in RoR.log, but it works. Currently it only displays red squares as conflict markers when one addonpart is hovered. The original conflict resolution logic is still present in `ResolveUnwantedAndTweakedElements()` - when a conflict is found, tweeaks for that elements are reset. But this is flawed, if there's an odd number of conflicting tweaks, the last one will pass. I'll remove it in next commits. --- source/main/gui/panels/GUI_TopMenubar.cpp | 33 +++++++- source/main/gui/panels/GUI_TopMenubar.h | 5 +- .../AddonPartFileFormat.cpp | 84 +++++++++++++++++++ .../AddonPartFileFormat.h | 14 ++++ 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/source/main/gui/panels/GUI_TopMenubar.cpp b/source/main/gui/panels/GUI_TopMenubar.cpp index 8c1f5de7e3..2a7cae77e5 100644 --- a/source/main/gui/panels/GUI_TopMenubar.cpp +++ b/source/main/gui/panels/GUI_TopMenubar.cpp @@ -1611,9 +1611,12 @@ void TopMenubar::Draw(float dt) for (CacheEntryPtr& addonpart_entry: tuning_addonparts) { ImGui::PushID(addonpart_entry->fname.c_str()); - + bool conflict = tuning_hovered_addonpart + && (addonpart_entry != tuning_hovered_addonpart) + && AddonPartUtility::CheckForAddonpartConflict(tuning_hovered_addonpart, addonpart_entry, tuning_conflicts); bool used = TuneupUtil::isAddonPartUsed(tuneup_def, addonpart_entry->fname); - if (ImGui::Checkbox(addonpart_entry->dname.c_str(), &used)) + ImVec2 checkbox_cursor = ImGui::GetCursorScreenPos(); + if (ImGui::Checkbox(addonpart_entry->dname.c_str(), &used) && !conflict) { ModifyProjectRequest* req = new ModifyProjectRequest(); req->mpr_type = (used) @@ -1623,6 +1626,22 @@ void TopMenubar::Draw(float dt) req->mpr_target_actor = tuning_actor; App::GetGameContext()->PushMessage(Message(MSG_EDI_MODIFY_PROJECT_REQUESTED, req)); } + // Draw conflict marker + if (conflict) + { + ImVec2 min = checkbox_cursor + ImGui::GetStyle().FramePadding; + ImVec2 max = min + ImVec2(ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight()); + ImGui::GetWindowDrawList()->AddRectFilled(min, max, ImColor(0.7f, 0.1f, 0.f)); + } + // Record when checkbox is hovered - for drawing conflict markers + if (ImGui::IsItemHovered()) + { + tuning_hovered_addonpart = addonpart_entry; + } + else if (tuning_hovered_addonpart == addonpart_entry) + { + tuning_hovered_addonpart = nullptr; + } // Reload button ImGui::SameLine(); ImGui::Dummy(ImVec2(10.f, 1.f)); @@ -2341,6 +2360,16 @@ void TopMenubar::RefreshTuningMenu() tuning_saves.resetResults(); App::GetCacheSystem()->Query(tuning_saves); + // Refresh `tuning_conflicts` database ~ test addonparts each with each once. + tuning_conflicts.clear(); + for (size_t i1 = 0; i1 < tuning_addonparts.size(); i1++) + { + for (size_t i2 = i1; i2 < tuning_addonparts.size(); i2++) + { + AddonPartUtility::RecordAddonpartConflicts(tuning_addonparts[i1], tuning_addonparts[i2], tuning_conflicts); + } + } + tuning_rwidget_cursorx_min = 0.f; } else if (!App::sim_tuning_enabled->getBool() || !current_actor) diff --git a/source/main/gui/panels/GUI_TopMenubar.h b/source/main/gui/panels/GUI_TopMenubar.h index 25279d3d25..517a8a06cc 100644 --- a/source/main/gui/panels/GUI_TopMenubar.h +++ b/source/main/gui/panels/GUI_TopMenubar.h @@ -25,6 +25,7 @@ #pragma once +#include "AddonPartFileFormat.h" #include "CacheSystem.h" #include "RoRnet.h" @@ -109,7 +110,8 @@ class TopMenubar // Tuning menu ActorPtr tuning_actor; //!< Detecting actor change to update cached values. - std::vector tuning_addonparts; //!< Addonparts of current actor, both matched by GUID and force-installed by user via [browse all] button. + std::vector tuning_addonparts; //!< Addonparts eligible for current actor, both matched by GUID and force-installed by user via [browse all] button. + AddonPartConflictVec tuning_conflicts; //!< Conflicts between eligible addonparts tweaking the same element. CacheQuery tuning_saves; //!< Tuneups saved by user, with category ID `RoR::CID_AddonpartUser` Str<200> tuning_savebox_buf; //!< Buffer for tuneup name to be saved bool tuning_savebox_visible = false; //!< User pressed 'save active' to open savebox. @@ -118,6 +120,7 @@ class TopMenubar const float TUNING_HOLDTOCONFIRM_TIMELIMIT = 1.5f; //!< Delete button must be held for several sec to confirm. bool tuning_force_refresh = false; float tuning_rwidget_cursorx_min = 0.f; //!< Avoid drawing right-side widgets ('Delete' button or 'Protected' chk) over saved tuneup names. + CacheEntryPtr tuning_hovered_addonpart; void RefreshTuningMenu(); private: diff --git a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp index 1e4d7076e0..39edb349b0 100644 --- a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp +++ b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.cpp @@ -567,3 +567,87 @@ void AddonPartUtility::ProcessTweakProp() LOG(fmt::format("[RoR|Addonpart] WARNING: file '{}', directive '{}': bad arguments", m_addonpart_entry->fname, m_context->getTokKeyword())); } } + +void AddonPartUtility::RecordAddonpartConflicts(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts) +{ + LOG(fmt::format("[RoR|Addonpart] -- Performing `RecordAddonpartConflicts()` between '{}' and '{}' ~ this involves generating dummy tuneups (hence messages below) --", addonpart1->fname, addonpart2->fname)); + + // Load both addonparts to dummy Tuneup instances + TuneupDefPtr dummy_t1 = new TuneupDef(); + App::GetCacheSystem()->LoadResource(addonpart1); + AddonPartUtility util_t1; + util_t1.ResolveUnwantedAndTweakedElements(dummy_t1, addonpart1); + + TuneupDefPtr dummy_t2 = new TuneupDef(); + App::GetCacheSystem()->LoadResource(addonpart2); + AddonPartUtility util_t2; + util_t2.ResolveUnwantedAndTweakedElements(dummy_t2, addonpart2); + + // NODE TWEAKS: + for (size_t i = 0; i < dummy_t1->node_tweaks.size(); i++) + { + NodeNum_t suspect = dummy_t1->node_tweaks[i].tnt_nodenum; + TuneupNodeTweak* offender = nullptr; + if (TuneupUtil::isNodeTweaked(dummy_t2, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_node", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - node {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // WHEEL TWEAKS: + for (size_t i = 0; i < dummy_t1->wheel_tweaks.size(); i++) + { + WheelID_t suspect = dummy_t1->wheel_tweaks[i].twt_wheel_id; + TuneupWheelTweak* offender = nullptr; + if (TuneupUtil::isWheelTweaked(dummy_t2, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_wheel", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - wheel {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // PROP TWEAKS: + for (size_t i = 0; i < dummy_t1->prop_tweaks.size(); i++) + { + PropID_t suspect = dummy_t1->prop_tweaks[i].tpt_prop_id; + TuneupPropTweak* offender = nullptr; + if (TuneupUtil::isPropTweaked(dummy_t2, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_prop", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - prop {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + // FLEXBODY TWEAKS: + for (size_t i = 0; i < dummy_t1->flexbody_tweaks.size(); i++) + { + FlexbodyID_t suspect = dummy_t1->flexbody_tweaks[i].tft_flexbody_id; + TuneupFlexbodyTweak* offender = nullptr; + if (TuneupUtil::isFlexbodyTweaked(dummy_t2, suspect, offender)) + { + conflicts.push_back(AddonPartConflict{addonpart1, addonpart2, "addonpart_tweak_flexbody", (int)suspect}); + LOG(fmt::format("[RoR|Addonpart] Found conflict between '{}' and '{}' - flexbody {} is tweaked by both", addonpart1->fname, addonpart2->fname, (int)suspect)); + } + } + + LOG(fmt::format("[RoR|Addonpart] -- Done with `RecordAddonpartConflicts()` between '{}' and '{}' --", addonpart1->fname, addonpart2->fname)); +} + +bool AddonPartUtility::CheckForAddonpartConflict(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts) +{ + if (!addonpart1 || !addonpart2) + { + return false; + } + + for (AddonPartConflict& conflict: conflicts) + { + if ((conflict.atc_addonpart1 == addonpart1 && conflict.atc_addonpart2 == addonpart2) || + (conflict.atc_addonpart1 == addonpart2 && conflict.atc_addonpart2 == addonpart1)) + { + return true; + } + } + return false; +} diff --git a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h index 95184d81f7..96d4b76c71 100644 --- a/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h +++ b/source/main/resources/addonpart_fileformat/AddonPartFileFormat.h @@ -35,6 +35,16 @@ namespace RoR { +struct AddonPartConflict //!< Conflict between two addonparts tweaking the same element +{ + CacheEntryPtr atc_addonpart1; + CacheEntryPtr atc_addonpart2; + std::string atc_keyword; + int atc_element_id = -1; +}; + +typedef std::vector AddonPartConflictVec; + /// NOTE: Modcache processes this format directly using `RoR::GenericDocument`, see `RoR::CacheSystem::FillAddonPartDetailInfo()` class AddonPartUtility { @@ -52,6 +62,10 @@ class AddonPartUtility static void ResetUnwantedAndTweakedElements(TuneupDefPtr& tuneup); + static void RecordAddonpartConflicts(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts); + + static bool CheckForAddonpartConflict(CacheEntryPtr addonpart1, CacheEntryPtr addonpart2, AddonPartConflictVec& conflicts); + private: // Helpers of `TransformToRigDefModule()`, they expect `m_context` to be in position: void ProcessManagedMaterial();