Skip to content

Commit

Permalink
Implemented correct reverting of outfits that are no longer distributed.
Browse files Browse the repository at this point in the history
This restores proper randomization of outfits logic where outfits are reset during load if SPID didn't distribute the same outfit in this game session.
  • Loading branch information
adya committed Jul 15, 2024
1 parent 305907e commit d89a356
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 140 deletions.
2 changes: 1 addition & 1 deletion SPID/include/DistributeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
77 changes: 46 additions & 31 deletions SPID/include/OutfitManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,70 @@ namespace Outfits
public:
static void Register();

bool IsLoadingGame() const
{
return isLoadingGame;
}

void StartLoadingGame()
{
isLoadingGame = true;
}

void FinishLoadingGame()
{
isLoadingGame = false;
}

/// <summary>
/// Sets given outfit as default outfit for the actor.
///
/// This method also makes sure to properly remove previously distributed outfit.
/// </summary>
/// <param name="Actor">Target Actor for whom the outfit will be set.</param>
/// <param name="Outfit">A new outfit to set as the default.</param>
bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*);
bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites);

/// <summary>
/// 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.
/// </summary>
/// <param name="Actor">Target Actor for whom the outfit should be reset.</param>
/// <param name="previous">Previously loaded outfit that needs to be unequipped.</param>
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<RE::Actor*, RE::BGSOutfit*> outfits;
friend fmt::formatter<Outfits::Manager::OutfitReplacement>;

std::unordered_map<RE::Actor*, OutfitReplacement> replacements;
};
}

template <>
struct fmt::formatter<Outfits::Manager::OutfitReplacement>
{
template <class ParseContext>
constexpr auto parse(ParseContext& a_ctx)
{
return a_ctx.begin();
}

template <class FormatContext>
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");
}
}
};
10 changes: 7 additions & 3 deletions SPID/src/Distribute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,19 @@ namespace Distribute

if (!for_first_form<RE::BGSOutfit>(
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<RE::BGSOutfit>(
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);

Expand Down
14 changes: 10 additions & 4 deletions SPID/src/DistributeManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
}
}
Expand All @@ -66,8 +72,8 @@ namespace Distribute

void Install()
{
stl::write_vfunc<RE::Character, ShouldBackgroundClone>();
stl::write_vfunc<RE::Character, InitLoadGame>();
stl::write_vfunc<RE::Character, ShouldBackgroundClone>();

logger::info("Installed actor load hooks");
}
Expand Down
Loading

0 comments on commit d89a356

Please sign in to comment.