diff --git a/CMakeLists.txt b/CMakeLists.txt index 839d10c..e22ccbb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,13 +143,13 @@ elseif ("$ENV{RUNTIME_DISABLE_FLAGS}" STREQUAL "-UENABLE_SKYRIM_SE -UENABLE_SKYR set(COPY_VR YES) endif() -if (COPY_SE) +if (COPY_SE AND EXISTS "${SKYRIM_SE_DIR}/Data/SKSE/Plugins") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${SKYRIM_SE_DIR}/Data/SKSE/Plugins/$" ) endif() -if (COPY_VR) +if (COPY_VR AND EXISTS "${SKYRIM_VR_DIR}/Data/SKSE/Plugins") add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${SKYRIM_VR_DIR}/Data/SKSE/Plugins/$" diff --git a/include/GFxDisplayObject.h b/include/GFxDisplayObject.h index 85ce4ef..28168fd 100644 --- a/include/GFxDisplayObject.h +++ b/include/GFxDisplayObject.h @@ -35,14 +35,19 @@ namespace IUI Invoke("swapDepths", a_depth); } - void LoadMovie(const std::string_view& a_swfRelativePath) + bool LoadMovie(const std::string_view& a_swfRelativePath) { - Invoke("loadMovie", a_swfRelativePath.data()); + return Invoke("loadMovie", static_cast(nullptr), a_swfRelativePath.data()); } - void RemoveMovieClip() + bool RemoveMovieClip() { - Invoke("removeMovieClip"); + return Invoke("removeMovieClip", static_cast(nullptr)); + } + + bool SetName(const std::string_view& a_name) + { + return SetMember("_name", a_name.data()); } std::pair GetDefs() const diff --git a/include/GFxMoviePatcher.h b/include/GFxMoviePatcher.h index 17e5d87..e501422 100644 --- a/include/GFxMoviePatcher.h +++ b/include/GFxMoviePatcher.h @@ -71,13 +71,17 @@ namespace IUI return std::string(dotPos != std::string::npos ? a_memberPath.substr(0, dotPos) : "_root"); } - void AddInstance(const std::string& a_name, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath); + bool AddInstance(const std::string& a_name, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath); - void ReplaceInstance(const std::string& a_name, GFxDisplayObject& a_originalInstance, + bool ReplaceInstance(const std::string& a_name, GFxDisplayObject& a_originalInstance, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath); void AbortReplaceInstance(const std::string& a_name, RE::GFxValue& a_originalInstance, const std::string& a_patchRelativePath) const; + bool IsPatchDisabled(const std::string& a_patchRelativePath) const; + bool LoadInstanceMovie(GFxDisplayObject& a_instance, const std::string& a_patchRelativePath, bool a_dispatchPostPatch = true); + std::string MakeTempInstanceName(const std::string& a_name) const; + // members RE::IMenu* menu; diff --git a/include/Settings.h b/include/Settings.h index 4f56997..60f7dba 100644 --- a/include/Settings.h +++ b/include/Settings.h @@ -16,4 +16,11 @@ namespace settings { inline logger::level logLevel = logger::level::err; } -} \ No newline at end of file + + namespace patching + { + inline bool safeReplacement = true; + inline std::uint32_t slowPatchWarningMS = 250; + inline const char* disabledPatchFiles = ""; + } +} diff --git a/source/GFxMoviePatcher.cpp b/source/GFxMoviePatcher.cpp index aafb2e8..21a6c94 100644 --- a/source/GFxMoviePatcher.cpp +++ b/source/GFxMoviePatcher.cpp @@ -1,11 +1,48 @@ #include "GFxMoviePatcher.h" #include "GFxDisplayObject.h" +#include "Settings.h" + +#include +#include +#include +#include namespace IUI { //std::unordered_map g_moviePatchersList; + namespace + { + using Clock = std::chrono::steady_clock; + + std::string NormalizePatchPath(std::string a_path) + { + std::replace(a_path.begin(), a_path.end(), '/', '\\'); + std::transform(a_path.begin(), a_path.end(), a_path.begin(), [](unsigned char a_char) { + return static_cast(std::tolower(a_char)); + }); + return a_path; + } + + void Trim(std::string& a_text) + { + const auto start = a_text.find_first_not_of(" \t"); + if (start == std::string::npos) { + a_text.clear(); + return; + } + + const auto end = a_text.find_last_not_of(" \t"); + a_text = a_text.substr(start, end - start + 1); + } + + std::uint64_t ElapsedMS(const Clock::time_point& a_start) + { + return static_cast(std::chrono::duration_cast(Clock::now() - a_start).count()); + } + } + GFxMoviePatcher::GFxMoviePatcher(RE::IMenu* a_menu, RE::GFxMovieRoot* a_movieRoot) : menu{ a_menu }, movieRoot{ a_movieRoot } { @@ -38,10 +75,10 @@ namespace IUI { std::filesystem::path currentPath = stack.top(); stack.pop(); - + if (std::filesystem::is_directory(currentPath)) { - for (const std::filesystem::directory_entry& childPath : std::filesystem::directory_iterator{ currentPath }) + for (const std::filesystem::directory_entry& childPath : std::filesystem::directory_iterator{ currentPath }) { stack.push(childPath); } @@ -52,6 +89,13 @@ namespace IUI logger::debug("Relative path is \"{}\"", patchRelativePath); + if (IsPatchDisabled(patchRelativePath)) { + logger::warn("Skipping disabled InfinityUI patch \"{}\"", patchRelativePath); + continue; + } + + const auto patchStart = Clock::now(); + std::string instancePath = GetInstanceASPath(startPath, currentPath); std::string parentPath = GetInstanceParentASPath(instancePath); @@ -77,22 +121,24 @@ namespace IUI if (member.IsDisplayObject()) { GFxDisplayObject instance = member; - ReplaceInstance(instanceName, instance, parent, patchRelativePath); - loadCount++; + if (ReplaceInstance(instanceName, instance, parent, patchRelativePath)) { + loadCount++; + } } else { AbortReplaceInstance(instanceName, member, patchRelativePath); } } - else + else { - AddInstance(instanceName, parent, patchRelativePath); - loadCount++; + if (AddInstance(instanceName, parent, patchRelativePath)) { + loadCount++; + } } } } - else + else { std::string parentName = GetInstanceASName(parentPath); @@ -100,6 +146,11 @@ namespace IUI } } } + + const auto patchElapsedMS = ElapsedMS(patchStart); + if (settings::patching::slowPatchWarningMS > 0 && patchElapsedMS >= settings::patching::slowPatchWarningMS) { + logger::warn("InfinityUI patch \"{}\" took {} ms", patchRelativePath, patchElapsedMS); + } } } @@ -110,32 +161,69 @@ namespace IUI return loadCount; } - void GFxMoviePatcher::AddInstance(const std::string& a_name, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath) + bool GFxMoviePatcher::AddInstance(const std::string& a_name, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath) { GFxDisplayObject newInstance = a_parent.CreateEmptyMovieClip(a_name, a_parent.GetNextHighestDepth()); logger::trace("Relative path to patch is: {}", a_patchRelativePath); - newInstance.LoadMovie(a_patchRelativePath); - movieRoot->Advance(0); + if (!LoadInstanceMovie(newInstance, a_patchRelativePath)) { + newInstance.RemoveMovieClip(); + return false; + } - logger::trace("New instance loaded!"); logger::trace(""); LogMembersOf(a_parent); - LogMembersOf(newInstance); logger::trace(""); - auto [movieDefImpl, spriteDef] = newInstance.GetDefs(); - - addedInstanceMovieDefs.push_back(movieDefImpl); - - // Actions after loading the movieclip - API::DispatchMessage(API::PostPatchInstanceMessage{ menu, movieRoot, newInstance, movieDefImpl, spriteDef }); + return true; } - void GFxMoviePatcher::ReplaceInstance(const std::string& a_name, GFxDisplayObject& a_originalInstance, + bool GFxMoviePatcher::ReplaceInstance(const std::string& a_name, GFxDisplayObject& a_originalInstance, GFxDisplayObject& a_parent, const std::string& a_patchRelativePath) { + if (settings::patching::safeReplacement) { + const std::string tempName = MakeTempInstanceName(a_name); + GFxDisplayObject newInstance = a_parent.CreateEmptyMovieClip(tempName, a_parent.GetNextHighestDepth()); + + logger::trace("Relative path to patch is: {}", a_patchRelativePath); + + if (!LoadInstanceMovie(newInstance, a_patchRelativePath, false)) { + logger::warn("Keeping original {} because replacement patch failed to load from {}", a_name, a_patchRelativePath); + newInstance.RemoveMovieClip(); + AbortReplaceInstance(a_name, a_originalInstance, a_patchRelativePath); + return false; + } + + // Last chance to retrieve info before removing the movieclip + API::DispatchMessage(API::PreReplaceInstanceMessage{ menu, movieRoot, a_originalInstance }); + + logger::trace(""); + LogMembersOf(a_parent); + LogMembersOf(a_originalInstance); + logger::trace(""); + + a_originalInstance.SwapDepths(1); + movieRoot->Advance(0); + + a_originalInstance.RemoveMovieClip(); + movieRoot->Advance(0); + + newInstance.SetName(a_name); + movieRoot->Advance(0); + + auto [movieDefImpl, spriteDef] = newInstance.GetDefs(); + API::DispatchMessage(API::PostPatchInstanceMessage{ menu, movieRoot, newInstance, movieDefImpl, spriteDef }); + + logger::trace("Original instance replaced!"); + logger::trace(""); + LogMembersOf(a_parent); + LogMembersOf(newInstance); + logger::trace(""); + + return true; + } + // Last chance to retrieve info before removing the movieclip API::DispatchMessage(API::PreReplaceInstanceMessage{ menu, movieRoot, a_originalInstance }); @@ -161,15 +249,85 @@ namespace IUI LogMembersOf(a_parent); logger::trace(""); - AddInstance(a_name, a_parent, a_patchRelativePath); + return AddInstance(a_name, a_parent, a_patchRelativePath); } void GFxMoviePatcher::AbortReplaceInstance(const std::string& a_name, RE::GFxValue& a_instance, const std::string& a_patchRelativePath) const { - logger::warn("{} exists in the movie, but it is not a DisplayObject. Aborting replacement for {}", + logger::warn("{} exists in the movie, but it is not a DisplayObject. Aborting replacement for {}", a_name, a_patchRelativePath); API::DispatchMessage(API::AbortPatchInstanceMessage{ menu, movieRoot, a_instance }); } + + bool GFxMoviePatcher::IsPatchDisabled(const std::string& a_patchRelativePath) const + { + const char* disabledPatchFiles = settings::patching::disabledPatchFiles; + if (!disabledPatchFiles || !disabledPatchFiles[0]) { + return false; + } + + const std::string patchPath = NormalizePatchPath(a_patchRelativePath); + std::stringstream disabledList{ disabledPatchFiles }; + std::string disabledPath; + + while (std::getline(disabledList, disabledPath, ';')) { + std::stringstream commaList{ disabledPath }; + std::string token; + + while (std::getline(commaList, token, ',')) { + token = NormalizePatchPath(token); + Trim(token); + + if (!token.empty() && patchPath.ends_with(token)) { + return true; + } + } + } + + return false; + } + + bool GFxMoviePatcher::LoadInstanceMovie(GFxDisplayObject& a_instance, const std::string& a_patchRelativePath, bool a_dispatchPostPatch) + { + const auto loadStart = Clock::now(); + + if (!a_instance.LoadMovie(a_patchRelativePath)) { + logger::warn("loadMovie invocation failed for {}", a_patchRelativePath); + return false; + } + + movieRoot->Advance(0); + + const auto loadElapsedMS = ElapsedMS(loadStart); + if (settings::patching::slowPatchWarningMS > 0 && loadElapsedMS >= settings::patching::slowPatchWarningMS) { + logger::warn("loadMovie for \"{}\" took {} ms", a_patchRelativePath, loadElapsedMS); + } + + auto [movieDefImpl, spriteDef] = a_instance.GetDefs(); + if (!movieDefImpl || !spriteDef) { + logger::warn("Could not resolve loaded movie definitions for {}", a_patchRelativePath); + return false; + } + + logger::trace("New instance loaded!"); + logger::trace(""); + LogMembersOf(a_instance); + logger::trace(""); + + addedInstanceMovieDefs.push_back(movieDefImpl); + + if (a_dispatchPostPatch) { + // Actions after loading the movieclip + API::DispatchMessage(API::PostPatchInstanceMessage{ menu, movieRoot, a_instance, movieDefImpl, spriteDef }); + } + + return true; + } + + std::string GFxMoviePatcher::MakeTempInstanceName(const std::string& a_name) const + { + return "__IUI_LoadCheck_" + a_name; + } } diff --git a/source/Hooks.cpp b/source/Hooks.cpp index 40aeb0b..09fb8b7 100644 --- a/source/Hooks.cpp +++ b/source/Hooks.cpp @@ -4,16 +4,27 @@ #include "GFxMoviePatcher.h" +#include + namespace hooks { void PatchGFxMovie(RE::GFxMovieRoot* a_movieRoot, float a_deltaT, std::uint32_t a_frameCatchUpCount, RE::IMenu* a_menu) { a_movieRoot->Advance(a_deltaT, a_frameCatchUpCount); + static thread_local std::unordered_set patchingMovieRoots; + const auto [it, inserted] = patchingMovieRoots.insert(a_movieRoot); + if (!inserted) { + logger::warn("Skipping recursive InfinityUI patch pass for {}", a_movieRoot->GetMovieDef()->GetFileURL()); + return; + } + IUI::GFxMoviePatcher moviePatcher{ a_menu, a_movieRoot }; int loadedInstancesCount = moviePatcher.LoadInstancePatches(); + patchingMovieRoots.erase(it); + const char* movieRootFileUrl = a_movieRoot->GetMovieDef()->GetFileURL(); @@ -42,4 +53,4 @@ namespace hooks IUI::API::DispatchMessage(IUI::API::PostInitExtensionsMessage{ a_menu, a_movieRoot }); //} } -} \ No newline at end of file +} diff --git a/source/Settings.cpp b/source/Settings.cpp index 6462be5..adfadf7 100644 --- a/source/Settings.cpp +++ b/source/Settings.cpp @@ -15,6 +15,13 @@ namespace settings iniSettingCollection->AddSettings( MakeSetting("uLogLevel:Debug", static_cast(logLevel))); } + { + using namespace patching; + iniSettingCollection->AddSettings( + MakeSetting("bSafeReplacement:Patching", safeReplacement), + MakeSetting("uSlowPatchWarningMS:Patching", slowPatchWarningMS), + MakeSetting("sDisabledPatchFiles:Patching", disabledPatchFiles)); + } if (!iniSettingCollection->ReadFromFile(a_iniFileName)) { logger::warn("Could not read {}, falling back to default options", a_iniFileName); @@ -24,5 +31,11 @@ namespace settings using namespace debug; logLevel = static_cast(iniSettingCollection->GetSetting("uLogLevel:Debug")); } + { + using namespace patching; + safeReplacement = iniSettingCollection->GetSetting("bSafeReplacement:Patching"); + slowPatchWarningMS = iniSettingCollection->GetSetting("uSlowPatchWarningMS:Patching"); + disabledPatchFiles = iniSettingCollection->GetSetting("sDisabledPatchFiles:Patching"); + } } -} \ No newline at end of file +}