Skip to content

Commit

Permalink
Fixed the crash. Improved merging of replacements.
Browse files Browse the repository at this point in the history
- Implemented equipping outfits logic in a semi-manual way. (still baking it into saves)
- Added more helpful message when reverting corrupted outfits or encounter corrupted actors.
  • Loading branch information
adya committed Dec 14, 2024
1 parent d7f30bc commit f116194
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 50 deletions.
14 changes: 11 additions & 3 deletions SPID/include/OutfitManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ namespace Outfits
/// <returns>True if the outfit was successfully set, false otherwise</returns>
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.
Expand All @@ -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<Outfits::Manager::OutfitReplacement>;
Expand Down Expand Up @@ -110,6 +116,8 @@ struct fmt::formatter<Outfits::Manager::OutfitReplacement>
} 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");
}
Expand Down
5 changes: 0 additions & 5 deletions SPID/src/DistributeManager.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#include "DistributeManager.h"
#include "DeathDistribution.h"
#include "Distribute.h"
#include "DistributePCLevelMult.h"
#include "OutfitManager.h"

namespace Distribute
{
Expand Down Expand Up @@ -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<decltype(thunk)> func;
Expand Down Expand Up @@ -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();
Expand Down
134 changes: 92 additions & 42 deletions SPID/src/OutfitManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<T>(id); form) {
output = form;
Expand All @@ -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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<RE::TESObjectARMO>()) {
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
Expand Down Expand Up @@ -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<RE::TESObjectARMO>()) {
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<RE::ExtraOutfitItem>() : 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
Expand All @@ -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<RE::Actor>(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);
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions SPID/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit f116194

Please sign in to comment.