diff --git a/SPID/include/DistributeManager.h b/SPID/include/DistributeManager.h index 283b65f..038443a 100644 --- a/SPID/include/DistributeManager.h +++ b/SPID/include/DistributeManager.h @@ -3,7 +3,7 @@ namespace Distribute { inline RE::BGSKeyword* processed{ nullptr }; - inline RE::BGSKeyword* processedOutfit{ nullptr }; + inline RE::BGSKeyword* processedOutfit{ nullptr }; // TODO: If OutfitManager works out we won't need this keyword. namespace detail { diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 787ebe7..a2e6ecf 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -7,21 +7,6 @@ namespace Outfits public: static void Register(); - bool IsLoadingGame() const - { - return isLoadingGame; - } - - void StartLoadingGame() - { - isLoadingGame = true; - } - - void FinishLoadingGame() - { - isLoadingGame = false; - } - /// /// Sets given outfit as default outfit for the actor. /// @@ -29,33 +14,63 @@ namespace Outfits /// /// Target Actor for whom the outfit will be set. /// A new outfit to set as the default. - bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); /// - /// Resets current default outfit for the actor. - /// - /// Use this method when outfit distribution doesn't find any suitable Outfit for an NPC. - /// In such cases we need to reset previously distributed outfit if any. - /// This is needed to preserve "runtime" behavior of the SPID, where SPID is expected to not leave any permanent changes. - /// Due to the way outfits work, the only reliable way to equip those is to use game's equipping logic, which stores equipped outfit in a save file, and then clean-up afterwards :) + /// Indicates that given actor didn't receive any distributed outfit and will be using the original one. /// - /// This method looks up any cached distributed outfits for this specific actor - /// and properly removes it from the NPC, then restores defaultOutfit that was used before the distribution. + /// This method helps distinguish cases when there was no outfit distribution for the actor vs when we're reloading the save and replacements cache was cleared. /// - /// Target Actor for whom the outfit should be reset. - /// Previously loaded outfit that needs to be unequipped. - void ResetDefaultOutfit(RE::Actor*, RE::BGSOutfit* previous); + void UseOriginalOutfit(RE::Actor*); private: static void Load(SKSE::SerializationInterface*); static void Save(SKSE::SerializationInterface*); static void Revert(SKSE::SerializationInterface*); - bool isLoadingGame = false; + struct OutfitReplacement + { + /// The one that NPC had before SPID distribution. + RE::BGSOutfit* original; + + /// The one that SPID distributed. + RE::BGSOutfit* distributed; + + OutfitReplacement() = default; + OutfitReplacement(RE::BGSOutfit* original) : + original(original), distributed(nullptr) {} + OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed): original(original), distributed(distributed) {} - void ApplyDefaultOutfit(RE::Actor*); + bool UsesOriginalOutfit() const + { + return original && !distributed; + } + }; - /// Map of Actor -> Outfit associations. - std::unordered_map outfits; + friend fmt::formatter; + + std::unordered_map replacements; }; } + +template <> +struct fmt::formatter +{ + template + constexpr auto parse(ParseContext& a_ctx) + { + return a_ctx.begin(); + } + + template + constexpr auto format(const Outfits::Manager::OutfitReplacement& replacement, FormatContext& a_ctx) + { + if (replacement.UsesOriginalOutfit()) { + return fmt::format_to(a_ctx.out(), "NO REPLACEMENT (Uses {})", *replacement.original); + } else if (replacement.original && replacement.distributed){ + return fmt::format_to(a_ctx.out(), "{} -> {}", *replacement.original, *replacement.distributed); + } else { + return fmt::format_to(a_ctx.out(), "INVALID REPLACEMENT"); + } + } +}; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 3b3075f..8c46a8d 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -106,15 +106,19 @@ namespace Distribute if (!for_first_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit); + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites); }, accumulatedForms)) { - Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), npcData.GetNPC()->defaultOutfit); + Outfits::Manager::GetSingleton()->UseOriginalOutfit(npcData.GetActor()); } for_first_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { - return npcData.GetActor()->SetSleepOutfit(a_outfit, false); + if (npc->sleepOutfit != a_outfit) { + npc->sleepOutfit = a_outfit; + return true; + } + return false; }, accumulatedForms); diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 2fb2b13..3234c1b 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -27,11 +27,13 @@ namespace Distribute namespace Actor { - // FF actor/outfit distribution + // General distribution + // FF actors distribution struct ShouldBackgroundClone { static bool thunk(RE::Character* a_this) { + logger::info("ShouldBackgroundClone({})", *a_this); if (const auto npc = a_this->GetActorBase()) { detail::distribute_on_load(a_this, npc); } @@ -45,16 +47,20 @@ namespace Distribute }; // Post distribution + // Fixes weird behavior with leveled npcs? struct InitLoadGame { static void thunk(RE::Character* a_this, RE::BGSLoadFormBuffer* a_buf) { func(a_this, a_buf); - + if (const auto npc = a_this->GetActorBase()) { // some leveled npcs are completely reset upon loading if (a_this->Is3DLoaded()) { - detail::distribute_on_load(a_this, npc); + // TODO: Test whether there are some NPCs that are getting in this branch + // I haven't experienced issues with ShouldBackgroundClone hook. + logger::info("InitLoadGame({})", *a_this); + // detail::distribute_on_load(a_this, npc); } } } @@ -66,8 +72,8 @@ namespace Distribute void Install() { - stl::write_vfunc(); stl::write_vfunc(); + stl::write_vfunc(); logger::info("Installed actor load hooks"); } diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index c280b68..0d16b2d 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -5,21 +5,19 @@ namespace Outfits constexpr std::uint32_t serializationKey = 'SPID'; constexpr std::uint32_t serializationVersion = 1; - using DistributedOutfit = std::pair; - namespace details { template - bool Write(SKSE::SerializationInterface* a_interface, const T& data) + bool Write(SKSE::SerializationInterface* interface, const T& data) { - return a_interface->WriteRecordData(&data, sizeof(T)); + return interface->WriteRecordData(&data, sizeof(T)); } template <> - bool Write(SKSE::SerializationInterface* a_interface, const std::string& data) + bool Write(SKSE::SerializationInterface* interface, const std::string& data) { const std::size_t size = data.length(); - return a_interface->WriteRecordData(size) && a_interface->WriteRecordData(data.data(), static_cast(size)); + return interface->WriteRecordData(size) && interface->WriteRecordData(data.data(), static_cast(size)); } template @@ -29,15 +27,15 @@ namespace Outfits } template <> - bool Read(SKSE::SerializationInterface* a_interface, std::string& result) + bool Read(SKSE::SerializationInterface* interface, std::string& result) { std::size_t size = 0; - if (!a_interface->ReadRecordData(size)) { + if (!interface->ReadRecordData(size)) { return false; } if (size > 0) { result.resize(size); - if (!a_interface->ReadRecordData(result.data(), static_cast(size))) { + if (!interface->ReadRecordData(result.data(), static_cast(size))) { return false; } } else { @@ -51,37 +49,61 @@ namespace Outfits { constexpr std::uint32_t recordType = 'OTFT'; - - bool Load(SKSE::SerializationInterface* a_interface, std::pair& data) + template + bool Load(SKSE::SerializationInterface* interface, T*& output) { - const bool result = details::Read(a_interface, data.first) && - details::Read(a_interface, data.second); + RE::FormID id = 0; + + if (!details::Read(interface, id)) { + return false; + } + + if (!id) { // If ID was 0 it means we don't have the outfit stored in this record. + output = nullptr; + return true; + } - if (!result) { - logger::warn("Failed to load outfit with FormID [0x{:X}] for NPC with FormID [0x{:X}]", data.second, data.first); + if (!interface->ResolveFormID(id, id)) { return false; } - if (!a_interface->ResolveFormID(data.first, data.first)) { - logger::warn("Failed to load outfit with FormID [0x{:X}] for NPC with FormID [0x{:X}]", data.second, data.first); + if (const auto form = RE::TESForm::LookupByID(id); form) { + output = form; + return true; + } + + return false; + } + + bool Load(SKSE::SerializationInterface* interface, RE::Actor*& loadedActor, RE::BGSOutfit*& loadedOriginalOutfit, RE::BGSOutfit*& loadedDistributedOutfit) + { + if (!Load(interface, loadedActor)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted actor."); return false; } - if (!a_interface->ResolveFormID(data.second, data.second)) { + if (!Load(interface, loadedOriginalOutfit)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted original outfit."); + return false; + } + + if (!Load(interface, loadedDistributedOutfit)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted distributed outfit."); return false; } return true; } - bool Save(SKSE::SerializationInterface* a_interface, const std::pair& data) + bool Save(SKSE::SerializationInterface* a_interface, const RE::Actor* actor, const RE::BGSOutfit* original, const RE::BGSOutfit* distributed) { if (!a_interface->OpenRecord(recordType, serializationVersion)) { return false; } - return details::Write(a_interface, data.first->formID) && - details::Write(a_interface, data.second->formID); + return details::Write(a_interface, actor->formID) && + details::Write(a_interface, original->formID) && + details::Write(a_interface, distributed ? distributed->formID : 0); } } @@ -94,49 +116,61 @@ namespace Outfits serializationInterface->SetRevertCallback(Revert); } - bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) + bool CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) { - if (!actor || !outfit) { - return false; + const auto race = actor->GetRace(); + for (const auto& item : outfit->outfitItems) { + if (const auto armor = item->As()) { + if (!std::any_of(armor->armorAddons.begin(), armor->armorAddons.end(), [&](const auto& arma) { + return arma && arma->IsValidRace(race); + })) { + return false; + } + } } - if (auto previous = outfits.find(actor); previous != outfits.end()) { + return true; + } + + bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) + { + if (!actor || !outfit) { return false; - } else { - outfits[actor] = outfit; - return true; } - } - void Manager::ApplyDefaultOutfit(RE::Actor* actor) { auto* npc = actor->GetActorBase(); + auto defaultOutfit = npc->defaultOutfit; - if (auto previous = outfits.find(actor); previous != outfits.end()) { - if (previous->second != npc->defaultOutfit) { - actor->SetDefaultOutfit(previous->second, false); // Having true here causes infinite loading. It seems that it works either way. - } + if (!allowOverwrites && replacements.find(actor) != replacements.end()) { + return true; } - } - void Manager::ResetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* previous) - { - if (!actor) { - return; + if (!CanEquipOutfit(actor, outfit)) { +#ifndef NDEBUG + logger::warn("Attempted to equip Outfit that can't be worn by given actor. Actor: {}; Outfit: {}", *actor, *outfit); +#endif + return false; } - auto* npc = actor->GetActorBase(); - auto restore = npc->defaultOutfit; // TODO: Probably need to add another outfit entry to keep reference to an outfit that was equipped before the previous one got distributed. + actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that it works either way. - if (previous) { - auto previousOutfit = previous; - outfits.erase(actor); // remove cached outfit as it's no longer needed. - if (previousOutfit == npc->defaultOutfit) { - return; - } - npc->defaultOutfit = previous; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. + if (auto previous = replacements.find(actor); previous != replacements.end()) { + previous->second.distributed = outfit; + } else if (defaultOutfit) { + replacements.try_emplace(actor, defaultOutfit, outfit); } - actor->SetDefaultOutfit(restore, false); // Having true here causes infinite loading. It seems that it works either way. + return true; + } + + void Manager::UseOriginalOutfit(RE::Actor* actor) + { + if (auto npc = actor->GetActorBase(); npc && npc->defaultOutfit) { + if (replacements.find(actor) != replacements.end()) { + logger::warn("Overwriting replacement for {}", *actor); + } + replacements.try_emplace(actor, npc->defaultOutfit); + } } void Manager::Load(SKSE::SerializationInterface* a_interface) @@ -144,87 +178,85 @@ namespace Outfits logger::info("{:*^30}", "LOADING"); auto* manager = Manager::GetSingleton(); - std::uint32_t loadedCount = 0; - std::unordered_map cachedOutfits; - auto& outfits = manager->outfits; + std::unordered_map loadedReplacements; + auto& newReplacements = manager->replacements; std::uint32_t type, version, length; - bool definitionsChanged = false; while (a_interface->GetNextRecordInfo(type, version, length)) { if (type == Data::recordType) { - DistributedOutfit data{}; - if (Data::Load(a_interface, data)) { - if (const auto actor = RE::TESForm::LookupByID(data.first); actor) { - if (const auto outfit = RE::TESForm::LookupByID(data.second); outfit) { + RE::Actor* actor; + RE::BGSOutfit* original; + RE::BGSOutfit* distributed; + if (Data::Load(a_interface, actor, original, distributed)) { + OutfitReplacement replacement(original, distributed); #ifndef NDEBUG - logger::info("\tLoaded outfit {} for actor {}", *outfit, *actor); + logger::info("\tLoaded Outfit Replacement ({}) for actor {}", replacement, *actor); #endif - cachedOutfits[actor] = outfit; - ++loadedCount; - } - } + loadedReplacements[actor] = replacement; } } } - logger::info("Loaded {} distributed outfits", loadedCount); - - - auto keys1 = outfits | std::views::keys; - auto keys2 = cachedOutfits | std::views::keys; - - std::set newActors(keys1.begin(), keys1.end()); - std::set cachedActors(keys2.begin(), keys2.end()); + logger::info("Loaded {} Outfit Replacements", loadedReplacements.size()); + for (const auto& pair : loadedReplacements) { + logger::info("\t{}", *pair.first); + logger::info("\t\t{}", pair.second); + } - std::unordered_set actors; + logger::info("Cached {} Outfit Replacements", newReplacements.size()); + for (const auto& pair : newReplacements) { + logger::info("\t{}", *pair.first); + logger::info("\t\t{}", pair.second); + } - std::set_union(newActors.begin(), newActors.end(), cachedActors.begin(), cachedActors.end(), std::inserter(actors, actors.begin())); + std::uint32_t revertedCount = 0; + for (const auto& it : loadedReplacements) { + const auto& actor = it.first; + const auto& replacement = it.second; - for (const auto& actor : actors) { - if (const auto it = outfits.find(actor); it != outfits.end()) { - if (const auto cit = cachedOutfits.find(actor); cit != cachedOutfits.end()) { - if (it->second != cit->second) { - manager->ResetDefaultOutfit(actor, cit->second); + if (auto newIt = newReplacements.find(actor); newIt != newReplacements.end()) { + if (newIt->second.UsesOriginalOutfit()) { // If new replacement uses original outfit + if (!replacement.UsesOriginalOutfit() && replacement.distributed == actor->GetActorBase()->defaultOutfit) { // but previous one doesn't and NPC still wears the distributed outfit +#ifndef NDEBUG + logger::info("\tReverting Outfit Replacement for {}", *actor); + logger::info("\t\t{}", replacement); +#endif + if (actor->SetDefaultOutfit(replacement.original, false)) { // Having true here causes infinite loading. It seems that it works either way. + ++revertedCount; + } } - } else { - manager->ApplyDefaultOutfit(actor); - } - } else { - if (const auto cit = cachedOutfits.find(actor); cit != cachedOutfits.end()) { - manager->ResetDefaultOutfit(actor, cit->second); + } else { // If new replacement + newIt->second.original = replacement.original; // if there was a previous distribution we want to forward original outfit from there to new distribution. } + + } else { // If there is no new distribution, we want to keep the old one, assuming that whatever outfit is stored in this replacement is what NPC still wears in this save file + newReplacements[actor] = replacement; } } - // TODO: Reset outfits if needed. - // 1) if nothing is in manager->outfits for given actor, but something is in cachedOutfits - // then Reset outfit - // 2) if something is in manager->outfits for given actor and nothing in cachedOutfits - // it should be stored and equipped - // 3) if something is in both sets we should compare two outfits, and - // 3.1) if they are the same - // do nothing, just store in the set (no equip or reset) - // 3.2) if they are not the same - // reset the cached outfit and equip a new one. + + if (revertedCount) { + logger::info("Reverted {} no longer existing Outfit Replacements", revertedCount); + } } - void Manager::Save(SKSE::SerializationInterface* a_interface) + void Manager::Save(SKSE::SerializationInterface* interface) { logger::info("{:*^30}", "SAVING"); - auto outfits = Manager::GetSingleton()->outfits; + auto outfits = Manager::GetSingleton()->replacements; logger::info("Saving {} distributed outfits...", outfits.size()); std::uint32_t savedCount = 0; - for (const auto& data : outfits) { - if (!Data::Save(a_interface, data)) { - logger::error("Failed to save outfit {} for {}", *data.second, *data.first); + for (const auto& pair : outfits) { + if (!Data::Save(interface, pair.first, pair.second.original, pair.second.distributed)) { + logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *pair.first); continue; } #ifndef NDEBUG - logger::info("\tSaved outfit {} for actor {}", *data.second, *data.first->GetActorBase()); + logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *pair.first); #endif ++savedCount; } @@ -235,7 +267,7 @@ namespace Outfits void Manager::Revert(SKSE::SerializationInterface*) { logger::info("{:*^30}", "REVERTING"); - Manager::GetSingleton()->outfits.clear(); - logger::info("\tOutfits cache has been cleared."); + Manager::GetSingleton()->replacements.clear(); + logger::info("\tOutfit Replacements have been cleared."); } }