diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index eb5a8a4..4c4c33f 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -58,6 +58,9 @@ namespace Outfits /// True if the outfit was successfully set, false otherwise bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + /// This re-creates game's function that performs a similar code, but crashes for unknown reasons :) + void AddWornOutfit(RE::Actor*, const RE::BGSOutfit*); + struct OutfitReplacement { /// The one that NPC had before SPID distribution. @@ -66,11 +69,14 @@ namespace Outfits /// The one that SPID distributed. RE::BGSOutfit* distributed; + /// FormID of the outfit that was meant to be distributed, but was not recognized during loading (most likely source plugin is no longer active). + RE::FormID unrecognizedDistributedFormID; + OutfitReplacement() = default; - OutfitReplacement(RE::BGSOutfit* original) : - original(original), distributed(nullptr) {} + OutfitReplacement(RE::BGSOutfit* original, RE::FormID unrecognizedDistributedFormID) : + original(original), distributed(nullptr), unrecognizedDistributedFormID(unrecognizedDistributedFormID) {} OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed) : - original(original), distributed(distributed) {} + original(original), distributed(distributed), unrecognizedDistributedFormID(0) {} }; friend fmt::formatter; @@ -110,6 +116,8 @@ struct fmt::formatter } else { return fmt::format_to(a_ctx.out(), "{} ➡️ {}", *replacement.original, *replacement.distributed); } + } else if (replacement.original) { + return fmt::format_to(a_ctx.out(), "{} 🔙 CORRUPTED [{}:{:08X}]", *replacement.original, RE::FormType::Outfit, replacement.unrecognizedDistributedFormID); } else { return fmt::format_to(a_ctx.out(), "INVALID REPLACEMENT"); } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index e247afc..72448d2 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -1,8 +1,6 @@ #include "DistributeManager.h" -#include "DeathDistribution.h" #include "Distribute.h" #include "DistributePCLevelMult.h" -#include "OutfitManager.h" namespace Distribute { @@ -37,7 +35,6 @@ namespace Distribute if (const auto npc = a_this->GetActorBase()) { detail::distribute_on_load(a_this, npc); } - return func(a_this); } static inline REL::Relocation func; @@ -95,8 +92,6 @@ namespace Distribute logger::info("{:*^50}", "EVENTS"); Event::Manager::Register(); PCLevelMult::Manager::Register(); - DeathDistribution::Manager::Register(); - Outfits::Manager::Register(); // TODO: No initial distribution. Check Packages distribution and see if those work as intended. //DoInitialDistribution(); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 3a2f85b..847b941 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -53,6 +53,7 @@ namespace Outfits bool Load(SKSE::SerializationInterface* interface, T*& output, RE::FormID& formID) { RE::FormID id = 0; + output = nullptr; if (!details::Read(interface, id)) { return false; @@ -62,13 +63,13 @@ namespace Outfits return false; } - formID = id; // save the originally read formID + formID = id; // save the originally read formID if (!interface->ResolveFormID(id, id)) { return false; } - formID = id; // save the resolved formID + formID = id; // save the resolved formID if (const auto form = RE::TESForm::LookupByID(id); form) { output = form; @@ -78,9 +79,10 @@ namespace Outfits return false; } - bool Load(SKSE::SerializationInterface* interface, RE::Actor*& loadedActor, RE::BGSOutfit*& loadedOriginalOutfit, RE::BGSOutfit*& loadedDistributedOutfit) + bool Load(SKSE::SerializationInterface* interface, RE::Actor*& loadedActor, RE::BGSOutfit*& loadedOriginalOutfit, RE::BGSOutfit*& loadedDistributedOutfit, RE::FormID& failedDistributedOutfitFormID) { RE::FormID id = 0; + failedDistributedOutfitFormID = 0; if (!Load(interface, loadedActor, id)) { logger::warn("Failed to load Outfit Replacement record: Corrupted actor [{:08X}].", id); @@ -93,6 +95,7 @@ namespace Outfits } if (!Load(interface, loadedDistributedOutfit, id)) { + failedDistributedOutfitFormID = id; logger::warn("Failed to load Outfit Replacement record: Corrupted distributed outfit [{:08X}].", id); return false; } @@ -133,22 +136,6 @@ namespace Outfits return RE::BSEventNotifyControl::kContinue; } - bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) - { - 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; - } - } - } - - return true; - } - ReplacementResult Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) { if (!actor || !outfit) { // invalid call @@ -189,6 +176,67 @@ namespace Outfits return ReplacementResult::Set; } + bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) + { + 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; + } + } + } + + return true; + } + + void Manager::AddWornOutfit(RE::Actor* actor, const RE::BGSOutfit* a_outfit) + { + bool equippedItems = false; + if (const auto invChanges = actor->GetInventoryChanges()) { + if (const auto entryLists = invChanges->entryList) { + const auto formID = a_outfit->GetFormID(); + + for (const auto& entryList : *entryLists) { + if (entryList && entryList->object && entryList->extraLists) { + for (const auto& xList : *entryList->extraLists) { + const auto outfitItem = xList ? xList->GetByType() : nullptr; + if (outfitItem && outfitItem->id == formID) { + RE::ActorEquipManager::GetSingleton()->EquipObject(actor, entryList->object, xList, 1, nullptr, true); + equippedItems = true; + } + } + } + } + } + } + + if (equippedItems) { + actor->currentProcess->Update3DModel(actor); + } + } + + bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) + { + if (!actor || !outfit) { + return false; + } + + const auto npc = actor->GetActorBase(); + if (!npc || npc->defaultOutfit == outfit) { + return false; + } + actor->RemoveOutfitItems(npc->defaultOutfit); + npc->SetDefaultOutfit(outfit); + actor->InitInventoryIfRequired(); + if (!actor->IsDisabled()) { + AddWornOutfit(actor, outfit); + } + return true; + } + void Manager::Load(SKSE::SerializationInterface* a_interface) { #ifndef NDEBUG @@ -200,39 +248,53 @@ namespace Outfits auto& newReplacements = manager->replacements; std::uint32_t type, version, length; - + int total = 0; while (a_interface->GetNextRecordInfo(type, version, length)) { if (type == Data::recordType) { RE::Actor* actor; RE::BGSOutfit* original; RE::BGSOutfit* distributed; - if (Data::Load(a_interface, actor, original, distributed); actor) { - loadedReplacements[actor] = { original, distributed }; + RE::FormID failedDistributedOutfitFormID; + total++; + if (Data::Load(a_interface, actor, original, distributed, failedDistributedOutfitFormID); actor) { + if (distributed) { + loadedReplacements[actor] = { original, distributed }; + } else { + loadedReplacements[actor] = { original, failedDistributedOutfitFormID }; + } } } } #ifndef NDEBUG - logger::info("Loaded {} Outfit Replacements", loadedReplacements.size()); + logger::info("Loaded {}/{} Outfit Replacements", loadedReplacements.size(), total); for (const auto& pair : loadedReplacements) { logger::info("\t{}", *pair.first); logger::info("\t\t{}", pair.second); } - logger::info("Cached {} Outfit Replacements", newReplacements.size()); + logger::info("Current {} Outfit Replacements", newReplacements.size()); for (const auto& pair : newReplacements) { if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { logger::info("\t{}", *actor); } logger::info("\t\t{}", pair.second); } + + logger::info("Merging..."); #endif std::uint32_t revertedCount = 0; + std::uint32_t updatedCount = 0; for (const auto& it : loadedReplacements) { const auto& actor = it.first; const auto& replacement = it.second; - if (auto newIt = newReplacements.find(actor->formID); newIt != newReplacements.end()) { // If we have some new replacement for this actor - newIt->second.original = replacement.original; // we want to forward original outfit from the previous replacement to the new one. (so that a chain of outfits like this A->B->C becomes A->C and we'll be able to revert to the very first outfit) - } else if (auto npc = actor->GetActorBase(); npc && replacement.distributed == npc->defaultOutfit) { // If there is no new replacement, and an actor is currently wearing the same outfit that was distributed to them last time, we want to revert whatever outfit was in previous replacement + if (auto newIt = newReplacements.find(actor->formID); newIt != newReplacements.end()) { // If we have some new replacement for this actor + newIt->second.original = replacement.original; // we want to forward original outfit from the previous replacement to the new one. (so that a chain of outfits like this A->B->C becomes A->C and we'll be able to revert to the very first outfit) + ++updatedCount; +#ifndef NDEBUG + logger::info("\tUpdating Outfit Replacement for {}", *actor); + logger::info("\t\t{}", newIt->second); +#endif + } else if (auto npc = actor->GetActorBase(); !replacement.distributed || npc && replacement.distributed == npc->defaultOutfit) { // If the replacement is not valid or there is no new replacement, and an actor is currently wearing the same outfit that was distributed to them last time, we want to revert whatever outfit was in previous replacement #ifndef NDEBUG logger::info("\tReverting Outfit Replacement for {}", *actor); logger::info("\t\t{:R}", replacement); @@ -246,22 +308,10 @@ namespace Outfits if (revertedCount) { logger::info("Reverted {} no longer existing Outfit Replacements", revertedCount); } -#endif - } - - bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) - { - actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that equipping works either way, so we are good :) - - // TODO: Implement the equipment solution from po3 to avoid crashes :) - // With that approach nothing will be saved in the save file again, as such we'll only need to make sure that whatever was NPC's default outfit will get equipped once replacement no longer available. - /* - if (const auto npc = actor->GetActorBase(); npc) { - npc->defaultOutfit = outfit; + if (updatedCount) { + logger::info("Updated {} existing Outfit Replacements", updatedCount); } - force_equip_outfit(actor, actor->GetActorBase()); - */ - return true; +#endif } void Manager::Save(SKSE::SerializationInterface* interface) diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index 2c634d6..4cdf01b 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -2,6 +2,8 @@ #include "LookupConfigs.h" #include "LookupForms.h" #include "PCLevelMultManager.h" +#include "OutfitManager.h" +#include "DeathDistribution.h" bool shouldLookupForms{ false }; bool shouldLogErrors{ false }; @@ -38,9 +40,12 @@ void MessageHandler(SKSE::MessagingInterface::Message* a_message) case SKSE::MessagingInterface::kDataLoaded: { if (shouldDistribute = Lookup::LookupForms(); shouldDistribute) { + DeathDistribution::Manager::Register(); Distribute::Setup(); } + Outfits::Manager::Register(); // Regardless of distribution, we register outfits manager to handle save/load events. It should revert all previously distributed outfits even if no _DISTR files are present. + if (shouldLogErrors) { const auto error = std::format("[SPID] Errors found when reading configs. Check {}.log in {} for more info\n", Version::PROJECT, SKSE::log::log_directory()->string()); RE::ConsoleLog::GetSingleton()->Print(error.c_str());