Skip to content

Commit

Permalink
Merge pull request #64 from powerof3/outfits-equipping-with-baked-saves
Browse files Browse the repository at this point in the history
Outfits in SPID 7.2.0
  • Loading branch information
adya authored Dec 28, 2024
2 parents 5906948 + 980a5d2 commit 2c87d07
Show file tree
Hide file tree
Showing 26 changed files with 1,018 additions and 103 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: 'b545373a9a536dc559dac8583467a21497a0e897'
5 changes: 4 additions & 1 deletion .github/workflows/maintenance.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Scripted maintenance

on: push
on:
push:
branches:
- '**'

jobs:
maintenance:
Expand Down
9 changes: 5 additions & 4 deletions SPID/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
cmake_minimum_required(VERSION 3.20)
set(NAME "po3_SpellPerkItemDistributor" CACHE STRING "")
set(VERSION 7.1.3 CACHE STRING "")
set(AE_VERSION 1)
set(VR_VERSION 1)
set(VERSION 7.2.0 CACHE STRING "")
set(SE_VERSION 1597)
set(AE_VERSION 16)
set(VR_VERSION 1415)

# ---- Options ----

Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions SPID/cmake/headerlist.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions SPID/cmake/sourcelist.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions SPID/include/DeathDistribution.h
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#pragma once
#include "FormData.h"
#include "LookupNPC.h"

namespace DeathDistribution
{
namespace INI
{
/// <summary>
/// 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.
/// </summary>
/// <returns>true if given entry was an on death distribuatble form. Note that returned value doesn't represent whether parsing was successful.</returns>
/// <returns>True if given entry was an On Death Distribuatble Form. Note that returned value doesn't represent whether parsing was successful.</returns>
bool TryParse(const std::string& key, const std::string& value, const Path&);
}

Expand All @@ -19,7 +20,7 @@ namespace DeathDistribution
public RE::BSTEventSink<RE::TESDeathEvent>
{
public:
static void Register();
void HandleMessage(SKSE::MessagingInterface::Message*);

/// <summary>
/// Does a forms lookup similar to what Filters do.
Expand All @@ -32,6 +33,14 @@ namespace DeathDistribution

bool IsEmpty();

/// <summary>
/// Performs Death Distribution on a given NPC.
///
/// NPC passed to this method must be Dead in order to be processed.
/// </summary>
/// <param name=""></param>
void Distribute(NPCData&);

private:
Distributables<RE::SpellItem> spells{ RECORD::kSpell };
Distributables<RE::BGSPerk> perks{ RECORD::kPerk };
Expand All @@ -46,7 +55,7 @@ namespace DeathDistribution
Distributables<RE::TESObjectARMO> skins{ RECORD::kSkin };

/// <summary>
/// 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.
/// </summary>
template <typename Func, typename... Args>
void ForEachDistributable(Func&& func, Args&&... args);
Expand Down
54 changes: 54 additions & 0 deletions SPID/include/Defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ struct Traits
std::optional<bool> child{};
std::optional<bool> leveled{};
std::optional<bool> teammate{};
std::optional<bool> startsDead{};
};

using Path = std::string;
Expand Down Expand Up @@ -154,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 <typename Form>
struct formatter<Form, std::enable_if_t<std::is_base_of<RE::TESForm, Form>::value, char>> : fmt::formatter<std::string>
{
template <class ParseContext>
constexpr auto parse(ParseContext& a_ctx)
{
return a_ctx.begin();
}

template <class FormatContext>
constexpr auto format(const Form& form, FormatContext& a_ctx) const
{
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<RE::Actor>
{
template <class ParseContext>
constexpr auto parse(ParseContext& a_ctx)
{
return a_ctx.begin();
}

template <class FormatContext>
constexpr auto format(const RE::Actor& actor, FormatContext& a_ctx) const
{
const auto& form = static_cast<const RE::TESForm&>(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);
}
};

}
22 changes: 16 additions & 6 deletions SPID/include/Distribute.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,22 +98,24 @@ namespace Distribute

#pragma region Outfits, Sleep Outfits, Skins
template <class Form>
void for_each_form(
bool for_first_form(
const NPCData& a_npcData,
Forms::DataVec<Form>& forms,
const PCLevelMult::Input& a_input,
std::function<bool(Form*)> 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

Expand Down Expand Up @@ -228,9 +230,17 @@ namespace Distribute
/// <param name="npcData">General information about NPC that is being processed.</param>
/// <param name="input">Leveling information about NPC that is being processed.</param>
/// <param name="forms">A set of forms that should be distributed to NPC.</param>
/// <param name="allowOverwrites">If true, overwritable forms (like Outfits) will be to overwrite last distributed form on NPC.</param>
/// <param name="accumulatedForms">An optional pointer to a set that will accumulate all distributed forms.</param>
void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, bool allowOverwrites, DistributedForms* accumulatedForms = nullptr);
void Distribute(NPCData& npcData, const PCLevelMult::Input& input);
void Distribute(NPCData& npcData, const PCLevelMult::Input& input, Forms::DistributionSet& forms, DistributedForms* accumulatedForms = nullptr);

/// <summary>
/// Invokes appropriate distribution for given NPC.
///
/// When NPC is dead a Death Distribution will be invoked, otherwise a normal distribution takes place.
/// </summary>
/// <param name="npcData">General information about NPC that is being processed.</param>
/// <param name="onlyLeveledEntries"> Flag indicating that distribution is invoked by a leveling event and only entries with LevelFilters needs to be processed.</param>
void Distribute(NPCData& npcData, bool onlyLeveledEntries);

void LogDistribution(const DistributedForms& forms, NPCData& npcData);
}
1 change: 0 additions & 1 deletion SPID/include/DistributeManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Distribute
{
inline RE::BGSKeyword* processed{ nullptr };
inline RE::BGSKeyword* processedOutfit{ nullptr };

namespace detail
{
Expand Down
10 changes: 7 additions & 3 deletions SPID/include/LookupConfigs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace RECORD
kPackage,
kOutfit,
kKeyword,
kDeathItem,
kFaction,
kSleepOutfit,
kSkin,
Expand All @@ -37,7 +36,6 @@ namespace RECORD
"Package"sv,
"Outfit"sv,
"Keyword"sv,
"DeathItem"sv,
"Faction"sv,
"SleepOutfit"sv,
"Skin"sv
Expand Down Expand Up @@ -483,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;
}
Expand Down Expand Up @@ -516,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);
}
}
Expand Down
21 changes: 20 additions & 1 deletion SPID/include/LookupNPC.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +31,24 @@ namespace NPC
[[nodiscard]] bool IsLeveled() const;
[[nodiscard]] bool IsTeammate() const;

/// <summary>
/// Flag indicating whether given NPC is dead.
///
/// IsDead returns true when either NPC starts dead or has already died. See IsDying for more details.
/// </summary>
[[nodiscard]] bool IsDead() const;

/// <summary>
/// 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.
/// </summary>
[[nodiscard]] bool IsDying() const;
[[nodiscard]] bool StartsDead() const;

[[nodiscard]] RE::TESRace* GetRace() const;

private:
Expand Down Expand Up @@ -63,6 +81,7 @@ namespace NPC
bool child;
bool teammate;
bool leveled;
bool dying;
};
}

Expand Down
Loading

0 comments on commit 2c87d07

Please sign in to comment.