From 36dbe9ed43a75f4ba1d2e4a19460690f2fa8de5b Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 17:48:44 +0300 Subject: [PATCH 01/43] Used SetOutfit functions to distribute outfits. This does the same as Papyrus SetOutfit function. So should be safer? --- SPID/src/Distribute.cpp | 15 +++------------ vcpkg.json | 1 + 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 471ecb5..7352625 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -104,22 +104,13 @@ namespace Distribute for_each_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - if (npc->defaultOutfit != a_outfit && (allowOverwrites || !npc->HasKeyword(processedOutfit))) { - npc->AddKeyword(processedOutfit); - npc->defaultOutfit = a_outfit; - return true; - } - return false; + return npcData.GetActor()->SetDefaultOutfit(a_outfit, false); // Having true here causes infinite loading. It seems that it works either way. }, accumulatedForms); - + for_each_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { - if (npc->sleepOutfit != a_outfit) { - npc->sleepOutfit = a_outfit; - return true; - } - return false; + return npcData.GetActor()->SetSleepOutfit(a_outfit, false); }, accumulatedForms); diff --git a/vcpkg.json b/vcpkg.json index b483153..951f852 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,6 +1,7 @@ { "name": "spid-common", "version-string": "1", + "builtin-baseline": "c4467cb686f92671f0172aa8299a77d908175b4e", "dependencies": [ "clib-util", "mergemapper", From da6d4871fb7b78682c936a3345f380709d79b50d Mon Sep 17 00:00:00 2001 From: adya Date: Fri, 5 Jul 2024 15:06:13 +0000 Subject: [PATCH 02/43] maintenance --- SPID/src/Distribute.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 7352625..43f4138 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -104,10 +104,10 @@ namespace Distribute for_each_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - return npcData.GetActor()->SetDefaultOutfit(a_outfit, false); // Having true here causes infinite loading. It seems that it works either way. + return npcData.GetActor()->SetDefaultOutfit(a_outfit, false); // Having true here causes infinite loading. It seems that it works either way. }, accumulatedForms); - + for_each_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { return npcData.GetActor()->SetSleepOutfit(a_outfit, false); From 79beae63764697f79be02b981fb6700072440970 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 19:06:23 +0300 Subject: [PATCH 03/43] Updated CommonLibSSE submodule. --- extern/CommonLibSSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/CommonLibSSE b/extern/CommonLibSSE index 26015a0..af3e8a1 160000 --- a/extern/CommonLibSSE +++ b/extern/CommonLibSSE @@ -1 +1 @@ -Subproject commit 26015a042947ef3787eac754d559e9653c664a2c +Subproject commit af3e8a1f46e57b114d6a97dc38f6afd6d4ffb2d7 From 98401321f74f7954ba1c8b4273cdfebb7a1366bc Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 19:09:35 +0300 Subject: [PATCH 04/43] Disabled maintenance on tags. --- .github/workflows/maintenance.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index 0c3213c..cd7fe84 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -1,6 +1,9 @@ name: Scripted maintenance -on: push +on: + push: + tags-ignore: + - '**' jobs: maintenance: From 5b5f7f9597b42cbb4e54683dbd59e4c06efcecc7 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 19:14:49 +0300 Subject: [PATCH 05/43] Updated vcpkg. --- .github/workflows/main.yml | 2 +- vcpkg.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4646790..266ffbd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,4 +30,4 @@ jobs: PUBLISH_MOD_CHANGELOG_FILE: "FOMOD/changelog.txt" PUBLISH_MOD_DESCRIPTION_FILE: "FOMOD/description.txt" PUBLISH_ARCHIVE_TYPE: '7z' - VCPKG_COMMIT_ID: '9854d1d92200d81dde189e53b64c9ba6a305dc9f' + VCPKG_COMMIT_ID: '49ac2134b31b95b0ddf29d56873dcd24392691df' diff --git a/vcpkg.json b/vcpkg.json index 951f852..f240378 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "spid-common", "version-string": "1", - "builtin-baseline": "c4467cb686f92671f0172aa8299a77d908175b4e", + "builtin-baseline": "49ac2134b31b95b0ddf29d56873dcd24392691df", "dependencies": [ "clib-util", "mergemapper", From 06d6f8fffc72a937255a44f6c9ace46f204bdb2e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 19:18:40 +0300 Subject: [PATCH 06/43] Set maintenance to run only on branches. Previous change made GitHub ignore both branches and tags. --- .github/workflows/maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index cd7fe84..283febb 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -2,7 +2,7 @@ name: Scripted maintenance on: push: - tags-ignore: + branches: - '**' jobs: From b51341a77de5d746b3ea5a755664c921ce0c0eec Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 5 Jul 2024 19:26:35 +0300 Subject: [PATCH 07/43] Set version. --- SPID/CMakeLists.txt | 2 +- SPID/vcpkg.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/CMakeLists.txt b/SPID/CMakeLists.txt index f523098..15c61ed 100644 --- a/SPID/CMakeLists.txt +++ b/SPID/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) set(NAME "po3_SpellPerkItemDistributor" CACHE STRING "") -set(VERSION 7.1.3 CACHE STRING "") +set(VERSION 7.2.0 CACHE STRING "") set(AE_VERSION 1) set(VR_VERSION 1) diff --git a/SPID/vcpkg.json b/SPID/vcpkg.json index f9787c1..116ad0f 100644 --- a/SPID/vcpkg.json +++ b/SPID/vcpkg.json @@ -1,6 +1,6 @@ { "name": "spid", - "version-string": "7.1.3", + "version-string": "7.2.0", "description": "Spell Perk Item Distributor", "homepage": "https://github.com/powerof3/Spell-Perk-Item-Distributor", "license": "MIT", From 8cf3570bbe77ed467bb33a12c9e0bbdf403044d2 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 6 Jul 2024 13:05:28 +0300 Subject: [PATCH 08/43] Improved Death Distribution. - NPCs that Start Dead will get On Death Distribution. - NPCs that die during the game will work the same. - Death Distribution will be performed once for each NPC during game session, so that they won't re-equip new things. --- SPID/include/DeathDistribution.h | 9 ++++ SPID/include/Distribute.h | 9 +++- SPID/include/LookupConfigs.h | 2 - SPID/include/LookupNPC.h | 2 + SPID/src/DeathDistribution.cpp | 81 ++++++++++++++++++++---------- SPID/src/Distribute.cpp | 16 ++++-- SPID/src/DistributeManager.cpp | 2 +- SPID/src/DistributePCLevelMult.cpp | 2 +- SPID/src/LookupNPC.cpp | 10 ++++ 9 files changed, 96 insertions(+), 37 deletions(-) diff --git a/SPID/include/DeathDistribution.h b/SPID/include/DeathDistribution.h index f5f172b..10f925b 100644 --- a/SPID/include/DeathDistribution.h +++ b/SPID/include/DeathDistribution.h @@ -1,5 +1,6 @@ #pragma once #include "FormData.h" +#include "LookupNPC.h" namespace DeathDistribution { @@ -32,6 +33,14 @@ namespace DeathDistribution bool IsEmpty(); + /// + /// Performs Death Distribution on a given NPC. + /// + /// NPC passed to this method must be Dead in order to be processed. + /// + /// + void Distribute(NPCData&); + private: Distributables spells{ RECORD::kSpell }; Distributables perks{ RECORD::kPerk }; diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 3e9e753..fc361d8 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -231,6 +231,13 @@ namespace Distribute /// If true, overwritable forms (like Outfits) will be to overwrite last distributed form on NPC. /// An optional pointer to a set that will accumulate all distributed forms. void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms = nullptr); - void Distribute(NPCData& npcData, const PCLevelMult::Input& input); + + /// + /// Invokes appropriate distribution for given NPC. + /// + /// When NPC is dead a Death Distribution will be invoked, otherwise a normal distribution takes place. + /// + /// General information about NPC that is being processed. + /// Flag indicating that distribution is invoked by a leveling event and only entries with LevelFilters needs to be processed. void Distribute(NPCData& npcData, bool onlyLeveledEntries); } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 84e42f2..bab1770 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -17,7 +17,6 @@ namespace RECORD kPackage, kOutfit, kKeyword, - kDeathItem, kFaction, kSleepOutfit, kSkin, @@ -37,7 +36,6 @@ namespace RECORD "Package"sv, "Outfit"sv, "Keyword"sv, - "DeathItem"sv, "Faction"sv, "SleepOutfit"sv, "Skin"sv diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index b01bfff..8833521 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -30,6 +30,8 @@ namespace NPC [[nodiscard]] bool IsChild() const; [[nodiscard]] bool IsLeveled() const; [[nodiscard]] bool IsTeammate() const; + [[nodiscard]] bool IsDead() const; + [[nodiscard]] bool StartsDead() const; [[nodiscard]] RE::TESRace* GetRace() const; diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index e67239f..6f932db 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -181,6 +181,8 @@ namespace DeathDistribution #pragma region Distribution + static RE::BGSKeyword* SPID_Dead = nullptr; + void Manager::Register() { if (INI::deathConfigs.empty()) { @@ -191,6 +193,56 @@ namespace DeathDistribution scripts->AddEventSink(GetSingleton()); logger::info("Registered for {}", typeid(RE::TESDeathEvent).name()); } + + // Create tag keywords + if (const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType()) { + if (SPID_Dead = factory->Create(); SPID_Dead) { + SPID_Dead->formEditorID = "SPID_Dead"; + } + } + + assert(SPID_Dead); + } + + void Manager::Distribute(NPCData& data) + { + assert(data.IsDead()); + + // We mark NPCs that were processed by Death Distribution with SPID_Dead keyword, + // to ensure that NPCs who received Death Distribution once won't get another Death Distribution + // (which might happen if cell or game is reloaded with dead NPC laying there) + if (data.GetNPC()->HasKeyword(SPID_Dead)) + return; + + data.GetNPC()->AddKeyword(SPID_Dead); + + const auto input = PCLevelMult::Input{ data.GetActor(), data.GetNPC(), false }; + + DistributedForms distributedForms{}; + + Forms::DistributionSet entries{ + spells.GetForms(), + perks.GetForms(), + items.GetForms(), + shouts.GetForms(), + levSpells.GetForms(), + packages.GetForms(), + outfits.GetForms(), + keywords.GetForms(), + factions.GetForms(), + sleepOutfits.GetForms(), + skins.GetForms() + }; + + Distribute::Distribute(data, input, entries, false, &distributedForms); + + if (!distributedForms.empty()) { + LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(LinkedDistribution::kDeath, distributedForms, [&](Forms::DistributionSet& set) { + Distribute::Distribute(data, input, set, true, &distributedForms); + }); + } + + // TODO: Log death distribution } RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) @@ -203,33 +255,8 @@ namespace DeathDistribution const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - auto npcData = NPCData(actor, npc); - const auto input = PCLevelMult::Input{ actor, npc, false }; - - DistributedForms distributedForms{}; - - Forms::DistributionSet entries{ - spells.GetForms(), - perks.GetForms(), - items.GetForms(), - shouts.GetForms(), - levSpells.GetForms(), - packages.GetForms(), - outfits.GetForms(), - keywords.GetForms(), - factions.GetForms(), - sleepOutfits.GetForms(), - skins.GetForms() - }; - - Distribute::Distribute(npcData, input, entries, false, &distributedForms); - // TODO: We can now log per-NPC distributed forms. - - if (!distributedForms.empty()) { - LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(LinkedDistribution::kDeath, distributedForms, [&](Forms::DistributionSet& set) { - Distribute::Distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. - }); - } + auto npcData = NPCData(actor, npc); + Distribute(npcData); } } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 43f4138..9e14268 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -2,6 +2,7 @@ #include "DistributeManager.h" #include "LinkedDistribution.h" +#include "DeathDistribution.h" namespace Distribute { @@ -133,9 +134,10 @@ namespace Distribute void Distribute(NPCData& npcData, const PCLevelMult::Input& input) { - if (input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(input)) { + assert(!npcData.IsDead()); // Dead NPCs are handled by Death Distribution. + + if (input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(input)) return; - } Forms::DistributionSet entries{ Forms::spells.GetForms(input.onlyPlayerLevelEntries), @@ -159,14 +161,18 @@ namespace Distribute if (!distributedForms.empty()) { // TODO: This only does one-level linking. So that linked entries won't trigger another level of distribution. LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(LinkedDistribution::kRegular, distributedForms, [&](Forms::DistributionSet& set) { - Distribute(npcData, input, set, true, nullptr); // TODO: Accumulate forms here? to log what was distributed. + Distribute(npcData, input, set, true, &distributedForms); }); } } void Distribute(NPCData& npcData, bool onlyLeveledEntries) { - const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; - Distribute(npcData, input); + if (npcData.IsDead()) { // Perform Death distribution for NPCs that are loaded dead, but haven't been processed by on death distribution. + DeathDistribution::Manager::GetSingleton()->Distribute(npcData); + } else { + const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; + Distribute(npcData, input); + } } } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 644aff2..970b574 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -134,7 +134,7 @@ namespace Distribute logger::info("{:*^50}", "MAIN MENU DISTRIBUTION"); ForEachDistributable([&](Distributables
& a_distributable) { - if (a_distributable && a_distributable.GetType() != RECORD::kDeathItem) { + if (a_distributable) { logger::info("{}", RECORD::GetTypeName(a_distributable.GetType())); auto& forms = a_distributable.GetForms(); diff --git a/SPID/src/DistributePCLevelMult.cpp b/SPID/src/DistributePCLevelMult.cpp index 2e8093d..82c63d7 100644 --- a/SPID/src/DistributePCLevelMult.cpp +++ b/SPID/src/DistributePCLevelMult.cpp @@ -98,7 +98,7 @@ namespace Distribute::PlayerLeveledActor if (!pcLevelMultManager->FindDistributedEntry(input)) { //start distribution of leveled entries for first time auto npcData = NPCData(a_this, npc); - Distribute(npcData, input); + Distribute(npcData, true); } else { //handle redistribution pcLevelMultManager->ForEachDistributedEntry(input, true, [&](RE::FormType a_formType, const Set& a_formIDSet) { diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 8d2d0d9..23ba10e 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -226,6 +226,16 @@ namespace NPC return teammate; } + bool Data::IsDead() const + { + return actor && actor->IsDead() || StartsDead(); + } + + bool Data::StartsDead() const + { + return actor && (actor->formFlags & RE::Actor::RecordFlags::kStartsDead); + } + RE::TESRace* Data::GetRace() const { return race; From 20271161a2f30a2b70cce6c4dea1702aa7ed504d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Jul 2024 13:55:37 +0300 Subject: [PATCH 09/43] Added "Starts Dead" trait. Allowed both regular and On Death distribution to work on initially dead NPCs. --- SPID/include/Defs.h | 1 + SPID/include/LookupConfigs.h | 8 +++++++- SPID/src/Distribute.cpp | 13 +++++-------- SPID/src/LookupFilters.cpp | 3 +++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index d3a21a8..4ab2951 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -91,6 +91,7 @@ struct Traits std::optional child{}; std::optional leveled{}; std::optional teammate{}; + std::optional startsDead{}; }; using Path = std::string; diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index bab1770..2e98b73 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -481,6 +481,12 @@ namespace Distribution::INI case "-T"_h: data.traits.teammate = false; break; + case "D"_h: + data.traits.startsDead = true; + break; + case "-D"_h: + data.traits.startsDead = false; + break; default: break; } @@ -514,7 +520,7 @@ namespace Distribution::INI data.idxOrCount = RandomCount(count, count); // create the exact match range. } } - } catch (const std::exception& e) { + } catch (const std::exception&) { throw InvalidIndexOrCountException(entry); } } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 9e14268..e9ebd11 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -134,8 +134,6 @@ namespace Distribute void Distribute(NPCData& npcData, const PCLevelMult::Input& input) { - assert(!npcData.IsDead()); // Dead NPCs are handled by Death Distribution. - if (input.onlyPlayerLevelEntries && PCLevelMult::Manager::GetSingleton()->HasHitLevelCap(input)) return; @@ -156,7 +154,6 @@ namespace Distribute DistributedForms distributedForms{}; Distribute(npcData, input, entries, false, &distributedForms); - // TODO: We can now log per-NPC distributed forms. if (!distributedForms.empty()) { // TODO: This only does one-level linking. So that linked entries won't trigger another level of distribution. @@ -168,11 +165,11 @@ namespace Distribute void Distribute(NPCData& npcData, bool onlyLeveledEntries) { - if (npcData.IsDead()) { // Perform Death distribution for NPCs that are loaded dead, but haven't been processed by on death distribution. + const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; + Distribute(npcData, input); + + if (npcData.IsDead()) { // If NPC is already dead, perform the On Death Distribution. DeathDistribution::Manager::GetSingleton()->Distribute(npcData); - } else { - const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; - Distribute(npcData, input); - } + } } } diff --git a/SPID/src/LookupFilters.cpp b/SPID/src/LookupFilters.cpp index edc395b..c7fbfc3 100644 --- a/SPID/src/LookupFilters.cpp +++ b/SPID/src/LookupFilters.cpp @@ -166,6 +166,9 @@ namespace Filter if (traits.teammate && a_npcData.IsTeammate() != *traits.teammate) { return Result::kFail; } + if (traits.startsDead && a_npcData.StartsDead() != *traits.startsDead) { + return Result::kFail; + } return Result::kPass; } From dcfbc104e6a218cec81134c9eaa4dcb5a9185e1b Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Jul 2024 23:26:48 +0300 Subject: [PATCH 10/43] Added implicit formatting support for all Forms. --- SPID/include/Defs.h | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index 4ab2951..81972e2 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -155,3 +155,56 @@ inline std::ostream& operator<<(std::ostream& os, RE::TESForm* form) return os; } + +namespace fmt +{ + // Produces formatted strings for all Forms like this: + // DefaultAshPile2 "Ash Pile" [ACTI:00000022] + // defaultBlankTrigger [ACTI:00000F55] + template + struct formatter::value, char>> : fmt::formatter + { + template + constexpr auto parse(ParseContext& a_ctx) + { + return a_ctx.begin(); + } + + template + constexpr auto format(const Form& form, FormatContext& a_ctx) + { + const auto name = std::string(form.GetName()); + const auto edid = editorID::get_editorID(&form); + if (name.empty() && edid.empty()) { + return fmt::format_to(a_ctx.out(), "[{}:{:08X}]", form.GetFormType(), form.formID); + } else if (name.empty()) { + return fmt::format_to(a_ctx.out(), "{} [{}:{:08X}]", edid, form.GetFormType(), form.formID); + } else if (edid.empty()) { + return fmt::format_to(a_ctx.out(), "{:?} [{}:{:08X}]", name, form.GetFormType(), form.formID); + } + return fmt::format_to(a_ctx.out(), "{} {:?} [{}:{:08X}]", edid, name, form.GetFormType(), form.formID); + } + }; + + // Does the same as generic formatter, but also includes a Base NPC info. + template <> + struct formatter + { + template + constexpr auto parse(ParseContext& a_ctx) + { + return a_ctx.begin(); + } + + template + constexpr auto format(const RE::Actor& actor, FormatContext& a_ctx) + { + const auto& form = static_cast(actor); + if (auto npc = actor.GetActorBase(); npc) { + return fmt::format_to(a_ctx.out(), "{} (Base: {})", form, *npc); + } + return fmt::format_to(a_ctx.out(), "{}", form); + } + }; + +} From 6c22c3f28f8f925b0a1d1c56990ebc7bfcaa77f8 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 7 Jul 2024 23:28:57 +0300 Subject: [PATCH 11/43] Added an Outfit Manager. A dedicated manager that will track distributed outfits with SKSE co-save and will be able to revert no longer available outfits. This will allow SPID to preserve randomized outfits behavior. --- SPID/cmake/headerlist.cmake | 1 + SPID/cmake/sourcelist.cmake | 1 + SPID/include/Distribute.h | 8 +- SPID/include/OutfitManager.h | 58 ++++++++++ SPID/src/Distribute.cpp | 15 ++- SPID/src/DistributeManager.cpp | 2 + SPID/src/OutfitManager.cpp | 205 +++++++++++++++++++++++++++++++++ 7 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 SPID/include/OutfitManager.h create mode 100644 SPID/src/OutfitManager.cpp diff --git a/SPID/cmake/headerlist.cmake b/SPID/cmake/headerlist.cmake index bc3c89f..da245c7 100644 --- a/SPID/cmake/headerlist.cmake +++ b/SPID/cmake/headerlist.cmake @@ -15,6 +15,7 @@ set(headers ${headers} include/LookupFilters.h include/LookupForms.h include/LookupNPC.h + include/OutfitManager.h include/PCH.h include/PCLevelMultManager.h include/Parser.h diff --git a/SPID/cmake/sourcelist.cmake b/SPID/cmake/sourcelist.cmake index fb8ad74..f41241e 100644 --- a/SPID/cmake/sourcelist.cmake +++ b/SPID/cmake/sourcelist.cmake @@ -13,6 +13,7 @@ set(sources ${sources} src/LookupFilters.cpp src/LookupForms.cpp src/LookupNPC.cpp + src/OutfitManager.cpp src/PCH.cpp src/PCLevelMultManager.cpp src/main.cpp diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index fc361d8..a67a11a 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -98,22 +98,24 @@ namespace Distribute #pragma region Outfits, Sleep Outfits, Skins template - void for_each_form( + bool for_first_form( const NPCData& a_npcData, Forms::DataVec& forms, const PCLevelMult::Input& a_input, std::function a_callback, DistributedForms* accumulatedForms = nullptr) { - for (auto& formData : forms) { // Vector is reversed in FinishLookupForms + for (auto& formData : forms) { if (!a_npcData.HasMutuallyExclusiveForm(formData.form) && detail::passed_filters(a_npcData, a_input, formData) && a_callback(formData.form)) { if (accumulatedForms) { accumulatedForms->insert({ formData.form, formData.path }); } ++formData.npcCount; - break; + return true; } } + + return false; } #pragma endregion diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h new file mode 100644 index 0000000..b2b4189 --- /dev/null +++ b/SPID/include/OutfitManager.h @@ -0,0 +1,58 @@ +#pragma once + +namespace Outfits +{ + class Manager : public ISingleton + { + 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. + /// + /// This method also makes sure to properly remove previously distributed outfit. + /// + /// Target Actor for whom the outfit will be set. + /// A new outfit to set as the default. + bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + + /// + /// 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 :) + /// + /// 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. + /// + /// Target Actor for whom the outfit should be reset. + void ResetDefaultOutfit(RE::Actor*); + + private: + static void Load(SKSE::SerializationInterface*); + static void Save(SKSE::SerializationInterface*); + static void Revert(SKSE::SerializationInterface*); + + bool isLoadingGame = false; + + /// Map of Actor -> Outfit associations. + std::unordered_map outfits; + }; +} diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index e9ebd11..75e193b 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -1,6 +1,7 @@ #include "Distribute.h" #include "DistributeManager.h" +#include "OutfitManager.h" #include "LinkedDistribution.h" #include "DeathDistribution.h" @@ -103,13 +104,15 @@ namespace Distribute }, accumulatedForms); - for_each_form( + if (!for_first_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - return npcData.GetActor()->SetDefaultOutfit(a_outfit, false); // Having true here causes infinite loading. It seems that it works either way. + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit); }, - accumulatedForms); - - for_each_form( + accumulatedForms)) { + return Outfits::Manager::GetSingleton()->ResetDefaultOutfit(npcData.GetActor()); + } + + for_first_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { return npcData.GetActor()->SetSleepOutfit(a_outfit, false); }, @@ -121,7 +124,7 @@ namespace Distribute }, accumulatedForms); - for_each_form( + for_first_form( npcData, forms.skins, input, [&](auto* a_skin) { if (npc->skin != a_skin) { npc->skin = a_skin; diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 970b574..6973631 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -1,4 +1,5 @@ #include "DistributeManager.h" +#include "OutfitManager.h" #include "DeathDistribution.h" #include "Distribute.h" #include "DistributePCLevelMult.h" @@ -92,6 +93,7 @@ namespace Distribute Event::Manager::Register(); PCLevelMult::Manager::Register(); DeathDistribution::Manager::Register(); + Outfits::Manager::Register(); DoInitialDistribution(); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp new file mode 100644 index 0000000..19f4490 --- /dev/null +++ b/SPID/src/OutfitManager.cpp @@ -0,0 +1,205 @@ +#include "OutfitManager.h" + +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) + { + return a_interface->WriteRecordData(&data, sizeof(T)); + } + + template <> + bool Write(SKSE::SerializationInterface* a_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)); + } + + template + bool Read(SKSE::SerializationInterface* a_interface, T& result) + { + return a_interface->ReadRecordData(&result, sizeof(T)); + } + + template <> + bool Read(SKSE::SerializationInterface* a_interface, std::string& result) + { + std::size_t size = 0; + if (!a_interface->ReadRecordData(size)) { + return false; + } + if (size > 0) { + result.resize(size); + if (!a_interface->ReadRecordData(result.data(), static_cast(size))) { + return false; + } + } else { + result = ""; + } + return true; + } + } + + namespace Data + { + constexpr std::uint32_t recordType = 'OTFT'; + + + bool Load(SKSE::SerializationInterface* a_interface, std::pair& data) + { + const bool result = details::Read(a_interface, data.first) && + details::Read(a_interface, data.second); + + if (!result) { + logger::warn("Failed to load outfit with FormID [0x{:X}] for NPC with FormID [0x{:X}]", data.second, data.first); + 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); + return false; + } + + if (!a_interface->ResolveFormID(data.second, data.second)) { + return false; + } + + return true; + } + + bool Save(SKSE::SerializationInterface* a_interface, const std::pair& data) + { + if (!a_interface->OpenRecord(recordType, serializationVersion)) { + return false; + } + + return details::Write(a_interface, data.first->formID) && + details::Write(a_interface, data.second->formID); + } + } + + void Manager::Register() + { + const auto serializationInterface = SKSE::GetSerializationInterface(); + serializationInterface->SetUniqueID(serializationKey); + serializationInterface->SetSaveCallback(Save); + serializationInterface->SetLoadCallback(Load); + serializationInterface->SetRevertCallback(Revert); + } + + bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) + { + if (!actor || !outfit) { + return false; + } + + auto* npc = actor->GetActorBase(); + + if (auto previous = outfits.find(actor); previous != outfits.end()) { + if (previous->second == outfit) { + return true; // if the new default outfit is the same as previous one, we can skip the rest, because it is already equipped. + } + npc->defaultOutfit = previous->second; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. + } else if (outfit == npc->defaultOutfit) { + return true; // if there is no previously distributed outfit and the new default outfit is the same as the original default outfit, we can also skip. + } + + // Otherwise we want to set the new default outfit and add it to our tracker map. + if (actor->SetDefaultOutfit(outfit, false)) { // Having true here causes infinite loading. It seems that it works either way. + outfits[actor] = outfit; + return true; + } + + return false; + } + + void Manager::ResetDefaultOutfit(RE::Actor* actor) + { + if (!actor) { + return; + } + + 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. + + if (auto previous = outfits.find(actor); previous != outfits.end()) { + auto previousOutfit = previous->second; + outfits.erase(previous); // remove cached outfit as it's no longer needed. + if (previousOutfit == npc->defaultOutfit) { + return; + } + npc->defaultOutfit = previous->second; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. + } + + actor->SetDefaultOutfit(restore, false); // Having true here causes infinite loading. It seems that it works either way. + } + + void Manager::Load(SKSE::SerializationInterface* a_interface) + { + logger::info("{:*^30}", "LOADING"); + + auto* manager = Manager::GetSingleton(); + std::uint32_t loadedCount = 0; + + auto& outfits = manager->outfits; + + std::uint32_t type, version, length; + outfits.clear(); + 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) { +#ifndef NDEBUG + logger::info("\tLoaded outfit {} for actor {}", *outfit, *actor); +#endif + outfits[actor] = outfit; + ++loadedCount; + } + } + } + } + } + + logger::info("Loaded {} distributed outfits", loadedCount); + } + + void Manager::Save(SKSE::SerializationInterface* a_interface) + { + logger::info("{:*^30}", "SAVING"); + + auto outfits = Manager::GetSingleton()->outfits; + + 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); + continue; + } +#ifndef NDEBUG + logger::info("\tSaved outfit {} for actor {}", *data.second, *data.first->GetActorBase()); +#endif + ++savedCount; + } + + logger::info("Saved {} names", savedCount); + } + + void Manager::Revert(SKSE::SerializationInterface*) + { + logger::info("{:*^30}", "REVERTING"); + Manager::GetSingleton()->outfits.clear(); + logger::info("\tOutfits cache has been cleared."); + } +} From 305907ea2e0eaac82f078f1958019da1ff6aea5b Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 9 Jul 2024 00:27:03 +0300 Subject: [PATCH 12/43] Desperate attempts to manage outfits :) This is bad, going back to the original idea of equipping stuff manually and retroactively fixing things. Also, added basic logging for distribution. --- SPID/include/Distribute.h | 2 + SPID/include/LookupConfigs.h | 1 + SPID/include/OutfitManager.h | 5 ++- SPID/src/DeathDistribution.cpp | 1 + SPID/src/Distribute.cpp | 21 ++++++++- SPID/src/DistributeManager.cpp | 3 +- SPID/src/OutfitManager.cpp | 78 +++++++++++++++++++++++++--------- 7 files changed, 87 insertions(+), 24 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index a67a11a..1306db0 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -242,4 +242,6 @@ namespace Distribute /// General information about NPC that is being processed. /// Flag indicating that distribution is invoked by a leveling event and only entries with LevelFilters needs to be processed. void Distribute(NPCData& npcData, bool onlyLeveledEntries); + + void LogDistribution(const DistributedForms& forms, NPCData& npcData); } diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 2e98b73..28c5ddf 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -1,4 +1,5 @@ #pragma once +#include "RE/F/FormTypes.h" namespace RECORD { diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index b2b4189..787ebe7 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -43,7 +43,8 @@ namespace Outfits /// and properly removes it from the NPC, then restores defaultOutfit that was used before the distribution. /// /// Target Actor for whom the outfit should be reset. - void ResetDefaultOutfit(RE::Actor*); + /// Previously loaded outfit that needs to be unequipped. + void ResetDefaultOutfit(RE::Actor*, RE::BGSOutfit* previous); private: static void Load(SKSE::SerializationInterface*); @@ -52,6 +53,8 @@ namespace Outfits bool isLoadingGame = false; + void ApplyDefaultOutfit(RE::Actor*); + /// Map of Actor -> Outfit associations. std::unordered_map outfits; }; diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 6f932db..1b4bc07 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -242,6 +242,7 @@ namespace DeathDistribution }); } + Distribute::LogDistribution(distributedForms, data); // TODO: Log death distribution } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 75e193b..3b3075f 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -109,7 +109,7 @@ namespace Distribute return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit); }, accumulatedForms)) { - return Outfits::Manager::GetSingleton()->ResetDefaultOutfit(npcData.GetActor()); + Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), npcData.GetNPC()->defaultOutfit); } for_first_form( @@ -164,6 +164,8 @@ namespace Distribute Distribute(npcData, input, set, true, &distributedForms); }); } + + LogDistribution(distributedForms, npcData); } void Distribute(NPCData& npcData, bool onlyLeveledEntries) @@ -175,4 +177,21 @@ namespace Distribute DeathDistribution::Manager::GetSingleton()->Distribute(npcData); } } + + void LogDistribution(const DistributedForms& forms, NPCData& npcData) + { + std::map> results; + + for (const auto& form : forms) { + results[RE::FormTypeToString(form.first->GetFormType())].push_back(form); + } + + logger::info("Distribution for {}", *npcData.GetActor()); + for (const auto& pair : results) { + logger::info("\t{}", pair.first); + for (const auto& form : pair.second) { + logger::info("\t\t{} @ {}", *form.first, form.second); + } + } + } } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 6973631..2fb2b13 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -95,7 +95,8 @@ namespace Distribute DeathDistribution::Manager::Register(); Outfits::Manager::Register(); - DoInitialDistribution(); + // TODO: No initial distribution. Check Packages distribution and see if those work as intended. + //DoInitialDistribution(); // Clear logger's buffer to free some memory :) buffered_logger::clear(); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 19f4490..c280b68 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -100,27 +100,25 @@ namespace Outfits return false; } - auto* npc = actor->GetActorBase(); - if (auto previous = outfits.find(actor); previous != outfits.end()) { - if (previous->second == outfit) { - return true; // if the new default outfit is the same as previous one, we can skip the rest, because it is already equipped. - } - npc->defaultOutfit = previous->second; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. - } else if (outfit == npc->defaultOutfit) { - return true; // if there is no previously distributed outfit and the new default outfit is the same as the original default outfit, we can also skip. - } - - // Otherwise we want to set the new default outfit and add it to our tracker map. - if (actor->SetDefaultOutfit(outfit, false)) { // Having true here causes infinite loading. It seems that it works either way. + return false; + } else { outfits[actor] = outfit; return true; } + } - return false; + void Manager::ApplyDefaultOutfit(RE::Actor* actor) { + auto* npc = actor->GetActorBase(); + + 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. + } + } } - void Manager::ResetDefaultOutfit(RE::Actor* actor) + void Manager::ResetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* previous) { if (!actor) { return; @@ -129,13 +127,13 @@ namespace Outfits 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. - if (auto previous = outfits.find(actor); previous != outfits.end()) { - auto previousOutfit = previous->second; - outfits.erase(previous); // remove cached outfit as it's no longer needed. + 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->second; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. + npc->defaultOutfit = previous; // restore reference to the previous default outfit, so that actor->SetDefaultOutfit would properly clean up. } actor->SetDefaultOutfit(restore, false); // Having true here causes infinite loading. It seems that it works either way. @@ -148,10 +146,11 @@ namespace Outfits auto* manager = Manager::GetSingleton(); std::uint32_t loadedCount = 0; - auto& outfits = manager->outfits; + std::unordered_map cachedOutfits; + auto& outfits = manager->outfits; std::uint32_t type, version, length; - outfits.clear(); + bool definitionsChanged = false; while (a_interface->GetNextRecordInfo(type, version, length)) { if (type == Data::recordType) { @@ -162,7 +161,7 @@ namespace Outfits #ifndef NDEBUG logger::info("\tLoaded outfit {} for actor {}", *outfit, *actor); #endif - outfits[actor] = outfit; + cachedOutfits[actor] = outfit; ++loadedCount; } } @@ -171,6 +170,43 @@ namespace Outfits } 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()); + + std::unordered_set actors; + + std::set_union(newActors.begin(), newActors.end(), cachedActors.begin(), cachedActors.end(), std::inserter(actors, actors.begin())); + + 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); + } + } else { + manager->ApplyDefaultOutfit(actor); + } + } else { + if (const auto cit = cachedOutfits.find(actor); cit != cachedOutfits.end()) { + manager->ResetDefaultOutfit(actor, cit->second); + } + } + } + // 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. } void Manager::Save(SKSE::SerializationInterface* a_interface) From d89a356cf4717cf782075e133b343c705a3444f5 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 16 Jul 2024 00:03:41 +0300 Subject: [PATCH 13/43] Implemented correct reverting of outfits that are no longer distributed. 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. --- SPID/include/DistributeManager.h | 2 +- SPID/include/OutfitManager.h | 77 ++++++---- SPID/src/Distribute.cpp | 10 +- SPID/src/DistributeManager.cpp | 14 +- SPID/src/OutfitManager.cpp | 234 ++++++++++++++++++------------- 5 files changed, 197 insertions(+), 140 deletions(-) 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."); } } From 50858bd4d1995476630bc48517582e5ad41bdd95 Mon Sep 17 00:00:00 2001 From: adya Date: Mon, 15 Jul 2024 21:04:06 +0000 Subject: [PATCH 14/43] maintenance --- SPID/include/DeathDistribution.h | 2 +- SPID/include/Distribute.h | 4 ++-- SPID/include/DistributeManager.h | 2 +- SPID/include/OutfitManager.h | 9 +++++---- SPID/src/DeathDistribution.cpp | 2 +- SPID/src/Distribute.cpp | 16 ++++++++-------- SPID/src/DistributeManager.cpp | 4 ++-- SPID/src/OutfitManager.cpp | 30 +++++++++++++++--------------- 8 files changed, 35 insertions(+), 34 deletions(-) diff --git a/SPID/include/DeathDistribution.h b/SPID/include/DeathDistribution.h index 10f925b..fd9c100 100644 --- a/SPID/include/DeathDistribution.h +++ b/SPID/include/DeathDistribution.h @@ -35,7 +35,7 @@ namespace DeathDistribution /// /// Performs Death Distribution on a given NPC. - /// + /// /// NPC passed to this method must be Dead in order to be processed. /// /// diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 1306db0..2aae160 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -235,8 +235,8 @@ namespace Distribute void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms = nullptr); /// - /// Invokes appropriate distribution for given NPC. - /// + /// Invokes appropriate distribution for given NPC. + /// /// When NPC is dead a Death Distribution will be invoked, otherwise a normal distribution takes place. /// /// General information about NPC that is being processed. diff --git a/SPID/include/DistributeManager.h b/SPID/include/DistributeManager.h index 038443a..963f9e3 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 }; // TODO: If OutfitManager works out we won't need this keyword. + 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 a2e6ecf..f696da5 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -9,7 +9,7 @@ namespace Outfits /// /// Sets given outfit as default outfit for the actor. - /// + /// /// This method also makes sure to properly remove previously distributed outfit. /// /// Target Actor for whom the outfit will be set. @@ -18,7 +18,7 @@ namespace Outfits /// /// Indicates that given actor didn't receive any distributed outfit and will be using the original one. - /// + /// /// 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. /// void UseOriginalOutfit(RE::Actor*); @@ -39,7 +39,8 @@ namespace Outfits OutfitReplacement() = default; OutfitReplacement(RE::BGSOutfit* original) : original(original), distributed(nullptr) {} - OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed): original(original), distributed(distributed) {} + OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed) : + original(original), distributed(distributed) {} bool UsesOriginalOutfit() const { @@ -67,7 +68,7 @@ struct fmt::formatter { if (replacement.UsesOriginalOutfit()) { return fmt::format_to(a_ctx.out(), "NO REPLACEMENT (Uses {})", *replacement.original); - } else if (replacement.original && replacement.distributed){ + } 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/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 1b4bc07..345b383 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -209,7 +209,7 @@ namespace DeathDistribution assert(data.IsDead()); // We mark NPCs that were processed by Death Distribution with SPID_Dead keyword, - // to ensure that NPCs who received Death Distribution once won't get another Death Distribution + // to ensure that NPCs who received Death Distribution once won't get another Death Distribution // (which might happen if cell or game is reloaded with dead NPC laying there) if (data.GetNPC()->HasKeyword(SPID_Dead)) return; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 8c46a8d..846859e 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -1,9 +1,9 @@ #include "Distribute.h" +#include "DeathDistribution.h" #include "DistributeManager.h" -#include "OutfitManager.h" #include "LinkedDistribution.h" -#include "DeathDistribution.h" +#include "OutfitManager.h" namespace Distribute { @@ -105,13 +105,13 @@ namespace Distribute accumulatedForms); if (!for_first_form( - npcData, forms.outfits, input, [&](auto* a_outfit) { - return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites); - }, - accumulatedForms)) { + npcData, forms.outfits, input, [&](auto* a_outfit) { + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites); + }, + accumulatedForms)) { Outfits::Manager::GetSingleton()->UseOriginalOutfit(npcData.GetActor()); } - + for_first_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { if (npc->sleepOutfit != a_outfit) { @@ -179,7 +179,7 @@ namespace Distribute if (npcData.IsDead()) { // If NPC is already dead, perform the On Death Distribution. DeathDistribution::Manager::GetSingleton()->Distribute(npcData); - } + } } void LogDistribution(const DistributedForms& forms, NPCData& npcData) diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 3234c1b..3f2c018 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -1,8 +1,8 @@ #include "DistributeManager.h" -#include "OutfitManager.h" #include "DeathDistribution.h" #include "Distribute.h" #include "DistributePCLevelMult.h" +#include "OutfitManager.h" namespace Distribute { @@ -53,7 +53,7 @@ namespace Distribute 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()) { diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 0d16b2d..7b6226a 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -48,8 +48,8 @@ namespace Outfits namespace Data { constexpr std::uint32_t recordType = 'OTFT'; - - template + + template bool Load(SKSE::SerializationInterface* interface, T*& output) { RE::FormID id = 0; @@ -58,7 +58,7 @@ namespace Outfits return false; } - if (!id) { // If ID was 0 it means we don't have the outfit stored in this record. + if (!id) { // If ID was 0 it means we don't have the outfit stored in this record. output = nullptr; return true; } @@ -70,7 +70,7 @@ namespace Outfits if (const auto form = RE::TESForm::LookupByID(id); form) { output = form; return true; - } + } return false; } @@ -177,16 +177,16 @@ namespace Outfits { logger::info("{:*^30}", "LOADING"); - auto* manager = Manager::GetSingleton(); + auto* manager = Manager::GetSingleton(); std::unordered_map loadedReplacements; - auto& newReplacements = manager->replacements; - + auto& newReplacements = manager->replacements; + std::uint32_t type, version, length; while (a_interface->GetNextRecordInfo(type, version, length)) { if (type == Data::recordType) { - RE::Actor* actor; + RE::Actor* actor; RE::BGSOutfit* original; RE::BGSOutfit* distributed; if (Data::Load(a_interface, actor, original, distributed)) { @@ -213,12 +213,12 @@ namespace Outfits std::uint32_t revertedCount = 0; for (const auto& it : loadedReplacements) { - const auto& actor = it.first; + const auto& actor = it.first; const auto& replacement = it.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 + 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); @@ -227,12 +227,12 @@ namespace Outfits ++revertedCount; } } - } else { // If new replacement + } 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; + + } 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; } } From ac65d98021686004782c1e38abeef1a781f43d4c Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 19 Jul 2024 00:35:34 +0300 Subject: [PATCH 15/43] Handled deletion of temp actors. replacements are now tracked by actor formIDs. Less memory consumption, better handling of deleted actors. --- SPID/include/OutfitManager.h | 9 ++++-- SPID/src/DistributeManager.cpp | 2 +- SPID/src/OutfitManager.cpp | 50 ++++++++++++++++++++++++---------- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index f696da5..6c99feb 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -2,7 +2,9 @@ namespace Outfits { - class Manager : public ISingleton + class Manager : + public ISingleton, + public RE::BSTEventSink { public: static void Register(); @@ -23,6 +25,9 @@ namespace Outfits /// void UseOriginalOutfit(RE::Actor*); + protected: + RE::BSEventNotifyControl ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) override; + private: static void Load(SKSE::SerializationInterface*); static void Save(SKSE::SerializationInterface*); @@ -50,7 +55,7 @@ namespace Outfits friend fmt::formatter; - std::unordered_map replacements; + std::unordered_map replacements; }; } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 3f2c018..f706f46 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -184,7 +184,7 @@ namespace Distribute::Event { if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { scripts->AddEventSink(GetSingleton()); - logger::info("Registered for {}", typeid(RE::TESFormDeleteEvent).name()); + logger::info("Registered Distribution Manager for {}", typeid(RE::TESFormDeleteEvent).name()); } } diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 7b6226a..8d69ea0 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -95,13 +95,13 @@ namespace Outfits return true; } - bool Save(SKSE::SerializationInterface* a_interface, const RE::Actor* actor, const RE::BGSOutfit* original, const RE::BGSOutfit* distributed) + bool Save(SKSE::SerializationInterface* a_interface, const RE::FormID actorFormId, const RE::BGSOutfit* original, const RE::BGSOutfit* distributed) { if (!a_interface->OpenRecord(recordType, serializationVersion)) { return false; } - return details::Write(a_interface, actor->formID) && + return details::Write(a_interface, actorFormId) && details::Write(a_interface, original->formID) && details::Write(a_interface, distributed ? distributed->formID : 0); } @@ -114,6 +114,19 @@ namespace Outfits serializationInterface->SetSaveCallback(Save); serializationInterface->SetLoadCallback(Load); serializationInterface->SetRevertCallback(Revert); + + if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { + scripts->AddEventSink(GetSingleton()); + logger::info("Registered Outfit Manager for {}", typeid(RE::TESFormDeleteEvent).name()); + } + } + + RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) + { + if (a_event && a_event->formID != 0) { + replacements.erase(a_event->formID); + } + return RE::BSEventNotifyControl::kContinue; } bool CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) @@ -141,7 +154,7 @@ namespace Outfits auto* npc = actor->GetActorBase(); auto defaultOutfit = npc->defaultOutfit; - if (!allowOverwrites && replacements.find(actor) != replacements.end()) { + if (!allowOverwrites && replacements.find(actor->formID) != replacements.end()) { return true; } @@ -154,10 +167,10 @@ namespace Outfits actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that it works either way. - if (auto previous = replacements.find(actor); previous != replacements.end()) { + if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { previous->second.distributed = outfit; } else if (defaultOutfit) { - replacements.try_emplace(actor, defaultOutfit, outfit); + replacements.try_emplace(actor->formID, defaultOutfit, outfit); } return true; @@ -166,10 +179,10 @@ namespace Outfits void Manager::UseOriginalOutfit(RE::Actor* actor) { if (auto npc = actor->GetActorBase(); npc && npc->defaultOutfit) { - if (replacements.find(actor) != replacements.end()) { + if (replacements.find(actor->formID) != replacements.end()) { logger::warn("Overwriting replacement for {}", *actor); } - replacements.try_emplace(actor, npc->defaultOutfit); + replacements.try_emplace(actor->formID, npc->defaultOutfit); } } @@ -207,7 +220,9 @@ namespace Outfits logger::info("Cached {} Outfit Replacements", newReplacements.size()); for (const auto& pair : newReplacements) { - logger::info("\t{}", *pair.first); + if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { + logger::info("\t{}", *actor); + } logger::info("\t\t{}", pair.second); } @@ -216,7 +231,7 @@ namespace Outfits const auto& actor = it.first; const auto& replacement = it.second; - if (auto newIt = newReplacements.find(actor); newIt != newReplacements.end()) { + if (auto newIt = newReplacements.find(actor->formID); 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 @@ -232,7 +247,7 @@ namespace Outfits } } 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; + newReplacements[actor->formID] = replacement; } } @@ -245,18 +260,23 @@ namespace Outfits { logger::info("{:*^30}", "SAVING"); - auto outfits = Manager::GetSingleton()->replacements; + auto replacements = Manager::GetSingleton()->replacements; - logger::info("Saving {} distributed outfits...", outfits.size()); + logger::info("Saving {} distributed outfits...", replacements.size()); std::uint32_t savedCount = 0; - for (const auto& pair : outfits) { + for (const auto& pair : replacements) { if (!Data::Save(interface, pair.first, pair.second.original, pair.second.distributed)) { - logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *pair.first); + if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { + logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *actor); + } + continue; } #ifndef NDEBUG - logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *pair.first); + if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { + logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *actor); + } #endif ++savedCount; } From 042789c042e36a9111fd288b835d122ba28eec9f Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 19 Jul 2024 22:22:15 +0300 Subject: [PATCH 16/43] minor inconsequential stuff. --- SPID/include/OutfitManager.h | 13 +++++++++++++ SPID/src/OutfitManager.cpp | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 6c99feb..dc2a2c3 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -9,6 +9,17 @@ namespace Outfits public: static void Register(); + /// + /// Checks whether the actor can technically wear a given outfit. + /// Actor can wear an outfit when all of its components are compatible with actor's race. + /// + /// This method doesn't validate any other logic. + /// + /// Target Actor to be tested + /// An outfit that needs to be equipped + /// True if the actor can wear the outfit, false otherwise + bool CanEquipOutfit(const RE::Actor*, RE::BGSOutfit*); + /// /// Sets given outfit as default outfit for the actor. /// @@ -16,6 +27,8 @@ namespace Outfits /// /// Target Actor for whom the outfit will be set. /// A new outfit to set as the default. + /// If true, the outfit will be set even if the actor already has a distributed outfit. + /// True if the outfit was successfully set, false otherwise. bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); /// diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 8d69ea0..adc4b7e 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -129,7 +129,7 @@ namespace Outfits return RE::BSEventNotifyControl::kContinue; } - bool CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) + bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) { const auto race = actor->GetRace(); for (const auto& item : outfit->outfitItems) { @@ -155,7 +155,7 @@ namespace Outfits auto defaultOutfit = npc->defaultOutfit; if (!allowOverwrites && replacements.find(actor->formID) != replacements.end()) { - return true; + return true; // return true to indicate that some outfit was already set for this actor, and with overwrite disabled we won't be able to set any outfit. } if (!CanEquipOutfit(actor, outfit)) { @@ -165,7 +165,7 @@ namespace Outfits return false; } - actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that it works either way. + actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that equipping works either way, so we are good :) if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { previous->second.distributed = outfit; From 8ff681cc7e85df6d7c58226f295899463827a939 Mon Sep 17 00:00:00 2001 From: adya Date: Fri, 19 Jul 2024 19:22:30 +0000 Subject: [PATCH 17/43] maintenance --- SPID/include/OutfitManager.h | 2 +- SPID/src/OutfitManager.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index dc2a2c3..3a3be9c 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -12,7 +12,7 @@ namespace Outfits /// /// Checks whether the actor can technically wear a given outfit. /// Actor can wear an outfit when all of its components are compatible with actor's race. - /// + /// /// This method doesn't validate any other logic. /// /// Target Actor to be tested diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index adc4b7e..175811e 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -155,7 +155,7 @@ namespace Outfits auto defaultOutfit = npc->defaultOutfit; if (!allowOverwrites && replacements.find(actor->formID) != replacements.end()) { - return true; // return true to indicate that some outfit was already set for this actor, and with overwrite disabled we won't be able to set any outfit. + return true; // return true to indicate that some outfit was already set for this actor, and with overwrite disabled we won't be able to set any outfit. } if (!CanEquipOutfit(actor, outfit)) { From 4e19ded56f3c45c0b610360a1e42f7539b2e3eeb Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 27 Jul 2024 12:53:59 +0300 Subject: [PATCH 18/43] Added formatting for reverse outfit replacements logging --- SPID/include/OutfitManager.h | 42 ++++++++++++++++++++++++++++++------ SPID/src/Distribute.cpp | 2 +- SPID/src/OutfitManager.cpp | 34 ++++++++++++++--------------- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 3a3be9c..1add179 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -2,6 +2,16 @@ namespace Outfits { + enum class ReplacementResult + { + /// New outfit for the actor was successfully set. + Set, + /// New outfit for the actor is not valid and was skipped. + Skipped, + /// Outfit for the actor was already set and the new outfit was not allowed to overwrite it. + NotOverwrittable + };; + class Manager : public ISingleton, public RE::BSTEventSink @@ -28,8 +38,8 @@ namespace Outfits /// Target Actor for whom the outfit will be set. /// A new outfit to set as the default. /// If true, the outfit will be set even if the actor already has a distributed outfit. - /// True if the outfit was successfully set, false otherwise. - bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); + /// Result of the replacement. + ReplacementResult SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); /// /// Indicates that given actor didn't receive any distributed outfit and will be using the original one. @@ -76,20 +86,40 @@ template <> struct fmt::formatter { template - constexpr auto parse(ParseContext& a_ctx) + constexpr auto parse(ParseContext& ctx) { - return a_ctx.begin(); + auto it = ctx.begin(); + auto end = ctx.end(); + + if (it != end && *it == 'R') { + reverse = true; + ++it; + } + + // Check if there are any other format specifiers + if (it != end && *it != '}') { + throw fmt::format_error("OutfitReplacement only supports Reversing format"); + } + + return it; } 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); + return fmt::format_to(a_ctx.out(), "♻ {}", *replacement.original); } else if (replacement.original && replacement.distributed) { - return fmt::format_to(a_ctx.out(), "{} -> {}", *replacement.original, *replacement.distributed); + if (reverse) { + return fmt::format_to(a_ctx.out(), "{} 🔙 {}", *replacement.original, *replacement.distributed); + } else { + return fmt::format_to(a_ctx.out(), "{} ➡️ {}", *replacement.original, *replacement.distributed); + } } else { return fmt::format_to(a_ctx.out(), "INVALID REPLACEMENT"); } } + +private: + bool reverse = false; }; diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 846859e..481baf8 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -106,7 +106,7 @@ namespace Distribute if (!for_first_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites); + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites) != Outfits::ReplacementResult::Skipped; // terminate as soon as valid outfit is confirmed. }, accumulatedForms)) { Outfits::Manager::GetSingleton()->UseOriginalOutfit(npcData.GetActor()); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 175811e..dbfbc5b 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -145,24 +145,28 @@ namespace Outfits return true; } - bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) + ReplacementResult Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) { - if (!actor || !outfit) { - return false; + if (!actor || !outfit) { // invalid call + return ReplacementResult::Skipped; } auto* npc = actor->GetActorBase(); auto defaultOutfit = npc->defaultOutfit; - if (!allowOverwrites && replacements.find(actor->formID) != replacements.end()) { - return true; // return true to indicate that some outfit was already set for this actor, and with overwrite disabled we won't be able to set any outfit. + if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement + if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. + return ReplacementResult::Set; + } else if (!allowOverwrites) { // if we are trying to set any other outfit and overwrites are not allowed, we skip it, indicating overwriting status. + return ReplacementResult::NotOverwrittable; + } } 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; + return ReplacementResult::Skipped; } actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that equipping works either way, so we are good :) @@ -173,7 +177,7 @@ namespace Outfits replacements.try_emplace(actor->formID, defaultOutfit, outfit); } - return true; + return ReplacementResult::Set; } void Manager::UseOriginalOutfit(RE::Actor* actor) @@ -202,12 +206,8 @@ namespace Outfits 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 Replacement ({}) for actor {}", replacement, *actor); -#endif - loadedReplacements[actor] = replacement; + if (Data::Load(a_interface, actor, original, distributed); actor) { + loadedReplacements[actor] = {original, distributed}; } } } @@ -236,7 +236,7 @@ namespace Outfits 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); + logger::info("\t\t{:R}", replacement); #endif if (actor->SetDefaultOutfit(replacement.original, false)) { // Having true here causes infinite loading. It seems that it works either way. ++revertedCount; @@ -275,7 +275,7 @@ namespace Outfits } #ifndef NDEBUG if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { - logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *actor); + logger::info("\tSaved Outfit Replacement ({}) for actor {:F}", pair.second, *actor); } #endif ++savedCount; @@ -287,7 +287,7 @@ namespace Outfits void Manager::Revert(SKSE::SerializationInterface*) { logger::info("{:*^30}", "REVERTING"); - Manager::GetSingleton()->replacements.clear(); - logger::info("\tOutfit Replacements have been cleared."); + /*Manager::GetSingleton()->replacements.clear(); + logger::info("\tOutfit Replacements have been cleared.");*/ } } From dc6420603191477ab50648bd9b89cf88ade6f982 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 30 Jul 2024 23:48:34 +0300 Subject: [PATCH 19/43] Simplified outfit management, by not reverting anything. Distribution is persistent between saves within the same game session, so only actual replacements are tracked and saved in a co-save to be able to revert distributed outfits that are no longer valid in current distribution. --- SPID/include/OutfitManager.h | 16 +----------- SPID/src/Distribute.cpp | 12 ++++----- SPID/src/OutfitManager.cpp | 47 ++++++++---------------------------- 3 files changed, 16 insertions(+), 59 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 1add179..4baa2a3 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -41,13 +41,6 @@ namespace Outfits /// Result of the replacement. ReplacementResult SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); - /// - /// Indicates that given actor didn't receive any distributed outfit and will be using the original one. - /// - /// 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. - /// - void UseOriginalOutfit(RE::Actor*); - protected: RE::BSEventNotifyControl ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) override; @@ -69,11 +62,6 @@ namespace Outfits original(original), distributed(nullptr) {} OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed) : original(original), distributed(distributed) {} - - bool UsesOriginalOutfit() const - { - return original && !distributed; - } }; friend fmt::formatter; @@ -107,9 +95,7 @@ struct fmt::formatter template constexpr auto format(const Outfits::Manager::OutfitReplacement& replacement, FormatContext& a_ctx) { - if (replacement.UsesOriginalOutfit()) { - return fmt::format_to(a_ctx.out(), "♻ {}", *replacement.original); - } else if (replacement.original && replacement.distributed) { + if (replacement.original && replacement.distributed) { if (reverse) { return fmt::format_to(a_ctx.out(), "{} 🔙 {}", *replacement.original, *replacement.distributed); } else { diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 481baf8..2cb984e 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -104,13 +104,11 @@ namespace Distribute }, accumulatedForms); - if (!for_first_form( - npcData, forms.outfits, input, [&](auto* a_outfit) { - return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites) != Outfits::ReplacementResult::Skipped; // terminate as soon as valid outfit is confirmed. - }, - accumulatedForms)) { - Outfits::Manager::GetSingleton()->UseOriginalOutfit(npcData.GetActor()); - } + for_first_form( + npcData, forms.outfits, input, [&](auto* a_outfit) { + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites) != Outfits::ReplacementResult::Skipped; // terminate as soon as valid outfit is confirmed. + }, + accumulatedForms); for_first_form( npcData, forms.sleepOutfits, input, [&](auto* a_outfit) { diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index dbfbc5b..49c6730 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -58,9 +58,8 @@ namespace Outfits 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 (!id) { + return false; } if (!interface->ResolveFormID(id, id)) { @@ -113,7 +112,6 @@ namespace Outfits serializationInterface->SetUniqueID(serializationKey); serializationInterface->SetSaveCallback(Save); serializationInterface->SetLoadCallback(Load); - serializationInterface->SetRevertCallback(Revert); if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { scripts->AddEventSink(GetSingleton()); @@ -180,16 +178,6 @@ namespace Outfits return ReplacementResult::Set; } - void Manager::UseOriginalOutfit(RE::Actor* actor) - { - if (auto npc = actor->GetActorBase(); npc && npc->defaultOutfit) { - if (replacements.find(actor->formID) != replacements.end()) { - logger::warn("Overwriting replacement for {}", *actor); - } - replacements.try_emplace(actor->formID, npc->defaultOutfit); - } - } - void Manager::Load(SKSE::SerializationInterface* a_interface) { logger::info("{:*^30}", "LOADING"); @@ -230,24 +218,16 @@ namespace Outfits 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 (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 + 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 (replacement.distributed == actor->GetActorBase()->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 #ifndef NDEBUG - logger::info("\tReverting Outfit Replacement for {}", *actor); - logger::info("\t\t{:R}", replacement); + logger::info("\tReverting Outfit Replacement for {}", *actor); + logger::info("\t\t{:R}", replacement); #endif - if (actor->SetDefaultOutfit(replacement.original, false)) { // Having true here causes infinite loading. It seems that it works either way. - ++revertedCount; - } - } - } 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. + if (actor->SetDefaultOutfit(replacement.original, false)) { // Having true here causes infinite loading. It seems that it works either way. + ++revertedCount; } - - } 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->formID] = replacement; } } @@ -275,7 +255,7 @@ namespace Outfits } #ifndef NDEBUG if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { - logger::info("\tSaved Outfit Replacement ({}) for actor {:F}", pair.second, *actor); + logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *actor); } #endif ++savedCount; @@ -283,11 +263,4 @@ namespace Outfits logger::info("Saved {} names", savedCount); } - - void Manager::Revert(SKSE::SerializationInterface*) - { - logger::info("{:*^30}", "REVERTING"); - /*Manager::GetSingleton()->replacements.clear(); - logger::info("\tOutfit Replacements have been cleared.");*/ - } } From c37eaa3bd3f6d5bf2fedc6478f41c81777cd6856 Mon Sep 17 00:00:00 2001 From: adya Date: Tue, 30 Jul 2024 20:48:55 +0000 Subject: [PATCH 20/43] maintenance --- SPID/include/OutfitManager.h | 3 ++- SPID/src/OutfitManager.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 4baa2a3..213055e 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -10,7 +10,8 @@ namespace Outfits Skipped, /// Outfit for the actor was already set and the new outfit was not allowed to overwrite it. NotOverwrittable - };; + }; + ; class Manager : public ISingleton, diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 49c6730..e86c49e 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -145,17 +145,17 @@ namespace Outfits ReplacementResult Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) { - if (!actor || !outfit) { // invalid call + if (!actor || !outfit) { // invalid call return ReplacementResult::Skipped; } auto* npc = actor->GetActorBase(); auto defaultOutfit = npc->defaultOutfit; - if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement - if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. + if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement + if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. return ReplacementResult::Set; - } else if (!allowOverwrites) { // if we are trying to set any other outfit and overwrites are not allowed, we skip it, indicating overwriting status. + } else if (!allowOverwrites) { // if we are trying to set any other outfit and overwrites are not allowed, we skip it, indicating overwriting status. return ReplacementResult::NotOverwrittable; } } @@ -195,7 +195,7 @@ namespace Outfits RE::BGSOutfit* original; RE::BGSOutfit* distributed; if (Data::Load(a_interface, actor, original, distributed); actor) { - loadedReplacements[actor] = {original, distributed}; + loadedReplacements[actor] = { original, distributed }; } } } From b1bcc738f81104ab30136301666d3061649e52b4 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 30 Jul 2024 23:53:30 +0300 Subject: [PATCH 21/43] disabled dev logs in release config. --- SPID/src/Distribute.cpp | 2 ++ SPID/src/OutfitManager.cpp | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 2cb984e..03a1897 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -182,6 +182,7 @@ namespace Distribute void LogDistribution(const DistributedForms& forms, NPCData& npcData) { +#ifndef NDEBUG std::map> results; for (const auto& form : forms) { @@ -195,5 +196,6 @@ namespace Distribute logger::info("\t\t{} @ {}", *form.first, form.second); } } +#endif } } diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index e86c49e..cfdca32 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -180,8 +180,9 @@ namespace Outfits void Manager::Load(SKSE::SerializationInterface* a_interface) { +#ifndef NDEBUG logger::info("{:*^30}", "LOADING"); - +#endif auto* manager = Manager::GetSingleton(); std::unordered_map loadedReplacements; @@ -199,7 +200,7 @@ namespace Outfits } } } - +#ifndef NDEBUG logger::info("Loaded {} Outfit Replacements", loadedReplacements.size()); for (const auto& pair : loadedReplacements) { logger::info("\t{}", *pair.first); @@ -213,7 +214,7 @@ namespace Outfits } logger::info("\t\t{}", pair.second); } - +#endif std::uint32_t revertedCount = 0; for (const auto& it : loadedReplacements) { const auto& actor = it.first; @@ -230,27 +231,30 @@ namespace Outfits } } } - +#ifndef NDEBUG if (revertedCount) { logger::info("Reverted {} no longer existing Outfit Replacements", revertedCount); } +#endif } void Manager::Save(SKSE::SerializationInterface* interface) { +#ifndef NDEBUG logger::info("{:*^30}", "SAVING"); - +#endif auto replacements = Manager::GetSingleton()->replacements; - +#ifndef NDEBUG logger::info("Saving {} distributed outfits...", replacements.size()); - +#endif std::uint32_t savedCount = 0; for (const auto& pair : replacements) { if (!Data::Save(interface, pair.first, pair.second.original, pair.second.distributed)) { +#ifndef NDEBUG if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *actor); } - +#endif continue; } #ifndef NDEBUG @@ -260,7 +264,8 @@ namespace Outfits #endif ++savedCount; } - - logger::info("Saved {} names", savedCount); +#ifndef NDEBUG + logger::info("Saved {} replacements", savedCount); +#endif } } From ef9068391f7f2ccafe4c5fa6fa7169c16dedd0d0 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Tue, 30 Jul 2024 23:55:43 +0300 Subject: [PATCH 22/43] minor cleanup --- SPID/include/DistributeManager.h | 1 - SPID/src/DeathDistribution.cpp | 1 - SPID/src/DistributeManager.cpp | 9 +++------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/SPID/include/DistributeManager.h b/SPID/include/DistributeManager.h index 963f9e3..eb8d5de 100644 --- a/SPID/include/DistributeManager.h +++ b/SPID/include/DistributeManager.h @@ -3,7 +3,6 @@ namespace Distribute { inline RE::BGSKeyword* processed{ nullptr }; - inline RE::BGSKeyword* processedOutfit{ nullptr }; // TODO: If OutfitManager works out we won't need this keyword. namespace detail { diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 345b383..7946de7 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -243,7 +243,6 @@ namespace DeathDistribution } Distribute::LogDistribution(distributedForms, data); - // TODO: Log death distribution } RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index f706f46..4e18507 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -33,7 +33,7 @@ namespace Distribute { static bool thunk(RE::Character* a_this) { - logger::info("ShouldBackgroundClone({})", *a_this); + //logger::info("ShouldBackgroundClone({})", *a_this); if (const auto npc = a_this->GetActorBase()) { detail::distribute_on_load(a_this, npc); } @@ -59,8 +59,8 @@ namespace Distribute if (a_this->Is3DLoaded()) { // 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); + //logger::info("InitLoadGame({})", *a_this); + detail::distribute_on_load(a_this, npc); } } } @@ -86,9 +86,6 @@ namespace Distribute if (processed = factory->Create(); processed) { processed->formEditorID = "SPID_Processed"; } - if (processedOutfit = factory->Create(); processedOutfit) { - processedOutfit->formEditorID = "SPID_ProcessedOutfit"; - } } if (Forms::GetTotalLeveledEntries() > 0) { From f90a7e3fa2dadac89fe24517b97ce24d05a9b16e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 31 Jul 2024 00:10:50 +0300 Subject: [PATCH 23/43] Set game version identifiers. --- SPID/CMakeLists.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SPID/CMakeLists.txt b/SPID/CMakeLists.txt index 15c61ed..3687300 100644 --- a/SPID/CMakeLists.txt +++ b/SPID/CMakeLists.txt @@ -1,8 +1,9 @@ cmake_minimum_required(VERSION 3.20) set(NAME "po3_SpellPerkItemDistributor" CACHE STRING "") set(VERSION 7.2.0 CACHE STRING "") -set(AE_VERSION 1) -set(VR_VERSION 1) +set(SE_VERSION 1597) +set(AE_VERSION 16) +set(VR_VERSION 1415) # ---- Options ---- @@ -53,7 +54,7 @@ else() set_from_environment(Skyrim64Path) set(SkyrimPath ${Skyrim64Path}) set(SkyrimVersion "Skyrim SSE") - set(VERSION ${VERSION}.0) + set(VERSION ${VERSION}.${SE_VERSION}) endif() find_commonlib_path() message( From da4c2bc6d54fb8e6f248511ad7335d7bbbdf8434 Mon Sep 17 00:00:00 2001 From: adya Date: Tue, 30 Jul 2024 21:11:18 +0000 Subject: [PATCH 24/43] maintenance --- SPID/src/DistributeManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 4e18507..e247afc 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -60,7 +60,7 @@ namespace Distribute // 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); + detail::distribute_on_load(a_this, npc); } } } From 6ca28fb9c1d2700cacd8af15d555df8d32e8871b Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 31 Jul 2024 21:57:42 +0300 Subject: [PATCH 25/43] Fixed a possible crash caused by missing ActorBase. --- SPID/src/OutfitManager.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index cfdca32..09f32e2 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -150,7 +150,12 @@ namespace Outfits } auto* npc = actor->GetActorBase(); - auto defaultOutfit = npc->defaultOutfit; + + if (!npc) { + return ReplacementResult::Skipped; + } + + auto defaultOutfit = npc->defaultOutfit; if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. @@ -219,9 +224,9 @@ namespace Outfits 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 (replacement.distributed == actor->GetActorBase()->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) + } 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 #ifndef NDEBUG logger::info("\tReverting Outfit Replacement for {}", *actor); logger::info("\t\t{:R}", replacement); From d7f30bcf3c7b4921e4c324edfda94e29b0e91ab1 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 24 Nov 2024 20:50:53 +0200 Subject: [PATCH 26/43] Few improvements for debugging - Added more information on deserialization error. - Extracted equipping outfits to a separate method for easier replacement. --- SPID/include/OutfitManager.h | 8 ++++++++ SPID/src/OutfitManager.cpp | 39 +++++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 213055e..eb5a8a4 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -50,6 +50,14 @@ namespace Outfits static void Save(SKSE::SerializationInterface*); static void Revert(SKSE::SerializationInterface*); + /// + /// This method performs the actual change of the outfit. + /// + /// Actor for whom outfit should be changed + /// The outfit to be set + /// True if the outfit was successfully set, false otherwise + bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + struct OutfitReplacement { /// The one that NPC had before SPID distribution. diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 09f32e2..3a2f85b 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -50,7 +50,7 @@ namespace Outfits constexpr std::uint32_t recordType = 'OTFT'; template - bool Load(SKSE::SerializationInterface* interface, T*& output) + bool Load(SKSE::SerializationInterface* interface, T*& output, RE::FormID& formID) { RE::FormID id = 0; @@ -62,10 +62,14 @@ namespace Outfits return false; } + formID = id; // save the originally read formID + if (!interface->ResolveFormID(id, id)) { return false; } + formID = id; // save the resolved formID + if (const auto form = RE::TESForm::LookupByID(id); form) { output = form; return true; @@ -76,18 +80,20 @@ namespace Outfits 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."); + RE::FormID id = 0; + + if (!Load(interface, loadedActor, id)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted actor [{:08X}].", id); return false; } - if (!Load(interface, loadedOriginalOutfit)) { - logger::warn("Failed to load Outfit Replacement record: Corrupted original outfit."); + if (!Load(interface, loadedOriginalOutfit, id)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted original outfit [{:08X}].", id); return false; } - if (!Load(interface, loadedDistributedOutfit)) { - logger::warn("Failed to load Outfit Replacement record: Corrupted distributed outfit."); + if (!Load(interface, loadedDistributedOutfit, id)) { + logger::warn("Failed to load Outfit Replacement record: Corrupted distributed outfit [{:08X}].", id); return false; } @@ -172,7 +178,7 @@ namespace Outfits return ReplacementResult::Skipped; } - actor->SetDefaultOutfit(outfit, false); // Having true here causes infinite loading. It seems that equipping works either way, so we are good :) + SetDefaultOutfit(actor, outfit); if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { previous->second.distributed = outfit; @@ -231,7 +237,7 @@ namespace Outfits logger::info("\tReverting Outfit Replacement for {}", *actor); logger::info("\t\t{:R}", replacement); #endif - if (actor->SetDefaultOutfit(replacement.original, false)) { // Having true here causes infinite loading. It seems that it works either way. + if (manager->SetDefaultOutfit(actor, replacement.original)) { ++revertedCount; } } @@ -243,6 +249,21 @@ namespace Outfits #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; + } + force_equip_outfit(actor, actor->GetActorBase()); + */ + return true; + } + void Manager::Save(SKSE::SerializationInterface* interface) { #ifndef NDEBUG From f1161943faaaa0ed2d4977070c5f8cf0cd1e0893 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 14 Dec 2024 03:30:13 +0200 Subject: [PATCH 27/43] Fixed the crash. Improved merging of replacements. - 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. --- SPID/include/OutfitManager.h | 14 +++- SPID/src/DistributeManager.cpp | 5 -- SPID/src/OutfitManager.cpp | 134 ++++++++++++++++++++++----------- SPID/src/main.cpp | 5 ++ 4 files changed, 108 insertions(+), 50 deletions(-) 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()); From 23cfd6e361b2c1ce6b4ba1e3c02fea33d56017b6 Mon Sep 17 00:00:00 2001 From: adya Date: Sat, 14 Dec 2024 01:30:33 +0000 Subject: [PATCH 28/43] maintenance --- SPID/src/OutfitManager.cpp | 2 +- SPID/src/main.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 847b941..2442636 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -288,7 +288,7 @@ namespace Outfits 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) + 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); diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index 4cdf01b..384dc61 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -1,9 +1,9 @@ +#include "DeathDistribution.h" #include "DistributeManager.h" #include "LookupConfigs.h" #include "LookupForms.h" -#include "PCLevelMultManager.h" #include "OutfitManager.h" -#include "DeathDistribution.h" +#include "PCLevelMultManager.h" bool shouldLookupForms{ false }; bool shouldLogErrors{ false }; @@ -44,7 +44,7 @@ void MessageHandler(SKSE::MessagingInterface::Message* a_message) 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. + 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()); From 48328a162f41a6aec5bddfb826b18bd8968359a2 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sun, 15 Dec 2024 02:05:35 +0200 Subject: [PATCH 29/43] Updated vcpkg. --- .github/workflows/main.yml | 2 +- vcpkg.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 266ffbd..7300017 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,4 +30,4 @@ jobs: PUBLISH_MOD_CHANGELOG_FILE: "FOMOD/changelog.txt" PUBLISH_MOD_DESCRIPTION_FILE: "FOMOD/description.txt" PUBLISH_ARCHIVE_TYPE: '7z' - VCPKG_COMMIT_ID: '49ac2134b31b95b0ddf29d56873dcd24392691df' + VCPKG_COMMIT_ID: 'b545373a9a536dc559dac8583467a21497a0e897' diff --git a/vcpkg.json b/vcpkg.json index f240378..153339f 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,7 +1,7 @@ { "name": "spid-common", "version-string": "1", - "builtin-baseline": "49ac2134b31b95b0ddf29d56873dcd24392691df", + "builtin-baseline": "b545373a9a536dc559dac8583467a21497a0e897", "dependencies": [ "clib-util", "mergemapper", From 90911d8f8b2440428ffe943cf5113b326831c05b Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 18 Dec 2024 21:58:50 +0200 Subject: [PATCH 30/43] Fixed the build. --- SPID/include/Defs.h | 4 ++-- SPID/include/OutfitManager.h | 2 +- SPID/src/PCLevelMultManager.cpp | 12 ++++++------ extern/CommonLibSSE | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SPID/include/Defs.h b/SPID/include/Defs.h index 81972e2..cfd77d3 100644 --- a/SPID/include/Defs.h +++ b/SPID/include/Defs.h @@ -171,7 +171,7 @@ namespace fmt } template - constexpr auto format(const Form& form, FormatContext& a_ctx) + constexpr auto format(const Form& form, FormatContext& a_ctx) const { const auto name = std::string(form.GetName()); const auto edid = editorID::get_editorID(&form); @@ -197,7 +197,7 @@ namespace fmt } template - constexpr auto format(const RE::Actor& actor, FormatContext& a_ctx) + constexpr auto format(const RE::Actor& actor, FormatContext& a_ctx) const { const auto& form = static_cast(actor); if (auto npc = actor.GetActorBase(); npc) { diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 4c4c33f..41514a0 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -108,7 +108,7 @@ struct fmt::formatter } template - constexpr auto format(const Outfits::Manager::OutfitReplacement& replacement, FormatContext& a_ctx) + constexpr auto format(const Outfits::Manager::OutfitReplacement& replacement, FormatContext& a_ctx) const { if (replacement.original && replacement.distributed) { if (reverse) { diff --git a/SPID/src/PCLevelMultManager.cpp b/SPID/src/PCLevelMultManager.cpp index 0efba9d..bfdef5b 100644 --- a/SPID/src/PCLevelMultManager.cpp +++ b/SPID/src/PCLevelMultManager.cpp @@ -107,15 +107,15 @@ namespace PCLevelMult void Manager::DumpDistributedEntries() { ReadLocker lock(_lock); - for (auto& [playerID, npcFormIDs] : _cache) { + for (const auto& [playerID, npcFormIDs] : _cache) { logger::info("PlayerID : {:X}", playerID); - for (auto& [npcFormID, levelMap] : npcFormIDs) { + for (const auto& [npcFormID, levelMap] : npcFormIDs) { logger::info("\tNPC : {} [{:X}]", editorID::get_editorID(RE::TESForm::LookupByID(npcFormID)), npcFormID); - for (auto& [level, distFormMap] : levelMap.entries) { + for (const auto& [level, distFormMap] : levelMap.entries) { logger::info("\t\tLevel : {}", level); - for (auto& [formType, formIDSet] : distFormMap.distributedEntries) { + for (const auto& [formType, formIDSet] : distFormMap.distributedEntries) { logger::info("\t\t\tDist FormType : {}", formType); - for (auto& formID : formIDSet) { + for (const auto& formID : formIDSet) { logger::info("\t\t\t\tDist FormID : {} [{:X}]", editorID::get_editorID(RE::TESForm::LookupByID(formID)), formID); } } @@ -209,7 +209,7 @@ namespace PCLevelMult std::uint64_t Manager::get_game_playerID() { - return RE::BGSSaveLoadManager::GetSingleton()->currentPlayerID & 0xFFFFFFFF; + return RE::BGSSaveLoadManager::GetSingleton()->currentCharacterID & 0xFFFFFFFF; } void Manager::remap_player_ids(std::uint64_t a_oldID, std::uint64_t a_newID) diff --git a/extern/CommonLibSSE b/extern/CommonLibSSE index af3e8a1..200ae55 160000 --- a/extern/CommonLibSSE +++ b/extern/CommonLibSSE @@ -1 +1 @@ -Subproject commit af3e8a1f46e57b114d6a97dc38f6afd6d4ffb2d7 +Subproject commit 200ae55cf18f21337838e1baab14d96af0b1e546 From 194c47db356b599d13d1f846e458d366da3768cb Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 18 Dec 2024 22:36:22 +0200 Subject: [PATCH 31/43] Updated CommonLibs. --- extern/CommonLibSSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/CommonLibSSE b/extern/CommonLibSSE index 200ae55..b41d280 160000 --- a/extern/CommonLibSSE +++ b/extern/CommonLibSSE @@ -1 +1 @@ -Subproject commit 200ae55cf18f21337838e1baab14d96af0b1e546 +Subproject commit b41d28091016ac3c1fa6bd063adbbcc1fcd8c36e From ec4a25d295e87a713042eeb2ddfe675296e7c041 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Wed, 18 Dec 2024 23:42:00 +0200 Subject: [PATCH 32/43] Fixed logger. --- SPID/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index 384dc61..f62162d 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -140,7 +140,7 @@ extern "C" DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_s logger::info("Game version : {}", a_skse->RuntimeVersion().string()); - SKSE::Init(a_skse); + SKSE::Init(a_skse, false); SKSE::GetMessagingInterface()->RegisterListener(MessageHandler); From 450e25ad1d5bb9046ebf817e16a33b2f47cc073d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 20 Dec 2024 23:39:31 +0200 Subject: [PATCH 33/43] Possible fixed a crash reported in RC4. --- SPID/src/OutfitManager.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 2442636..74fc40b 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -213,8 +213,14 @@ namespace Outfits } } - if (equippedItems) { - actor->currentProcess->Update3DModel(actor); + if (equippedItems && actor->Is3DLoaded()) { + // dispatching updating 3D model (presumably) to the main thread. + // There was a crash when trying to update 3D model for some users, so this is an attempt to fix it. + // Additionally, an extra check ensures that update of the model is only called when said model is already loaded. + SKSE::GetTaskInterface()->AddTask([actor]() { + actor->currentProcess->Update3DModel(actor); + }); + } } From 9011c9feed58b2774b3164614c0bbe99c53772ee Mon Sep 17 00:00:00 2001 From: adya Date: Fri, 20 Dec 2024 21:39:54 +0000 Subject: [PATCH 34/43] maintenance --- SPID/src/OutfitManager.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 74fc40b..7016a5c 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -218,9 +218,8 @@ namespace Outfits // There was a crash when trying to update 3D model for some users, so this is an attempt to fix it. // Additionally, an extra check ensures that update of the model is only called when said model is already loaded. SKSE::GetTaskInterface()->AddTask([actor]() { - actor->currentProcess->Update3DModel(actor); + actor->currentProcess->Update3DModel(actor); }); - } } From edf7deb09c5f8188cf2fc54d3ddea2e179811855 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 26 Dec 2024 01:46:03 +0200 Subject: [PATCH 35/43] Fixed one more crash caused by equipping outfits. - Added detailed logging for outfit manager in debug --- SPID/include/Distribute.h | 2 +- SPID/src/OutfitManager.cpp | 57 ++++++++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index 2aae160..a996a74 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -230,7 +230,7 @@ namespace Distribute /// General information about NPC that is being processed. /// Leveling information about NPC that is being processed. /// A set of forms that should be distributed to NPC. - /// If true, overwritable forms (like Outfits) will be to overwrite last distributed form on NPC. + /// If true, overwritable forms (like Outfits) will use last distributed form on NPC. If false, then the first form will be used. /// An optional pointer to a set that will accumulate all distributed forms. void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms = nullptr); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 7016a5c..4fb955a 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -142,25 +142,47 @@ namespace Outfits return ReplacementResult::Skipped; } - auto* npc = actor->GetActorBase(); + const auto npc = actor->GetActorBase(); if (!npc) { return ReplacementResult::Skipped; } - auto defaultOutfit = npc->defaultOutfit; + const auto defaultOutfit = npc->defaultOutfit; +#ifndef NDEBUG + logger::info("Evaluating outfit for {}", *actor); + logger::info("\tNew Outfit: {}", *outfit); + if (defaultOutfit) { + logger::info("\tCurrent Outfit: {}", *defaultOutfit); + } else { + logger::info("\tCurrent Outfit: None"); + } +#endif if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement +#ifndef NDEBUG + logger::info("\tFound existing replacement {}", existing->second); +#endif if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. +#ifndef NDEBUG + logger::info("\tExisting replacement is already set"); +#endif return ReplacementResult::Set; } else if (!allowOverwrites) { // if we are trying to set any other outfit and overwrites are not allowed, we skip it, indicating overwriting status. +#ifndef NDEBUG + logger::info("\tOverwriting outfit is not allowed"); +#endif return ReplacementResult::NotOverwrittable; } } + // TODO: Try to not equip outfit right away. Only store the replacement. The actual distribution will happen in Load3D hook here + // For dynamic distributions such as On Death Distribution, we can use a boolean parameter "forceEquip" to equip the outfit right away. + // This will add proper support for linked distribution where one outfit should be distributed immediately after another one. + if (!CanEquipOutfit(actor, outfit)) { #ifndef NDEBUG - logger::warn("Attempted to equip Outfit that can't be worn by given actor. Actor: {}; Outfit: {}", *actor, *outfit); + logger::warn("\tAttempted to equip Outfit {} that can't be worn by given actor.", *outfit); #endif return ReplacementResult::Skipped; } @@ -169,8 +191,14 @@ namespace Outfits if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { previous->second.distributed = outfit; +#ifndef NDEBUG + logger::warn("\tUpdated replacement {}", previous->second); +#endif } else if (defaultOutfit) { replacements.try_emplace(actor->formID, defaultOutfit, outfit); +#ifndef NDEBUG + logger::warn("\tAdded replacement {}", OutfitReplacement(defaultOutfit, outfit)); +#endif } return ReplacementResult::Set; @@ -194,7 +222,6 @@ namespace Outfits 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(); @@ -204,23 +231,15 @@ namespace Outfits 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; + // queueEquip causes random crashes, probably when the equipping is executed. + // forceEquip seems to do the job. + RE::ActorEquipManager::GetSingleton()->EquipObject(actor, entryList->object, xList, 1, nullptr, false, true); } } } } } } - - if (equippedItems && actor->Is3DLoaded()) { - // dispatching updating 3D model (presumably) to the main thread. - // There was a crash when trying to update 3D model for some users, so this is an attempt to fix it. - // Additionally, an extra check ensures that update of the model is only called when said model is already loaded. - SKSE::GetTaskInterface()->AddTask([actor]() { - actor->currentProcess->Update3DModel(actor); - }); - } } bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) @@ -228,7 +247,9 @@ namespace Outfits if (!actor || !outfit) { return false; } - +#ifndef NDEBUG + logger::info("\tEquipping Outfit {}", *outfit); +#endif const auto npc = actor->GetActorBase(); if (!npc || npc->defaultOutfit == outfit) { return false; @@ -291,7 +312,9 @@ namespace Outfits std::uint32_t updatedCount = 0; for (const auto& it : loadedReplacements) { const auto& actor = it.first; + const auto npc = actor->GetActorBase(); const auto& replacement = it.second; + logger::info("\t\tActual outfit on NPC: {}", *(npc->defaultOutfit)); 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; @@ -299,7 +322,7 @@ namespace Outfits 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 + } else if (!replacement.distributed || 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); From 6b5da4c8c23c38e6844da5efe4b51c659d25d26d Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 26 Dec 2024 13:05:29 +0200 Subject: [PATCH 36/43] Added IsDying flag to NPCData to restore validation of Death Distribution. --- SPID/include/LookupNPC.h | 18 +++++++++++++++++- SPID/src/DeathDistribution.cpp | 4 ++-- SPID/src/LookupNPC.cpp | 10 ++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index 8833521..7929e21 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -7,7 +7,7 @@ namespace NPC struct Data { - Data(RE::Actor* a_actor, RE::TESNPC* a_npc); + Data(RE::Actor* a_actor, RE::TESNPC* a_npc, bool isDying = false); ~Data() = default; [[nodiscard]] RE::TESNPC* GetNPC() const; @@ -30,7 +30,22 @@ namespace NPC [[nodiscard]] bool IsChild() const; [[nodiscard]] bool IsLeveled() const; [[nodiscard]] bool IsTeammate() const; + /// + /// Flag indicating whether given NPC is dead. + /// + /// IsDead returns true when either NPC starts dead or has already died. See IsDying for more details. + /// [[nodiscard]] bool IsDead() const; + + /// + /// Flag indicating whether given NPC is currently dying. + /// + /// This is detected with RE::TESDeathEvent. + /// It is called twice for each dying Actor, first when they are dying and second when they are dead. + /// When IsDying is true, IsDead will remain false. + /// Once actor IsDead IsDying will be false. + /// + [[nodiscard]] bool IsDying() const; [[nodiscard]] bool StartsDead() const; [[nodiscard]] RE::TESRace* GetRace() const; @@ -65,6 +80,7 @@ namespace NPC bool child; bool teammate; bool leveled; + bool dying; }; } diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 7946de7..25bef24 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -206,7 +206,7 @@ namespace DeathDistribution void Manager::Distribute(NPCData& data) { - assert(data.IsDead()); + assert(data.IsDead() || data.IsDying()); // We mark NPCs that were processed by Death Distribution with SPID_Dead keyword, // to ensure that NPCs who received Death Distribution once won't get another Death Distribution @@ -255,7 +255,7 @@ namespace DeathDistribution const auto actor = a_event->actorDying->As(); const auto npc = actor ? actor->GetActorBase() : nullptr; if (actor && npc) { - auto npcData = NPCData(actor, npc); + auto npcData = NPCData(actor, npc, true); Distribute(npcData); } } diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 23ba10e..34bd10a 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -28,14 +28,15 @@ namespace NPC return formID == a_formID; } - Data::Data(RE::Actor* a_actor, RE::TESNPC* a_npc) : + Data::Data(RE::Actor* a_actor, RE::TESNPC* a_npc, bool isDying = false) : npc(a_npc), actor(a_actor), race(a_actor->GetRace()), name(a_actor->GetName()), level(a_npc->GetLevel()), child(a_actor->IsChild() || race && race->formEditorID.contains("RaceChild")), - leveled(a_actor->IsLeveled()) + leveled(a_actor->IsLeveled()), + dying(isDying) { npc->ForEachKeyword([&](const RE::BGSKeyword* a_keyword) { keywords.emplace(a_keyword->GetFormEditorID()); @@ -231,6 +232,11 @@ namespace NPC return actor && actor->IsDead() || StartsDead(); } + bool Data::IsDying() const + { + return isDying; + } + bool Data::StartsDead() const { return actor && (actor->formFlags & RE::Actor::RecordFlags::kStartsDead); From 06a17f476f43ddb50f6713ae062e8ecedabb96ce Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 26 Dec 2024 19:26:03 +0200 Subject: [PATCH 37/43] Implemented deferred outfit equipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Outfit Manager now automatically handles proper equipping of outfits by utilizing necessary hooks. - As a bonus, Death Distribution now handles reloads properly, what was looted stays looted 🙂 --- SPID/include/DeathDistribution.h | 8 +- SPID/include/Distribute.h | 3 +- SPID/include/LookupNPC.h | 1 + SPID/include/OutfitManager.h | 70 +++++-- SPID/src/DeathDistribution.cpp | 81 +++++--- SPID/src/Distribute.cpp | 8 +- SPID/src/DistributeManager.cpp | 2 +- SPID/src/LookupNPC.cpp | 4 +- SPID/src/OutfitManager.cpp | 345 ++++++++++++++++++------------- SPID/src/main.cpp | 5 +- 10 files changed, 329 insertions(+), 198 deletions(-) diff --git a/SPID/include/DeathDistribution.h b/SPID/include/DeathDistribution.h index fd9c100..695fd37 100644 --- a/SPID/include/DeathDistribution.h +++ b/SPID/include/DeathDistribution.h @@ -7,9 +7,9 @@ namespace DeathDistribution namespace INI { /// - /// Checks whether given entry is an on death distribuatble form and attempts to parse it. + /// Checks whether given entry is an On Death Distributable Form and attempts to parse it. /// - /// true if given entry was an on death distribuatble form. Note that returned value doesn't represent whether parsing was successful. + /// True if given entry was an On Death Distribuatble Form. Note that returned value doesn't represent whether parsing was successful. bool TryParse(const std::string& key, const std::string& value, const Path&); } @@ -20,7 +20,7 @@ namespace DeathDistribution public RE::BSTEventSink { public: - static void Register(); + void HandleMessage(SKSE::MessagingInterface::Message*); /// /// Does a forms lookup similar to what Filters do. @@ -55,7 +55,7 @@ namespace DeathDistribution Distributables skins{ RECORD::kSkin }; /// - /// Iterates over each type of LinkedForms and calls a callback with each of them. + /// Iterates over each type of On Death Distributable Form and calls a callback with each of them. /// template void ForEachDistributable(Func&& func, Args&&... args); diff --git a/SPID/include/Distribute.h b/SPID/include/Distribute.h index a996a74..0690c67 100644 --- a/SPID/include/Distribute.h +++ b/SPID/include/Distribute.h @@ -230,9 +230,8 @@ namespace Distribute /// General information about NPC that is being processed. /// Leveling information about NPC that is being processed. /// A set of forms that should be distributed to NPC. - /// If true, overwritable forms (like Outfits) will use last distributed form on NPC. If false, then the first form will be used. /// An optional pointer to a set that will accumulate all distributed forms. - void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms = nullptr); + void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, DistributedForms* accumulatedForms = nullptr); /// /// Invokes appropriate distribution for given NPC. diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index 7929e21..edad49c 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -30,6 +30,7 @@ namespace NPC [[nodiscard]] bool IsChild() const; [[nodiscard]] bool IsLeveled() const; [[nodiscard]] bool IsTeammate() const; + /// /// Flag indicating whether given NPC is dead. /// diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 41514a0..3abcd10 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -2,23 +2,13 @@ namespace Outfits { - enum class ReplacementResult - { - /// New outfit for the actor was successfully set. - Set, - /// New outfit for the actor is not valid and was skipped. - Skipped, - /// Outfit for the actor was already set and the new outfit was not allowed to overwrite it. - NotOverwrittable - }; - ; - class Manager : public ISingleton, - public RE::BSTEventSink + public RE::BSTEventSink, + public RE::BSTEventSink { public: - static void Register(); + void HandleMessage(SKSE::MessagingInterface::Message*); /// /// Checks whether the actor can technically wear a given outfit. @@ -34,21 +24,46 @@ namespace Outfits /// /// Sets given outfit as default outfit for the actor. /// - /// This method also makes sure to properly remove previously distributed outfit. + /// SetDefaultOutfit does not apply outfits immediately, it only registers them within the manager. + /// ApplyOutfit must be called afterwards to actually equip the outfit. + /// + /// For actors that are loading as usual ApplyOutfit will be called automatically during their Load3D. + /// For Death Distribution, ApplyOutfit will be called automatically during the event processing. + /// For other runtime distributions, ApplyOutfit should be called manually. + /// /// /// Target Actor for whom the outfit will be set. /// A new outfit to set as the default. - /// If true, the outfit will be set even if the actor already has a distributed outfit. - /// Result of the replacement. - ReplacementResult SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*, bool allowOverwrites); + /// True if the outfit was set, otherwise false + bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + + /// + /// Applies tracked outfit replacement for given actor. + /// + /// SetDefaultOutfit does not apply outfits immediately, it only registers them within the manager. + /// + /// For actors that are loading as usual ApplyOutfit will be called automatically during their Load3D. + /// For Death Distribution, ApplyOutfit will be called automatically during the event processing. + /// For other runtime distributions, ApplyOutfit should be called manually. + /// + /// If there is no replacement for the actor, then nothing will be done. + /// This method also makes sure to properly remove previously distributed outfit. + /// + /// Actor for whom outfit should be changed + /// True if the outfit was successfully set, false otherwise + bool ApplyOutfit(RE::Actor*); protected: RE::BSEventNotifyControl ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) override; + /// + /// TESDeathEvent is used to update outfit after potential Death Distribution of a new outfit. + /// + RE::BSEventNotifyControl ProcessEvent(const RE::TESDeathEvent*, RE::BSTEventSource*) override; + private: static void Load(SKSE::SerializationInterface*); static void Save(SKSE::SerializationInterface*); - static void Revert(SKSE::SerializationInterface*); /// /// This method performs the actual change of the outfit. @@ -56,7 +71,7 @@ namespace Outfits /// Actor for whom outfit should be changed /// The outfit to be set /// True if the outfit was successfully set, false otherwise - bool SetDefaultOutfit(RE::Actor*, RE::BGSOutfit*); + bool ApplyOutfit(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*); @@ -79,9 +94,26 @@ namespace Outfits original(original), distributed(distributed), unrecognizedDistributedFormID(0) {} }; + friend struct Load3D; + friend fmt::formatter; + /// + /// Map of Actor's FormID and corresponding Outfit Replacements that are being tracked by the manager. + /// + /// This map is serialized in a co-save and is used to clean up no longer distributed outfits when loading a previous save. + /// Latest distribution replacements always take priority over the ones stored in the save file. + /// std::unordered_map replacements; + + /// + /// Flag indicating whether there is a loading of a save file in progress. + /// + /// This flag is used to defer equipping outfits in Load3D hook, until after LoadGame event is processed. + /// By doing so we can properly handle state of the outfits and determine if anything needs to be equipped. + /// Among other things, this allows to avoid re-equipping looted outfits On Death Distribution after relaunching the game. + /// + bool isLoadingGame = false; }; } diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 25bef24..1040474 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -183,36 +183,68 @@ namespace DeathDistribution static RE::BGSKeyword* SPID_Dead = nullptr; - void Manager::Register() + struct ShouldBackgroundClone { - if (INI::deathConfigs.empty()) { - return; + static bool thunk(RE::Character* a_this) + { + logger::info("Death: ShouldBackgroundClone({})", *a_this); + return func(a_this); } + static inline REL::Relocation func; - if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { - scripts->AddEventSink(GetSingleton()); - logger::info("Registered for {}", typeid(RE::TESDeathEvent).name()); - } + static inline constexpr std::size_t index{ 0 }; + static inline constexpr std::size_t size{ 0x6D }; + }; + + void Manager::HandleMessage(SKSE::MessagingInterface::Message* message) + { + switch (message->type) { + case SKSE::MessagingInterface::kDataLoaded: + if (INI::deathConfigs.empty()) { + return; + } - // Create tag keywords - if (const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType()) { - if (SPID_Dead = factory->Create(); SPID_Dead) { - SPID_Dead->formEditorID = "SPID_Dead"; + if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { + scripts->AddEventSink(this); + logger::info("Death Distribution: Registered for {}.", typeid(RE::TESDeathEvent).name()); } - } - assert(SPID_Dead); - } + // TODO: Death Distribution should hook ShouldBackgroundClone to perform distribution of NPCs that are loaded dead. + // This will allow to decouple regular distribution from death distribution completely. Just for aesthetics :) + // + // Current challenge is that hooks are called in the reverse order of their installation, + // so this would require also refactoring existing MessageHandler to this new system with specialized Manager::HandleMessage. + // + // stl::write_vfunc(); + // logger::info("Death Distribution: Installed ShouldBackgroundClone hook."); + + // Create tag keywords + if (const auto factory = RE::IFormFactory::GetConcreteFormFactoryByType()) { + if (SPID_Dead = factory->Create(); SPID_Dead) { + SPID_Dead->formEditorID = "SPID_Dead"; + logger::info("Death Distribution: Created SPID_Dead keyword."); + } + } + assert(SPID_Dead); + break; + default: + break; + } + } + void Manager::Distribute(NPCData& data) { assert(data.IsDead() || data.IsDying()); + // TODO: Test that we actually hit that log warning. Doesn't seem like it's possible, and keyword can be removed. // We mark NPCs that were processed by Death Distribution with SPID_Dead keyword, // to ensure that NPCs who received Death Distribution once won't get another Death Distribution // (which might happen if cell or game is reloaded with dead NPC laying there) - if (data.GetNPC()->HasKeyword(SPID_Dead)) + if (data.GetNPC()->HasKeyword(SPID_Dead)) { + logger::warn("NPC {} already processed by Death Distribution", *(data.GetActor())); return; + } data.GetNPC()->AddKeyword(SPID_Dead); @@ -234,11 +266,11 @@ namespace DeathDistribution skins.GetForms() }; - Distribute::Distribute(data, input, entries, false, &distributedForms); + Distribute::Distribute(data, input, entries, &distributedForms); if (!distributedForms.empty()) { LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(LinkedDistribution::kDeath, distributedForms, [&](Forms::DistributionSet& set) { - Distribute::Distribute(data, input, set, true, &distributedForms); + Distribute::Distribute(data, input, set, &distributedForms); }); } @@ -247,14 +279,15 @@ namespace DeathDistribution RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) { - constexpr auto is_NPC = [](auto&& a_ref) { - return a_ref && !a_ref->IsPlayerRef(); - }; + // TODO: Make it header for Distribution output (e.g. "On Death Distribution for actor {}") + //logger::info("Death: {} {}", a_event->dead ? "Dead" : "Dying", *(a_event->actorDying->As())); + + if (!a_event || a_event->dead) { + return RE::BSEventNotifyControl::kContinue; + } - if (a_event && a_event->dead && is_NPC(a_event->actorDying)) { - const auto actor = a_event->actorDying->As(); - const auto npc = actor ? actor->GetActorBase() : nullptr; - if (actor && npc) { + if (const auto actor = a_event->actorDying->As(); actor && !actor->IsPlayerRef()) { + if (const auto npc = actor->GetActorBase(); npc) { auto npcData = NPCData(actor, npc, true); Distribute(npcData); } diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 03a1897..7d992aa 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -7,7 +7,7 @@ namespace Distribute { - void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms) + void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, DistributedForms* accumulatedForms) { const auto npc = npcData.GetNPC(); @@ -106,7 +106,7 @@ namespace Distribute for_first_form( npcData, forms.outfits, input, [&](auto* a_outfit) { - return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit, allowOverwrites) != Outfits::ReplacementResult::Skipped; // terminate as soon as valid outfit is confirmed. + return Outfits::Manager::GetSingleton()->SetDefaultOutfit(npcData.GetActor(), a_outfit); // terminate as soon as valid outfit is confirmed. }, accumulatedForms); @@ -158,12 +158,12 @@ namespace Distribute DistributedForms distributedForms{}; - Distribute(npcData, input, entries, false, &distributedForms); + Distribute(npcData, input, entries, &distributedForms); if (!distributedForms.empty()) { // TODO: This only does one-level linking. So that linked entries won't trigger another level of distribution. LinkedDistribution::Manager::GetSingleton()->ForEachLinkedDistributionSet(LinkedDistribution::kRegular, distributedForms, [&](Forms::DistributionSet& set) { - Distribute(npcData, input, set, true, &distributedForms); + Distribute(npcData, input, set, &distributedForms); }); } diff --git a/SPID/src/DistributeManager.cpp b/SPID/src/DistributeManager.cpp index 72448d2..ca81d5e 100644 --- a/SPID/src/DistributeManager.cpp +++ b/SPID/src/DistributeManager.cpp @@ -31,7 +31,7 @@ namespace Distribute { static bool thunk(RE::Character* a_this) { - //logger::info("ShouldBackgroundClone({})", *a_this); + //logger::info("Distribute: ShouldBackgroundClone({})", *a_this); if (const auto npc = a_this->GetActorBase()) { detail::distribute_on_load(a_this, npc); } diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 34bd10a..0bedb69 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -28,7 +28,7 @@ namespace NPC return formID == a_formID; } - Data::Data(RE::Actor* a_actor, RE::TESNPC* a_npc, bool isDying = false) : + Data::Data(RE::Actor* a_actor, RE::TESNPC* a_npc, bool isDying) : npc(a_npc), actor(a_actor), race(a_actor->GetRace()), @@ -234,7 +234,7 @@ namespace NPC bool Data::IsDying() const { - return isDying; + return dying; } bool Data::StartsDead() const diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 4fb955a..0928ab9 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -2,6 +2,8 @@ namespace Outfits { +#pragma region Serialization + constexpr std::uint32_t serializationKey = 'SPID'; constexpr std::uint32_t serializationVersion = 1; @@ -115,16 +117,170 @@ namespace Outfits } } - void Manager::Register() + void Manager::Load(SKSE::SerializationInterface* a_interface) + { +#ifndef NDEBUG + logger::info("{:*^30}", "LOADING"); +#endif + auto* manager = Manager::GetSingleton(); + + std::unordered_map loadedReplacements; + 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; + 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(), total); + for (const auto& pair : loadedReplacements) { + logger::info("\t{}", *pair.first); + logger::info("\t\t{}", pair.second); + } + + 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 npc = actor->GetActorBase(); + 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) + ++updatedCount; +#ifndef NDEBUG + logger::info("\tUpdating Outfit Replacement for {}", *actor); + logger::info("\t\t{}", newIt->second); +#endif + } else if (!replacement.distributed || 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); +#endif + if (manager->ApplyOutfit(actor, replacement.original)) { + ++revertedCount; + } + } + } +#ifndef NDEBUG + if (revertedCount) { + logger::info("Reverted {} no longer existing Outfit Replacements", revertedCount); + } + if (updatedCount) { + logger::info("Updated {} existing Outfit Replacements", updatedCount); + } + logger::info("{:*^30}", "RUNTIME"); // continue runtime logging section +#endif + } + + void Manager::Save(SKSE::SerializationInterface* interface) { - const auto serializationInterface = SKSE::GetSerializationInterface(); - serializationInterface->SetUniqueID(serializationKey); - serializationInterface->SetSaveCallback(Save); - serializationInterface->SetLoadCallback(Load); - - if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { - scripts->AddEventSink(GetSingleton()); - logger::info("Registered Outfit Manager for {}", typeid(RE::TESFormDeleteEvent).name()); +#ifndef NDEBUG + logger::info("{:*^30}", "SAVING"); +#endif + auto replacements = Manager::GetSingleton()->replacements; +#ifndef NDEBUG + logger::info("Saving {} distributed outfits...", replacements.size()); +#endif + std::uint32_t savedCount = 0; + for (const auto& pair : replacements) { + if (!Data::Save(interface, pair.first, pair.second.original, pair.second.distributed)) { +#ifndef NDEBUG + if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { + logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *actor); + } +#endif + continue; + } +#ifndef NDEBUG + if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { + logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *actor); + } +#endif + ++savedCount; + } +#ifndef NDEBUG + logger::info("Saved {} replacements", savedCount); +#endif + } +#pragma endregion + +#pragma region Hooks + struct Load3D + { + static RE::NiAVObject* thunk(RE::Character* a_this, bool a_backgroundLoading) + { +#ifndef NDEBUG + logger::info("Load3D({})", *a_this); +#endif + const auto manager = Manager::GetSingleton(); + if (!manager->isLoadingGame) { + manager->ApplyOutfit(a_this); + } + + return func(a_this, a_backgroundLoading); + } + static inline REL::Relocation func; + + static inline constexpr std::size_t index{ 0 }; + static inline constexpr std::size_t size{ 0x6A }; + }; + + void Manager::HandleMessage(SKSE::MessagingInterface::Message* message) + { + switch (message->type) { + case SKSE::MessagingInterface::kDataLoaded: + { + const auto serializationInterface = SKSE::GetSerializationInterface(); + serializationInterface->SetUniqueID(serializationKey); + serializationInterface->SetSaveCallback(Save); + serializationInterface->SetLoadCallback(Load); + + if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { + scripts->AddEventSink(this); + logger::info("Outfit Manager: Registered for {}.", typeid(RE::TESFormDeleteEvent).name()); + } + + if (const auto scripts = RE::ScriptEventSourceHolder::GetSingleton()) { + scripts->AddEventSink(this); + logger::info("Outfit Manager: Registered for {}.", typeid(RE::TESDeathEvent).name()); + } + + stl::write_vfunc(); + logger::info("Outfit Manager: Installed Load3D hook."); + } + break; + case SKSE::MessagingInterface::kPreLoadGame: + isLoadingGame = true; + break; + case SKSE::MessagingInterface::kPostLoadGame: + isLoadingGame = false; + break; + default: + break; } } @@ -136,59 +292,65 @@ namespace Outfits return RE::BSEventNotifyControl::kContinue; } - ReplacementResult Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit, bool allowOverwrites) + RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESDeathEvent* a_event, RE::BSTEventSource*) + { + //logger::info("Outfits: {} {}", a_event->dead ? "Dead" : "Dying", *(a_event->actorDying->As())); + + if (!a_event || a_event->dead) { + return RE::BSEventNotifyControl::kContinue; + } + + if (const auto actor = a_event->actorDying->As(); actor && !actor->IsPlayerRef()) { + ApplyOutfit(actor); + } + + return RE::BSEventNotifyControl::kContinue; + } +#pragma endregion + +#pragma region Outfit Management + bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) { if (!actor || !outfit) { // invalid call - return ReplacementResult::Skipped; + return false; } const auto npc = actor->GetActorBase(); if (!npc) { - return ReplacementResult::Skipped; + return false; } const auto defaultOutfit = npc->defaultOutfit; #ifndef NDEBUG logger::info("Evaluating outfit for {}", *actor); - logger::info("\tNew Outfit: {}", *outfit); if (defaultOutfit) { logger::info("\tCurrent Outfit: {}", *defaultOutfit); } else { logger::info("\tCurrent Outfit: None"); } + logger::info("\tNew Outfit: {}", *outfit); #endif if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement #ifndef NDEBUG logger::info("\tFound existing replacement {}", existing->second); #endif - if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. + if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. #ifndef NDEBUG logger::info("\tExisting replacement is already set"); #endif - return ReplacementResult::Set; - } else if (!allowOverwrites) { // if we are trying to set any other outfit and overwrites are not allowed, we skip it, indicating overwriting status. -#ifndef NDEBUG - logger::info("\tOverwriting outfit is not allowed"); -#endif - return ReplacementResult::NotOverwrittable; + return true; } } - // TODO: Try to not equip outfit right away. Only store the replacement. The actual distribution will happen in Load3D hook here - // For dynamic distributions such as On Death Distribution, we can use a boolean parameter "forceEquip" to equip the outfit right away. - // This will add proper support for linked distribution where one outfit should be distributed immediately after another one. - if (!CanEquipOutfit(actor, outfit)) { #ifndef NDEBUG logger::warn("\tAttempted to equip Outfit {} that can't be worn by given actor.", *outfit); #endif - return ReplacementResult::Skipped; + return false; } - SetDefaultOutfit(actor, outfit); - if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { previous->second.distributed = outfit; #ifndef NDEBUG @@ -201,7 +363,21 @@ namespace Outfits #endif } - return ReplacementResult::Set; + return true; + } + + bool Manager::ApplyOutfit(RE::Actor* actor) + { + if (!actor) + return false; + + if (const auto replacement = replacements.find(actor->formID); replacement != replacements.end()) { + if (replacement->second.distributed) { + return ApplyOutfit(actor, replacement->second.distributed); + } + } + + return false; } bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) @@ -242,7 +418,7 @@ namespace Outfits } } - bool Manager::SetDefaultOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) + bool Manager::ApplyOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) { if (!actor || !outfit) { return false; @@ -262,114 +438,5 @@ namespace Outfits } return true; } - - void Manager::Load(SKSE::SerializationInterface* a_interface) - { -#ifndef NDEBUG - logger::info("{:*^30}", "LOADING"); -#endif - auto* manager = Manager::GetSingleton(); - - std::unordered_map loadedReplacements; - 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; - 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(), total); - for (const auto& pair : loadedReplacements) { - logger::info("\t{}", *pair.first); - logger::info("\t\t{}", pair.second); - } - - 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 npc = actor->GetActorBase(); - const auto& replacement = it.second; - logger::info("\t\tActual outfit on NPC: {}", *(npc->defaultOutfit)); - 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 (!replacement.distributed || 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); -#endif - if (manager->SetDefaultOutfit(actor, replacement.original)) { - ++revertedCount; - } - } - } -#ifndef NDEBUG - if (revertedCount) { - logger::info("Reverted {} no longer existing Outfit Replacements", revertedCount); - } - if (updatedCount) { - logger::info("Updated {} existing Outfit Replacements", updatedCount); - } -#endif - } - - void Manager::Save(SKSE::SerializationInterface* interface) - { -#ifndef NDEBUG - logger::info("{:*^30}", "SAVING"); -#endif - auto replacements = Manager::GetSingleton()->replacements; -#ifndef NDEBUG - logger::info("Saving {} distributed outfits...", replacements.size()); -#endif - std::uint32_t savedCount = 0; - for (const auto& pair : replacements) { - if (!Data::Save(interface, pair.first, pair.second.original, pair.second.distributed)) { -#ifndef NDEBUG - if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { - logger::error("Failed to save Outfit Replacement ({}) for {}", pair.second, *actor); - } -#endif - continue; - } -#ifndef NDEBUG - if (const auto actor = RE::TESForm::LookupByID(pair.first); actor) { - logger::info("\tSaved Outfit Replacement ({}) for actor {}", pair.second, *actor); - } -#endif - ++savedCount; - } -#ifndef NDEBUG - logger::info("Saved {} replacements", savedCount); -#endif - } +#pragma endregion } diff --git a/SPID/src/main.cpp b/SPID/src/main.cpp index f62162d..1b945f6 100644 --- a/SPID/src/main.cpp +++ b/SPID/src/main.cpp @@ -40,12 +40,9 @@ 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()); @@ -70,6 +67,8 @@ void MessageHandler(SKSE::MessagingInterface::Message* a_message) default: break; } + DeathDistribution::Manager::GetSingleton()->HandleMessage(a_message); + Outfits::Manager::GetSingleton()->HandleMessage(a_message); } #ifdef SKYRIM_AE From 4ec6f5e0cacaacab9dc1e4c43081f818a2dec13e Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Thu, 26 Dec 2024 22:54:04 +0200 Subject: [PATCH 38/43] Added tracking of initial outfits set in the plugin. This solves the problem where filters that reference outfits stopped working after another outfit was distributed. --- SPID/include/OutfitManager.h | 35 +++++++++++++++++++---- SPID/src/LookupNPC.cpp | 2 ++ SPID/src/OutfitManager.cpp | 55 ++++++++++++++++++++++++++++++------ 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 3abcd10..a645cb5 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -9,6 +9,19 @@ namespace Outfits { public: void HandleMessage(SKSE::MessagingInterface::Message*); + + // TODO: This method should check both initial and distributed outfits in SPID 8 or something. + /// + /// Checks whether the NPC uses specified outfit as the default outfit. + /// + /// This method checks initial outfit which was set in the plugins. + /// Outfits distributed on top of the initial outfits won't be recognized in SPID 7. + /// There should be a way to handle both of them in the future. + /// + /// Target NPC to be tested + /// An outfit that needs to be checked + /// True if specified outfit was used as the default outift by the NPC + bool HasDefaultOutfit(RE::TESNPC*, RE::BGSOutfit*) const; /// /// Checks whether the actor can technically wear a given outfit. @@ -19,7 +32,7 @@ namespace Outfits /// Target Actor to be tested /// An outfit that needs to be equipped /// True if the actor can wear the outfit, false otherwise - bool CanEquipOutfit(const RE::Actor*, RE::BGSOutfit*); + bool CanEquipOutfit(const RE::Actor*, RE::BGSOutfit*) const; /// /// Sets given outfit as default outfit for the actor. @@ -51,7 +64,7 @@ namespace Outfits /// /// Actor for whom outfit should be changed /// True if the outfit was successfully set, false otherwise - bool ApplyOutfit(RE::Actor*); + bool ApplyOutfit(RE::Actor*) const; protected: RE::BSEventNotifyControl ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) override; @@ -71,10 +84,10 @@ namespace Outfits /// Actor for whom outfit should be changed /// The outfit to be set /// True if the outfit was successfully set, false otherwise - bool ApplyOutfit(RE::Actor*, RE::BGSOutfit*); + bool ApplyOutfit(RE::Actor*, RE::BGSOutfit*) const; /// This re-creates game's function that performs a similar code, but crashes for unknown reasons :) - void AddWornOutfit(RE::Actor*, const RE::BGSOutfit*); + void AddWornOutfit(RE::Actor*, const RE::BGSOutfit*) const; struct OutfitReplacement { @@ -88,13 +101,14 @@ namespace Outfits RE::FormID unrecognizedDistributedFormID; OutfitReplacement() = default; - OutfitReplacement(RE::BGSOutfit* original, RE::FormID unrecognizedDistributedFormID) : + OutfitReplacement(RE::BGSOutfit* original, RE::FormID unrecognizedDistributedFormID = 0) : original(original), distributed(nullptr), unrecognizedDistributedFormID(unrecognizedDistributedFormID) {} OutfitReplacement(RE::BGSOutfit* original, RE::BGSOutfit* distributed) : original(original), distributed(distributed), unrecognizedDistributedFormID(0) {} }; friend struct Load3D; + friend struct LoadGame; friend fmt::formatter; @@ -106,6 +120,17 @@ namespace Outfits /// std::unordered_map replacements; + /// + /// Map of NPC's FormID and corresponding initial Outfit that is set in loaded plugins. + /// + /// This map is used for filtering during distribution to be able to provide consistent filtering behavior. + /// Once the Manager applies new outfit, all filters that use initial outfit of this NPC will stop working, + /// the reason is that distributed outfits are baked into the save, and are loaded as part of NPC. + /// + /// The map is constructed with TESNPC::LoadGame hook, at which point defaultOutfit hasn't been loaded yet from the save. + /// + std::unordered_map initialOutfits; + /// /// Flag indicating whether there is a loading of a save file in progress. /// diff --git a/SPID/src/LookupNPC.cpp b/SPID/src/LookupNPC.cpp index 0bedb69..9db93c6 100644 --- a/SPID/src/LookupNPC.cpp +++ b/SPID/src/LookupNPC.cpp @@ -1,5 +1,6 @@ #include "LookupNPC.h" #include "ExclusiveGroups.h" +#include "OutfitManager.h" namespace NPC { @@ -130,6 +131,7 @@ namespace NPC case RE::FormType::Race: return GetRace() == a_form; case RE::FormType::Outfit: + return Outfits::Manager::GetSingleton()->HasDefaultOutfit(npc, a_form->As()); return npc->defaultOutfit == a_form; case RE::FormType::NPC: return npc == a_form || std::ranges::any_of(IDs, [&](const auto& ID) { return ID == a_form->GetFormID(); }); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 0928ab9..81d19cf 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -87,18 +87,18 @@ namespace Outfits failedDistributedOutfitFormID = 0; if (!Load(interface, loadedActor, id)) { - logger::warn("Failed to load Outfit Replacement record: Corrupted actor [{:08X}].", id); + logger::warn("Failed to load Outfit Replacement record: Unknown actor [{:08X}].", id); return false; } if (!Load(interface, loadedOriginalOutfit, id)) { - logger::warn("Failed to load Outfit Replacement record: Corrupted original outfit [{:08X}].", id); + logger::warn("Failed to load Outfit Replacement record: Unknown original outfit [{:08X}].", id); return false; } if (!Load(interface, loadedDistributedOutfit, id)) { failedDistributedOutfitFormID = id; - logger::warn("Failed to load Outfit Replacement record: Corrupted distributed outfit [{:08X}].", id); + logger::warn("Failed to load Outfit Replacement record: Unknown distributed outfit [{:08X}].", id); return false; } @@ -229,12 +229,13 @@ namespace Outfits #pragma endregion #pragma region Hooks + /// This hook appliues pending outfit replacements before loading 3D model. Outfit Replacements are created by SetDefaultOutfit. struct Load3D { static RE::NiAVObject* thunk(RE::Character* a_this, bool a_backgroundLoading) { #ifndef NDEBUG - logger::info("Load3D({})", *a_this); + //logger::info("Load3D({})", *a_this); #endif const auto manager = Manager::GetSingleton(); if (!manager->isLoadingGame) { @@ -249,6 +250,27 @@ namespace Outfits static inline constexpr std::size_t size{ 0x6A }; }; + /// This hook builds a map of initialOutfits for all NPCs that have different outfit in the save. + struct LoadGame + { + static void thunk(RE::TESNPC* npc, RE::BGSLoadFormBuffer* a_buf) + { +#ifndef NDEBUG + //logger::info("LoadGame({})", *npc); +#endif + auto initialOutfit = npc->defaultOutfit; + func(npc, a_buf); + if (initialOutfit && initialOutfit != npc->defaultOutfit) { + Manager::GetSingleton()->initialOutfits.try_emplace(npc->formID, initialOutfit); + } + } + + static inline REL::Relocation func; + + static inline constexpr std::size_t index{ 0 }; + static inline constexpr std::size_t size{ 0xF }; + }; + void Manager::HandleMessage(SKSE::MessagingInterface::Message* message) { switch (message->type) { @@ -271,6 +293,9 @@ namespace Outfits stl::write_vfunc(); logger::info("Outfit Manager: Installed Load3D hook."); + + stl::write_vfunc(); + logger::info("Outfit Manager: Installed LoadGame hook."); } break; case SKSE::MessagingInterface::kPreLoadGame: @@ -288,6 +313,7 @@ namespace Outfits { if (a_event && a_event->formID != 0) { replacements.erase(a_event->formID); + initialOutfits.erase(a_event->formID); } return RE::BSEventNotifyControl::kContinue; } @@ -366,7 +392,7 @@ namespace Outfits return true; } - bool Manager::ApplyOutfit(RE::Actor* actor) + bool Manager::ApplyOutfit(RE::Actor* actor) const { if (!actor) return false; @@ -380,7 +406,20 @@ namespace Outfits return false; } - bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) + bool Manager::HasDefaultOutfit(RE::TESNPC* npc, RE::BGSOutfit* outfit) const + { + if (!outfit) { + return false; + } + + if (auto existing = initialOutfits.find(npc->formID); existing != initialOutfits.end()) { + return existing->second == outfit; + } + + return npc->defaultOutfit == outfit; + } + + bool Manager::CanEquipOutfit(const RE::Actor* actor, RE::BGSOutfit* outfit) const { const auto race = actor->GetRace(); for (const auto& item : outfit->outfitItems) { @@ -396,7 +435,7 @@ namespace Outfits return true; } - void Manager::AddWornOutfit(RE::Actor* actor, const RE::BGSOutfit* a_outfit) + void Manager::AddWornOutfit(RE::Actor* actor, const RE::BGSOutfit* a_outfit) const { if (const auto invChanges = actor->GetInventoryChanges()) { if (const auto entryLists = invChanges->entryList) { @@ -418,7 +457,7 @@ namespace Outfits } } - bool Manager::ApplyOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) + bool Manager::ApplyOutfit(RE::Actor* actor, RE::BGSOutfit* outfit) const { if (!actor || !outfit) { return false; From a2cbe06d4e8a77db5d39d074890c42d40bdedd9f Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Fri, 27 Dec 2024 00:28:41 +0200 Subject: [PATCH 39/43] #60 Fixed spells do not apply when distributed on death --- SPID/src/Distribute.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 7d992aa..df72be7 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -34,7 +34,9 @@ namespace Distribute for_each_form( npcData, forms.spells, input, [&](const std::vector& a_spells) { - npc->GetSpellList()->AddSpells(a_spells); + for (auto& spell : a_spells) { + npcData.GetActor()->AddSpell(spell); // Adding spells one by one to actor properly applies them. This solves On Death distribution issue #60 + } }, accumulatedForms); From 4d0ebd4573239d5413be50da3d20673f393bd4b4 Mon Sep 17 00:00:00 2001 From: adya Date: Thu, 26 Dec 2024 22:29:06 +0000 Subject: [PATCH 40/43] maintenance --- SPID/include/LookupNPC.h | 14 +++++++------- SPID/include/OutfitManager.h | 22 +++++++++++----------- SPID/src/DeathDistribution.cpp | 8 ++++---- SPID/src/Distribute.cpp | 2 +- SPID/src/OutfitManager.cpp | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/SPID/include/LookupNPC.h b/SPID/include/LookupNPC.h index edad49c..54177b7 100644 --- a/SPID/include/LookupNPC.h +++ b/SPID/include/LookupNPC.h @@ -33,21 +33,21 @@ namespace NPC /// /// Flag indicating whether given NPC is dead. - /// + /// /// IsDead returns true when either NPC starts dead or has already died. See IsDying for more details. /// - [[nodiscard]] bool IsDead() const; - + [[nodiscard]] bool IsDead() const; + /// /// Flag indicating whether given NPC is currently dying. - /// - /// This is detected with RE::TESDeathEvent. + /// + /// This is detected with RE::TESDeathEvent. /// It is called twice for each dying Actor, first when they are dying and second when they are dead. /// When IsDying is true, IsDead will remain false. /// Once actor IsDead IsDying will be false. /// - [[nodiscard]] bool IsDying() const; - [[nodiscard]] bool StartsDead() const; + [[nodiscard]] bool IsDying() const; + [[nodiscard]] bool StartsDead() const; [[nodiscard]] RE::TESRace* GetRace() const; diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index a645cb5..986388f 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -9,13 +9,13 @@ namespace Outfits { public: void HandleMessage(SKSE::MessagingInterface::Message*); - + // TODO: This method should check both initial and distributed outfits in SPID 8 or something. /// /// Checks whether the NPC uses specified outfit as the default outfit. - /// - /// This method checks initial outfit which was set in the plugins. - /// Outfits distributed on top of the initial outfits won't be recognized in SPID 7. + /// + /// This method checks initial outfit which was set in the plugins. + /// Outfits distributed on top of the initial outfits won't be recognized in SPID 7. /// There should be a way to handle both of them in the future. /// /// Target NPC to be tested @@ -39,7 +39,7 @@ namespace Outfits /// /// SetDefaultOutfit does not apply outfits immediately, it only registers them within the manager. /// ApplyOutfit must be called afterwards to actually equip the outfit. - /// + /// /// For actors that are loading as usual ApplyOutfit will be called automatically during their Load3D. /// For Death Distribution, ApplyOutfit will be called automatically during the event processing. /// For other runtime distributions, ApplyOutfit should be called manually. @@ -52,9 +52,9 @@ namespace Outfits /// /// Applies tracked outfit replacement for given actor. - /// + /// /// SetDefaultOutfit does not apply outfits immediately, it only registers them within the manager. - /// + /// /// For actors that are loading as usual ApplyOutfit will be called automatically during their Load3D. /// For Death Distribution, ApplyOutfit will be called automatically during the event processing. /// For other runtime distributions, ApplyOutfit should be called manually. @@ -114,7 +114,7 @@ namespace Outfits /// /// Map of Actor's FormID and corresponding Outfit Replacements that are being tracked by the manager. - /// + /// /// This map is serialized in a co-save and is used to clean up no longer distributed outfits when loading a previous save. /// Latest distribution replacements always take priority over the ones stored in the save file. /// @@ -122,18 +122,18 @@ namespace Outfits /// /// Map of NPC's FormID and corresponding initial Outfit that is set in loaded plugins. - /// + /// /// This map is used for filtering during distribution to be able to provide consistent filtering behavior. /// Once the Manager applies new outfit, all filters that use initial outfit of this NPC will stop working, /// the reason is that distributed outfits are baked into the save, and are loaded as part of NPC. - /// + /// /// The map is constructed with TESNPC::LoadGame hook, at which point defaultOutfit hasn't been loaded yet from the save. /// std::unordered_map initialOutfits; /// /// Flag indicating whether there is a loading of a save file in progress. - /// + /// /// This flag is used to defer equipping outfits in Load3D hook, until after LoadGame event is processed. /// By doing so we can properly handle state of the outfits and determine if anything needs to be equipped. /// Among other things, this allows to avoid re-equipping looted outfits On Death Distribution after relaunching the game. diff --git a/SPID/src/DeathDistribution.cpp b/SPID/src/DeathDistribution.cpp index 1040474..2ca7016 100644 --- a/SPID/src/DeathDistribution.cpp +++ b/SPID/src/DeathDistribution.cpp @@ -211,10 +211,10 @@ namespace DeathDistribution // TODO: Death Distribution should hook ShouldBackgroundClone to perform distribution of NPCs that are loaded dead. // This will allow to decouple regular distribution from death distribution completely. Just for aesthetics :) - // - // Current challenge is that hooks are called in the reverse order of their installation, + // + // Current challenge is that hooks are called in the reverse order of their installation, // so this would require also refactoring existing MessageHandler to this new system with specialized Manager::HandleMessage. - // + // // stl::write_vfunc(); // logger::info("Death Distribution: Installed ShouldBackgroundClone hook."); @@ -232,7 +232,7 @@ namespace DeathDistribution break; } } - + void Manager::Distribute(NPCData& data) { assert(data.IsDead() || data.IsDying()); diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index df72be7..7f39d78 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -35,7 +35,7 @@ namespace Distribute for_each_form( npcData, forms.spells, input, [&](const std::vector& a_spells) { for (auto& spell : a_spells) { - npcData.GetActor()->AddSpell(spell); // Adding spells one by one to actor properly applies them. This solves On Death distribution issue #60 + npcData.GetActor()->AddSpell(spell); // Adding spells one by one to actor properly applies them. This solves On Death distribution issue #60 } }, accumulatedForms); diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index 81d19cf..a8e87e8 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -410,7 +410,7 @@ namespace Outfits { if (!outfit) { return false; - } + } if (auto existing = initialOutfits.find(npc->formID); existing != initialOutfits.end()) { return existing->second == outfit; From ed5a81e1f0bf84ea338695c9496775842caaf8f7 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 28 Dec 2024 22:11:08 +0200 Subject: [PATCH 41/43] Ensured that dead NPCs don't equip outfits. - Added locking in OutfitManager for extra safety. --- SPID/include/OutfitManager.h | 5 +++ SPID/include/PCH.h | 4 ++ SPID/include/PCLevelMultManager.h | 3 -- SPID/src/Distribute.cpp | 4 ++ SPID/src/OutfitManager.cpp | 72 ++++++++++++++++++++++--------- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/SPID/include/OutfitManager.h b/SPID/include/OutfitManager.h index 986388f..06cd9b5 100644 --- a/SPID/include/OutfitManager.h +++ b/SPID/include/OutfitManager.h @@ -112,6 +112,11 @@ namespace Outfits friend fmt::formatter; + /// + /// Lock for replacements. + /// + mutable Lock _lock; + /// /// Map of Actor's FormID and corresponding Outfit Replacements that are being tracked by the manager. /// diff --git a/SPID/include/PCH.h b/SPID/include/PCH.h index ba76683..0e6bc89 100644 --- a/SPID/include/PCH.h +++ b/SPID/include/PCH.h @@ -48,6 +48,10 @@ using Map = ankerl::unordered_dense::map; template using Set = ankerl::unordered_dense::set; +using Lock = std::shared_mutex; +using ReadLocker = std::shared_lock; +using WriteLocker = std::unique_lock; + struct string_hash { using is_transparent = void; // enable heterogeneous overloads diff --git a/SPID/include/PCLevelMultManager.h b/SPID/include/PCLevelMultManager.h index 330bfaa..41ec098 100644 --- a/SPID/include/PCLevelMultManager.h +++ b/SPID/include/PCLevelMultManager.h @@ -39,9 +39,6 @@ namespace PCLevelMult void SetNewGameStarted(); private: - using Lock = std::shared_mutex; - using ReadLocker = std::shared_lock; - using WriteLocker = std::unique_lock; enum class LEVEL_CAP_STATE { diff --git a/SPID/src/Distribute.cpp b/SPID/src/Distribute.cpp index 7f39d78..649066d 100644 --- a/SPID/src/Distribute.cpp +++ b/SPID/src/Distribute.cpp @@ -175,8 +175,12 @@ namespace Distribute void Distribute(NPCData& npcData, bool onlyLeveledEntries) { const auto input = PCLevelMult::Input{ npcData.GetActor(), npcData.GetNPC(), onlyLeveledEntries }; + + // We always do the normal distribution even for Dead NPCs, + // if Distributable Form is only meant to be distributed while NPC is alive, the entry must contain -D filter. Distribute(npcData, input); + // TODO: This will be moved to DeathDistribution's own hook. if (npcData.IsDead()) { // If NPC is already dead, perform the On Death Distribution. DeathDistribution::Manager::GetSingleton()->Distribute(npcData); } diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index a8e87e8..f5e1644 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -124,6 +124,8 @@ namespace Outfits #endif auto* manager = Manager::GetSingleton(); + ReadLocker lock(manager->_lock); + std::unordered_map loadedReplacements; auto& newReplacements = manager->replacements; @@ -201,7 +203,9 @@ namespace Outfits #ifndef NDEBUG logger::info("{:*^30}", "SAVING"); #endif - auto replacements = Manager::GetSingleton()->replacements; + auto manager = Manager::GetSingleton(); + ReadLocker lock(manager->_lock); + const auto& replacements = manager->replacements; #ifndef NDEBUG logger::info("Saving {} distributed outfits...", replacements.size()); #endif @@ -232,17 +236,19 @@ namespace Outfits /// This hook appliues pending outfit replacements before loading 3D model. Outfit Replacements are created by SetDefaultOutfit. struct Load3D { - static RE::NiAVObject* thunk(RE::Character* a_this, bool a_backgroundLoading) + static RE::NiAVObject* thunk(RE::Character* actor, bool a_backgroundLoading) { #ifndef NDEBUG - //logger::info("Load3D({})", *a_this); + logger::info("Load3D({}); Background: {}", *actor, a_backgroundLoading); #endif - const auto manager = Manager::GetSingleton(); - if (!manager->isLoadingGame) { - manager->ApplyOutfit(a_this); + if (!Manager::GetSingleton()->isLoadingGame) { + // Wrapping in a task maybe possibly perhaps would fix the crash in issue #67 + SKSE::GetTaskInterface()->AddTask([actor]() { + Manager::GetSingleton()->ApplyOutfit(actor); + }); } - return func(a_this, a_backgroundLoading); + return func(actor, a_backgroundLoading); } static inline REL::Relocation func; @@ -311,6 +317,7 @@ namespace Outfits RE::BSEventNotifyControl Manager::ProcessEvent(const RE::TESFormDeleteEvent* a_event, RE::BSTEventSource*) { + WriteLocker lock(_lock); if (a_event && a_event->formID != 0) { replacements.erase(a_event->formID); initialOutfits.erase(a_event->formID); @@ -358,35 +365,58 @@ namespace Outfits } logger::info("\tNew Outfit: {}", *outfit); #endif + WriteLocker lock(_lock); if (auto existing = replacements.find(actor->formID); existing != replacements.end()) { // we already have tracked replacement #ifndef NDEBUG logger::info("\tFound existing replacement {}", existing->second); #endif - if (outfit == defaultOutfit && outfit == existing->second.distributed) { // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. + // If we have an existing replacement and actor is already dead, + // then we don't want to set new outfit to avoid sudden changes that player might not expect. + // But only if the outfit was already given to them. + // This will allow to apply initial outfit to the dead actor in case new mod was added or something. + // + // TODO: Consider tracking looting state of the outfit? + // e.g. if an actor still wears all parts of the outfit, then allow to change it. + // This might be unexpected, since dead NPCs are supposed to have their outfit locked. + if (actor->IsDead()) { +#ifndef NDEBUG + logger::info("\tDead NPCs can't change the outfit"); +#endif + return false; + } + + // if the outfit we are trying to set is already the default one and we have a replacement for it, then we confirm that it was set. + if (outfit == defaultOutfit && outfit == existing->second.distributed) { #ifndef NDEBUG logger::info("\tExisting replacement is already set"); #endif return true; } - } - if (!CanEquipOutfit(actor, outfit)) { + if (!CanEquipOutfit(actor, outfit)) { #ifndef NDEBUG - logger::warn("\tAttempted to equip Outfit {} that can't be worn by given actor.", *outfit); + logger::warn("\tAttempted to equip Outfit {} that can't be worn by given actor.", *outfit); #endif - return false; - } - - if (auto previous = replacements.find(actor->formID); previous != replacements.end()) { - previous->second.distributed = outfit; + return false; + } + existing->second.distributed = outfit; +#ifndef NDEBUG + logger::warn("\tUpdated replacement {}", existing->second); +#endif + } else { + if (!CanEquipOutfit(actor, outfit)) { #ifndef NDEBUG - logger::warn("\tUpdated replacement {}", previous->second); + logger::warn("\tAttempted to equip Outfit {} that can't be worn by given actor.", *outfit); #endif - } else if (defaultOutfit) { - replacements.try_emplace(actor->formID, defaultOutfit, outfit); + return false; + } + + if (defaultOutfit) { + replacements.try_emplace(actor->formID, defaultOutfit, outfit); #ifndef NDEBUG - logger::warn("\tAdded replacement {}", OutfitReplacement(defaultOutfit, outfit)); + logger::warn("\tAdded replacement {}", OutfitReplacement(defaultOutfit, outfit)); #endif + } } return true; @@ -396,7 +426,7 @@ namespace Outfits { if (!actor) return false; - + ReadLocker lock(_lock); if (const auto replacement = replacements.find(actor->formID); replacement != replacements.end()) { if (replacement->second.distributed) { return ApplyOutfit(actor, replacement->second.distributed); From ed1e4e88fa009f2ffe35acd15c0e69410cbbe380 Mon Sep 17 00:00:00 2001 From: adya Date: Sat, 28 Dec 2024 20:11:54 +0000 Subject: [PATCH 42/43] maintenance --- SPID/include/PCLevelMultManager.h | 1 - SPID/src/OutfitManager.cpp | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/SPID/include/PCLevelMultManager.h b/SPID/include/PCLevelMultManager.h index 41ec098..e4ed7ad 100644 --- a/SPID/include/PCLevelMultManager.h +++ b/SPID/include/PCLevelMultManager.h @@ -39,7 +39,6 @@ namespace PCLevelMult void SetNewGameStarted(); private: - enum class LEVEL_CAP_STATE { kNotHit = 0, diff --git a/SPID/src/OutfitManager.cpp b/SPID/src/OutfitManager.cpp index f5e1644..a519d61 100644 --- a/SPID/src/OutfitManager.cpp +++ b/SPID/src/OutfitManager.cpp @@ -203,7 +203,7 @@ namespace Outfits #ifndef NDEBUG logger::info("{:*^30}", "SAVING"); #endif - auto manager = Manager::GetSingleton(); + auto manager = Manager::GetSingleton(); ReadLocker lock(manager->_lock); const auto& replacements = manager->replacements; #ifndef NDEBUG @@ -370,12 +370,12 @@ namespace Outfits #ifndef NDEBUG logger::info("\tFound existing replacement {}", existing->second); #endif - // If we have an existing replacement and actor is already dead, + // If we have an existing replacement and actor is already dead, // then we don't want to set new outfit to avoid sudden changes that player might not expect. - // But only if the outfit was already given to them. + // But only if the outfit was already given to them. // This will allow to apply initial outfit to the dead actor in case new mod was added or something. - // - // TODO: Consider tracking looting state of the outfit? + // + // TODO: Consider tracking looting state of the outfit? // e.g. if an actor still wears all parts of the outfit, then allow to change it. // This might be unexpected, since dead NPCs are supposed to have their outfit locked. if (actor->IsDead()) { From 980a5d2583ef27e1040be37eea67899c31a415a5 Mon Sep 17 00:00:00 2001 From: Arkadii Hlushchevskyi Date: Sat, 28 Dec 2024 22:56:37 +0200 Subject: [PATCH 43/43] Changed D trait to work on both Dead and StartsDead actors. --- SPID/include/LookupConfigs.h | 1 - SPID/src/LookupFilters.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/SPID/include/LookupConfigs.h b/SPID/include/LookupConfigs.h index 28c5ddf..2e98b73 100644 --- a/SPID/include/LookupConfigs.h +++ b/SPID/include/LookupConfigs.h @@ -1,5 +1,4 @@ #pragma once -#include "RE/F/FormTypes.h" namespace RECORD { diff --git a/SPID/src/LookupFilters.cpp b/SPID/src/LookupFilters.cpp index c7fbfc3..5f22ef5 100644 --- a/SPID/src/LookupFilters.cpp +++ b/SPID/src/LookupFilters.cpp @@ -166,7 +166,7 @@ namespace Filter if (traits.teammate && a_npcData.IsTeammate() != *traits.teammate) { return Result::kFail; } - if (traits.startsDead && a_npcData.StartsDead() != *traits.startsDead) { + if (traits.startsDead && a_npcData.IsDead() != *traits.startsDead) { return Result::kFail; }