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.");
}
}