From 2ea0b2334f228bc6072d32931a84528d372405f2 Mon Sep 17 00:00:00 2001 From: MetricExpansion <> Date: Wed, 31 Aug 2022 18:21:08 -0700 Subject: [PATCH 1/4] Change Submodule Refs --- dependencies/CommonLibSSE_AE | 2 +- dependencies/CommonLibSSE_PRE_AE | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/CommonLibSSE_AE b/dependencies/CommonLibSSE_AE index 78c14c2..3ce010c 160000 --- a/dependencies/CommonLibSSE_AE +++ b/dependencies/CommonLibSSE_AE @@ -1 +1 @@ -Subproject commit 78c14c2aaef930eba6639beaaf0308ef051da7e5 +Subproject commit 3ce010c4b42ff2d94d2adc0497a29db855ca7cf7 diff --git a/dependencies/CommonLibSSE_PRE_AE b/dependencies/CommonLibSSE_PRE_AE index e582160..ed16f26 160000 --- a/dependencies/CommonLibSSE_PRE_AE +++ b/dependencies/CommonLibSSE_PRE_AE @@ -1 +1 @@ -Subproject commit e582160ab1a077bc3bd6164d1d7a340b74f90d62 +Subproject commit ed16f26455d0909bc46f944fad30fb346f22070a From c8154b3a6e0b0058848805bb01033eb289580ac8 Mon Sep 17 00:00:00 2001 From: MetricExpansion <2558466-metricexpansion@users.noreply.gitlab.com> Date: Sat, 10 Sep 2022 21:32:46 +0000 Subject: [PATCH 2/4] Fix ShimWornFlags in AE --- src/hooking/PlayerSkinning_AE.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooking/PlayerSkinning_AE.cpp b/src/hooking/PlayerSkinning_AE.cpp index ac1aa56..f569f82 100644 --- a/src/hooking/PlayerSkinning_AE.cpp +++ b/src/hooking/PlayerSkinning_AE.cpp @@ -145,7 +145,7 @@ namespace OutfitSystem { // target in rsi push(rcx); - mov(rcx, rsi); + mov(rcx, rbx); sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer sub(rsp, 0x20); call(ptr[rip + f_ShouldOverrideSkinning]); From c246a3b9aa801836b74f8227ba7ed2d30a6419b9 Mon Sep 17 00:00:00 2001 From: MetricExpansion <2558466-metricexpansion@users.noreply.gitlab.com> Date: Sun, 11 Sep 2022 03:11:02 +0000 Subject: [PATCH 3/4] Bugfix/head clipping --- src/hooking/PlayerSkinning_AE.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooking/PlayerSkinning_AE.cpp b/src/hooking/PlayerSkinning_AE.cpp index f569f82..9dabfd6 100644 --- a/src/hooking/PlayerSkinning_AE.cpp +++ b/src/hooking/PlayerSkinning_AE.cpp @@ -143,7 +143,7 @@ namespace OutfitSystem { Xbyak::Label f_GetWornMask; Xbyak::Label f_OverrideWornFlags; - // target in rsi + // target in rbx push(rcx); mov(rcx, rbx); sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer @@ -159,7 +159,7 @@ namespace OutfitSystem { L(j_SuppressVanilla); push(rdx); - mov(rdx, rsi); + mov(rdx, rbx); sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer sub(rsp, 0x20); call(ptr[rip + f_OverrideWornFlags]); From 226a7fa39f0b341643869faf042478eeffc5a352 Mon Sep 17 00:00:00 2001 From: MetricExpansion <> Date: Sun, 9 Oct 2022 15:18:47 -0700 Subject: [PATCH 4/4] Version 0.4.0 release --- .clang-format | 67 + .gitmodules | 6 - CMakeLists.txt | 33 +- CMakeSettings.json | 22 + README.md | 20 +- appveyorscript | 26 + dependencies/CommonLibSSE_AE | 1 - dependencies/CommonLibSSE_PRE_AE | 1 - include/ArmorAddonOverrideService.h | 208 +- include/OutfitSystem.h | 5 +- include/PlayerSkinning.h | 5 +- include/RE/REAugments.h | 56 + include/SOS_PCH.h | 53 +- include/Utility.h | 19 + include/hooking/Hooks.hpp | 25 + include/hooking/Patches.hpp | 40 + include/version.h | 12 +- .../SKSE/Plugins/SkyrimOutfitSystemSE.ini | 2 + .../skyrimoutfitsystem_english.txt | Bin 16026 -> 17246 bytes src/ArmorAddonOverrideService.cpp | 956 +++---- src/OutfitSystem.cpp | 2337 +++++++++-------- src/RE/REAugments.cpp | 52 + src/Utility.cpp | 72 +- src/cobb/utf8naturalsort.cpp | 102 +- src/cobb/utf8string.cpp | 382 +-- src/hooking/Hooks_AE.cpp | 426 +++ src/hooking/Hooks_PRE_AE.cpp | 348 +++ src/hooking/Patches.cpp | 290 ++ src/hooking/PlayerSkinning_AE.cpp | 523 ---- src/hooking/PlayerSkinning_PRE_AE.cpp | 462 ---- src/main.cpp | 332 +-- src/papyrus/skyoutsysautoswitchtrigger.psc | 2 +- src/papyrus/skyoutsysmcm.psc | 147 +- src/papyrus/skyoutsysquicksloteffect.psc | 6 +- src/papyrus/skyrimoutfitsystemnativefuncs.psc | 17 +- vcpkg-configuration.json | 16 + vcpkg.json | 4 +- 37 files changed, 3825 insertions(+), 3250 deletions(-) create mode 100644 .clang-format create mode 100644 CMakeSettings.json create mode 100644 appveyorscript delete mode 160000 dependencies/CommonLibSSE_AE delete mode 160000 dependencies/CommonLibSSE_PRE_AE create mode 100644 include/RE/REAugments.h create mode 100644 include/hooking/Hooks.hpp create mode 100644 include/hooking/Patches.hpp create mode 100644 mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.ini create mode 100644 src/RE/REAugments.cpp create mode 100644 src/hooking/Hooks_AE.cpp create mode 100644 src/hooking/Hooks_PRE_AE.cpp create mode 100644 src/hooking/Patches.cpp delete mode 100644 src/hooking/PlayerSkinning_AE.cpp delete mode 100644 src/hooking/PlayerSkinning_PRE_AE.cpp create mode 100644 vcpkg-configuration.json diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..464317d --- /dev/null +++ b/.clang-format @@ -0,0 +1,67 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: None +AlignOperands: DontAlign +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Always +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: MultiLine +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CompactNamespaces: false +ContinuationIndentWidth: 4 +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 4 +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Left +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: false +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 0 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never +FixNamespaceComments: true diff --git a/.gitmodules b/.gitmodules index 1f70809..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +0,0 @@ -[submodule "dependencies/CommonLibSSE_AE"] - path = dependencies/CommonLibSSE_AE - url = ../CommonLibSSE.git -[submodule "dependencies/CommonLibSSE_PRE_AE"] - path = dependencies/CommonLibSSE_PRE_AE - url = ../CommonLibSSE.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 59a1fd9..5a4a77c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,14 +49,7 @@ set_property(GLOBAL PROPERTY USE_FOLDERS ON) ################################################################################ set(SKSE_SUPPORT_XBYAK 1) -message("SKYRIM_VERSION is ${SKYRIM_VERSION}") -if("${SKYRIM_VERSION}" STREQUAL "AE") - add_subdirectory(dependencies/CommonLibSSE_AE) -elseif("${SKYRIM_VERSION}" STREQUAL "PRE_AE") - add_subdirectory(dependencies/CommonLibSSE_PRE_AE) -else() - message(FATAL_ERROR "Must set SKYRIM_VERSION to one of: PRE_AE, AE") -endif() +find_package(CommonLibSSE REQUIRED) ################################################################################ # Build Protocol Buffers @@ -79,8 +72,7 @@ string(CONCAT "MSVC_RUNTIME_LIBRARY_STR" $<$:MultiThreadedDebug> $<$:MultiThreaded>) -# Also force it on CommonLibSSE and Protobuf -set_target_properties(CommonLibSSE PROPERTIES MSVC_RUNTIME_LIBRARY ${MSVC_RUNTIME_LIBRARY_STR}) +# Also force it on Protobuf set_target_properties(ProtocolBuffers PROPERTIES MSVC_RUNTIME_LIBRARY ${MSVC_RUNTIME_LIBRARY_STR}) ################################################################################ @@ -101,10 +93,11 @@ add_library(SkyrimOutfitSystemSE SHARED include/OutfitSystem.h src/OutfitSystem.cpp include/PlayerSkinning.h - src/hooking/PlayerSkinning_AE.cpp - src/hooking/PlayerSkinning_PRE_AE.cpp + src/hooking/Hooks_AE.cpp + src/hooking/Hooks_PRE_AE.cpp + src/hooking/Patches.cpp src/Utility.cpp -) + include/RE/REAugments.h src/RE/REAugments.cpp) set(ROOT_NAMESPACE SkyrimOutfitSystemSE) @@ -126,7 +119,9 @@ target_precompile_headers(SkyrimOutfitSystemSE PRIVATE target_compile_definitions(SkyrimOutfitSystemSE PRIVATE XBYAK_NO_OP_NAMES - "SKYRIM_VERSION_IS_${SKYRIM_VERSION}" + ENABLE_SKYRIM_SE + ENABLE_SKYRIM_AE + ENABLE_COMMONLIBSSE_TESTING ) target_compile_features(SkyrimOutfitSystemSE PRIVATE @@ -166,7 +161,7 @@ endif() # Install steps ################################################################################ add_custom_command(TARGET SkyrimOutfitSystemSE POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy $ "${CMAKE_CURRENT_SOURCE_DIR}/mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.dll" + COMMAND ${CMAKE_COMMAND} -E copy $ "${CMAKE_CURRENT_SOURCE_DIR}/mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.dll" ) ################################################################################ @@ -176,11 +171,17 @@ add_custom_command(TARGET SkyrimOutfitSystemSE POST_BUILD find_package(span-lite REQUIRED CONFIG) find_package(spdlog REQUIRED CONFIG) find_package(xbyak REQUIRED CONFIG) +find_library(INIH_LIBRARY inih REQUIRED) +find_path(INIH_INCLUDE_DIRS "ini.h" REQUIRED) target_link_libraries(SkyrimOutfitSystemSE PUBLIC - CommonLibSSE + CommonLibSSE::CommonLibSSE nonstd::span-lite spdlog::spdlog + ${INIH_LIBRARY} ProtocolBuffers xbyak::xbyak ) + +target_include_directories(SkyrimOutfitSystemSE PRIVATE + ${INIH_INCLUDE_DIRS}) diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..9d9d956 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "-DSKYRIM_VERSION=PRE_AE", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "variables": [ + { + "name": "VCPKG_TARGET_TRIPLET", + "value": "x64-windows-static", + "type": "STRING" + } + ] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 5e18fd4..dc5ab0b 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,13 @@ This mod is a resurrection of [aers's port](https://github.com/aers/SkyrimOutfitSystemSE) of David J Cobb's [Skyrim Outfit System](https://github.com/DavidJCobb/skyrim-outfit-system) for Skyrim SE, now able to run on newer Skyrim SE versions including Anniversary Edition. -This version also features a better Quickselect system using UIExtensions menus and is also version independent from the SKSE/Skyrim runtime version (except for pre-AE vs AE) by use of the [Address Library](https://www.nexusmods.com/skyrimspecialedition/mods/32444). +This version also features a better Quickselect system using UIExtensions menus and is also version independent from the SKSE/Skyrim runtime version by use of the [Address Library](https://www.nexusmods.com/skyrimspecialedition/mods/32444) and [CommonLibSSE-NG](https://github.com/CharmedBaryon/CommonLibSSE-NG). # Compatibility -This mod supports AE and pre-AE with different DLLs. +Thanks to CommonLibSSE-NG, this mod supports SE and AE with a single DLL. Runtimes 1.5.73 to 1.6.353 are officially supported. -The pre-AE DLL supports runtimes 1.5.73 to 1.5.97. - -The AE DLL supports runtimes 1.6.317 to 1.6.353 (so far, it is only tested on 1.6.353). +Runtimes 1.6.629 and later have modified struct layouts and, while CommonLibSSE-NG should technically allow this mod to support them, they are *completely* untested and unsupported for now. Likewise for compatibility with any store besides Steam. # License @@ -37,14 +35,6 @@ Before attempting to build this project, please have the following tools install ### Preparation -#### Submodules - -First, make sure this project is cloned and that the submodules are initialized and updated: - - git submodule update --init - -This will initialize various submodules that contain the fork of CommonLibSSE that this mod uses. - #### SKSE (Optional) If you want to edit and compile Papyrus scripts, you will need to download SKSE. If you only plan to work on the DLL itself, you can skip this step. @@ -68,7 +58,7 @@ First, create a build folder (anywhere is fine). For this example, we will creat Now invoke CMake as follows: - cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${PATH TO VCPKG TOOLCHAIN} -DVCPKG_TARGET_TRIPLET=x64-windows-static -DSKYRIM_VERSION=AE ../ + cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${PATH TO VCPKG TOOLCHAIN} -DVCPKG_TARGET_TRIPLET=x64-windows-static ../ Note that `${PATH TO VCPKG TOOLCHAIN}` is to be replaced with the path to the `vkpkg` toolchain, as described by the [vcpkg documentation](https://vcpkg.io/en/getting-started.html). @@ -76,8 +66,6 @@ Note that `${PATH TO VCPKG TOOLCHAIN}` is to be replaced with the path to the `v > **WARNING:** `Ninja` is the only supported generator. MSBuild may or may not work. -Take special note of the `-DSKYRIM_VERSION=AE` option. **This mod uses different DLLs for AE and pre-AE**. If you want to build the DLL for AE, use `AE` as shown. If you want to build for pre-AE, use `PRE_AE` instead. If you plan to contribute changes to this mod, they need to work with both variants. - Once the project is successfully configured, build it by running cmake --build ./ diff --git a/appveyorscript b/appveyorscript new file mode 100644 index 0000000..05f72e5 --- /dev/null +++ b/appveyorscript @@ -0,0 +1,26 @@ +$env:GIT_REDIRECT_STDERR = '2>&1' +function Invoke-CmdScript { + param( + [String] $scriptName + ) + $cmdLine = """$scriptName"" $args & set" + & $Env:SystemRoot\system32\cmd.exe /c $cmdLine | + select-string '^([^=]*)=(.*)$' | foreach-object { + $varName = $_.Matches[0].Groups[1].Value + $varValue = $_.Matches[0].Groups[2].Value + set-item Env:$varName $varValue + } +} + +git submodule update --init + +mkdir build +cd build +Invoke-CmdScript "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" +# Run cmake to set up proto compiler (need to do this) +cmake -DCMAKE_BUILD_TYPE=Release -G Ninja -DCMAKE_TOOLCHAIN_FILE=C:\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static -DSKYRIM_VERSION=PRE_AE ../ + +# Run cmake to build +cmake -DCMAKE_BUILD_TYPE=Release -G Ninja -DVCPKG_TARGET_TRIPLET=x64-windows-static ../ +# Finally, build the thing +cmake --build . -j diff --git a/dependencies/CommonLibSSE_AE b/dependencies/CommonLibSSE_AE deleted file mode 160000 index 3ce010c..0000000 --- a/dependencies/CommonLibSSE_AE +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ce010c4b42ff2d94d2adc0497a29db855ca7cf7 diff --git a/dependencies/CommonLibSSE_PRE_AE b/dependencies/CommonLibSSE_PRE_AE deleted file mode 160000 index ed16f26..0000000 --- a/dependencies/CommonLibSSE_PRE_AE +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ed16f26455d0909bc46f944fad30fb346f22070a diff --git a/include/ArmorAddonOverrideService.h b/include/ArmorAddonOverrideService.h index 435c927..fbce9d7 100644 --- a/include/ArmorAddonOverrideService.h +++ b/include/ArmorAddonOverrideService.h @@ -9,10 +9,10 @@ #include "outfit.pb.h" namespace RE { - class TESObjectARMO; + class TESObjectARMO; } -enum class LocationType: std::uint32_t { +enum class LocationType : std::uint32_t { World = 0, Town = 1, Dungeon = 2, @@ -35,113 +35,119 @@ struct WeatherFlags { }; struct Outfit { - Outfit() {}; // we shouldn't need this, really, but std::unordered_map is a brat - Outfit(const char* n) : name(n), isFavorite(false), allowsPassthrough(false), requiresEquipped(false) {}; - Outfit(const Outfit& other) = default; - Outfit(const char* n, const Outfit& other) : name(n), isFavorite(false), allowsPassthrough(false), requiresEquipped(false) { - this->armors = other.armors; - } - std::string name; // can't be const; prevents assigning to Outfit vars - std::unordered_set armors; - bool isFavorite; - bool allowsPassthrough; - bool requiresEquipped; + Outfit(){};// we shouldn't need this, really, but std::unordered_map is a brat + Outfit(const char* n) : name(n), isFavorite(false), allowsPassthrough(false), requiresEquipped(false){}; + Outfit(const Outfit& other) = default; + Outfit(const char* n, const Outfit& other) : name(n), isFavorite(false), allowsPassthrough(false), requiresEquipped(false) { + this->armors = other.armors; + } + std::string name;// can't be const; prevents assigning to Outfit vars + std::unordered_set armors; + bool isFavorite; + bool allowsPassthrough; + bool requiresEquipped; - bool conflictsWith(RE::TESObjectARMO*) const; - bool hasShield() const; - std::unordered_set computeDisplaySet(const std::unordered_set& equipped); + bool conflictsWith(RE::TESObjectARMO*) const; + bool hasShield() const; + std::unordered_set computeDisplaySet(const std::unordered_set& equipped); - void load(const proto::Outfit& proto, const SKSE::SerializationInterface*); - void load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version); // can throw ArmorAddonOverrideService::load_error - proto::Outfit save() const; // can throw ArmorAddonOverrideService::save_error + void load(const proto::Outfit& proto, const SKSE::SerializationInterface*); + void load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version);// can throw ArmorAddonOverrideService::load_error + proto::Outfit save() const; // can throw ArmorAddonOverrideService::save_error }; const constexpr char* g_noOutfitName = ""; -static Outfit g_noOutfit(g_noOutfitName); // can't be const; prevents us from assigning it to Outfit&s +static Outfit g_noOutfit(g_noOutfitName);// can't be const; prevents us from assigning it to Outfit&s class ArmorAddonOverrideService { - public: - typedef Outfit Outfit; - static constexpr std::uint32_t signature = 'AAOS'; - // Uses protobufs starting with V4 - enum { kSaveVersionV1 = 1, kSaveVersionV2 = 2, kSaveVersionV3 = 3, kSaveVersionV4 = 4 }; - // - static constexpr std::uint32_t ce_outfitNameMaxLength = 256; // SKSE caps serialized std::strings and const char*s to 256 bytes. - // - static void _validateNameOrThrow(const char* outfitName); - // - struct bad_name : public std::runtime_error { - explicit bad_name(const std::string& what_arg) : runtime_error(what_arg) {}; - }; - struct load_error : public std::runtime_error { - explicit load_error(const std::string& what_arg) : runtime_error(what_arg) {}; - }; - struct name_conflict : public std::runtime_error { - explicit name_conflict(const std::string& what_arg) : runtime_error(what_arg) {}; - }; - struct save_error : public std::runtime_error { - explicit save_error(const std::string& what_arg) : runtime_error(what_arg) {}; - }; - // - private: - struct OutfitReferenceByName : public cobb::istring { - OutfitReferenceByName(const value_type* s) : cobb::istring(s) {}; - OutfitReferenceByName(const basic_string& other) : cobb::istring(other) {}; - // - operator Outfit&() { +public: + typedef Outfit Outfit; + static constexpr std::uint32_t signature = 'AAOS'; + enum { + kSaveVersionV1 = 1, + kSaveVersionV2 = 2, + kSaveVersionV3 = 3, + kSaveVersionV4 = 4// First version with protobuf + }; + // + static constexpr std::uint32_t ce_outfitNameMaxLength = 256;// SKSE caps serialized std::strings and const char*s to 256 bytes. + // + static void _validateNameOrThrow(const char* outfitName); + // + struct bad_name: public std::runtime_error { + explicit bad_name(const std::string& what_arg) : runtime_error(what_arg){}; + }; + struct load_error: public std::runtime_error { + explicit load_error(const std::string& what_arg) : runtime_error(what_arg){}; + }; + struct name_conflict: public std::runtime_error { + explicit name_conflict(const std::string& what_arg) : runtime_error(what_arg){}; + }; + struct save_error: public std::runtime_error { + explicit save_error(const std::string& what_arg) : runtime_error(what_arg){}; + }; + // +private: + struct OutfitReferenceByName: public cobb::istring { + OutfitReferenceByName(const value_type* s) : cobb::istring(s){}; + OutfitReferenceByName(const basic_string& other) : cobb::istring(other){}; + // + operator Outfit&() { return ArmorAddonOverrideService::GetInstance().outfits.at(*this); - } - }; - public: - struct ActorOutfitAssignments { + } + }; + +public: + struct ActorOutfitAssignments { cobb::istring currentOutfitName = g_noOutfitName; std::map locationOutfits; - }; - bool enabled = true; - std::map outfits; - // TODO: You probably shouldn't use an Actor pointer to refer to actors. It works for the PlayerCharacter, but likely not for NPCs. - std::map actorOutfitAssignments; - // Location-based switching - bool locationBasedAutoSwitchEnabled = false; - // - static ArmorAddonOverrideService& GetInstance() { - static ArmorAddonOverrideService instance; - return instance; - }; - // - Outfit& getOutfit(const char* name); // throws std::out_of_range if not found - Outfit& getOrCreateOutfit(const char* name); // can throw bad_name - // - void addOutfit(const char* name); // can throw bad_name - void addOutfit(const char* name, std::vector armors); // can throw bad_name - Outfit& currentOutfit(RE::Actor* target); - bool hasOutfit(const char* name) const; - void deleteOutfit(const char* name); - void setFavorite(const char* name, bool favorite); - void setOutfitPassthrough(const char* name, bool allowPassthrough); - void setOutfitEquipRequired(const char* name, bool requiresEquipped); - void modifyOutfit(const char* name, std::vector& add, std::vector& remove, bool createIfMissing = false); // can throw bad_name if (createIfMissing) - void renameOutfit(const char* oldName, const char* newName); // throws name_conflict if the new name is already taken; can throw bad_name; throws std::out_of_range if the oldName doesn't exist - void setOutfit(const char* name, RE::Actor* target); - void addActor(RE::Actor* target); - void removeActor(RE::Actor* target); + }; + bool enabled = true; + std::map outfits; + // TODO: You probably shouldn't use an Actor pointer to refer to actors. It works for the PlayerCharacter, but likely not for NPCs. + std::map actorOutfitAssignments; + // Location-based switching + bool locationBasedAutoSwitchEnabled = false; // - void setLocationBasedAutoSwitchEnabled(bool) noexcept; - void setOutfitUsingLocation(LocationType location, RE::Actor* target); - void setLocationOutfit(LocationType location, const char* name, RE::Actor* target); - void unsetLocationOutfit(LocationType location, RE::Actor* target); - std::optional getLocationOutfit(LocationType location, RE::Actor* target); - std::optional checkLocationType(const std::unordered_set& keywords, const WeatherFlags& weather_flags, RE::Actor* target); - // - bool shouldOverride(RE::Actor* target) const noexcept; - void getOutfitNames(std::vector& out, bool favoritesOnly = false) const; - void setEnabled(bool) noexcept; - // - void refreshCurrentIfChanged(const char* testName); - // - void reset(); - void load(const SKSE::SerializationInterface* intfc, const proto::OutfitSystem& data); // can throw load_error - void load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version); // can throw load_error - proto::OutfitSystem save(); // can throw save_error - // - void dump() const; + static ArmorAddonOverrideService& GetInstance() { + static ArmorAddonOverrideService instance; + return instance; + }; + // + Outfit& getOutfit(const char* name); // throws std::out_of_range if not found + Outfit& getOrCreateOutfit(const char* name);// can throw bad_name + // + void addOutfit(const char* name); // can throw bad_name + void addOutfit(const char* name, std::vector armors);// can throw bad_name + Outfit& currentOutfit(RE::RawActorHandle target); + bool hasOutfit(const char* name) const; + void deleteOutfit(const char* name); + void setFavorite(const char* name, bool favorite); + void setOutfitPassthrough(const char* name, bool allowPassthrough); + void setOutfitEquipRequired(const char* name, bool requiresEquipped); + void modifyOutfit(const char* name, std::vector& add, std::vector& remove, bool createIfMissing = false);// can throw bad_name if (createIfMissing) + void renameOutfit(const char* oldName, const char* newName); // throws name_conflict if the new name is already taken; can throw bad_name; throws std::out_of_range if the oldName doesn't exist + void setOutfit(const char* name, RE::RawActorHandle target); + void addActor(RE::RawActorHandle target); + void removeActor(RE::RawActorHandle target); + std::unordered_set listActors(); + // + void setLocationBasedAutoSwitchEnabled(bool) noexcept; + void setOutfitUsingLocation(LocationType location, RE::RawActorHandle target); + void setLocationOutfit(LocationType location, const char* name, RE::RawActorHandle target); + void unsetLocationOutfit(LocationType location, RE::RawActorHandle target); + std::optional getLocationOutfit(LocationType location, RE::RawActorHandle target); + std::optional checkLocationType(const std::unordered_set& keywords, const WeatherFlags& weather_flags, RE::RawActorHandle target); + // + bool shouldOverride(RE::RawActorHandle target) const noexcept; + void getOutfitNames(std::vector& out, bool favoritesOnly = false) const; + void setEnabled(bool) noexcept; + // + void refreshCurrentIfChanged(const char* testName); + // + void reset(); + void load(const SKSE::SerializationInterface* intfc, const proto::OutfitSystem& data);// can throw load_error + void load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version); // can throw load_error + proto::OutfitSystem save(); // can throw save_error + // + void dump() const; }; \ No newline at end of file diff --git a/include/OutfitSystem.h b/include/OutfitSystem.h index b72ac96..90ab532 100644 --- a/include/OutfitSystem.h +++ b/include/OutfitSystem.h @@ -4,9 +4,8 @@ namespace RE { namespace BSScript { class IVirtualMachine; } -} +}// namespace RE -namespace OutfitSystem -{ +namespace OutfitSystem { bool RegisterPapyrus(RE::BSScript::IVirtualMachine* registry); } diff --git a/include/PlayerSkinning.h b/include/PlayerSkinning.h index 148e858..22cb34f 100644 --- a/include/PlayerSkinning.h +++ b/include/PlayerSkinning.h @@ -4,9 +4,8 @@ namespace SKSE { class Trampoline; } -namespace OutfitSystem -{ +namespace OutfitSystem { extern SKSE::Trampoline* g_localTrampoline; extern SKSE::Trampoline* g_branchTrampoline; void ApplyPlayerSkinningHooks(); -} \ No newline at end of file +}// namespace OutfitSystem \ No newline at end of file diff --git a/include/RE/REAugments.h b/include/RE/REAugments.h new file mode 100644 index 0000000..d9a77e7 --- /dev/null +++ b/include/RE/REAugments.h @@ -0,0 +1,56 @@ +// +// Created by m on 10/8/2022. +// + +#ifndef SKYRIMOUTFITSYSTEMSE_INCLUDE_RE_REAUGMENTS_H +#define SKYRIMOUTFITSYSTEMSE_INCLUDE_RE_REAUGMENTS_H + +namespace RE { + class ActorWeightModel; + + class IItemChangeVisitorAugment { + public: + inline static constexpr auto RTTI = RTTI_InventoryChanges__IItemChangeVisitor; + + virtual ~IItemChangeVisitorAugment(){};// 00 + + enum VisitorReturn : std::uint32_t { + kStop, + kContinue + }; + + // add + virtual VisitorReturn Visit(InventoryEntryData* a_entryData) = 0;// 01 + virtual void Unk_02(void){}; // 02 - { return 1; } + virtual void Unk_03(void){}; // 03 + }; + static_assert(sizeof(IItemChangeVisitorAugment) == 0x8); + + namespace InventoryChangesAugments { + void ExecuteVisitor(RE::InventoryChanges* thisPtr, RE::InventoryChanges::IItemChangeVisitor* a_visitor); + void ExecuteAugmentVisitor(RE::InventoryChanges* thisPtr, RE::IItemChangeVisitorAugment* a_visitor); + void ExecuteVisitorOnWorn(RE::InventoryChanges* thisPtr, RE::InventoryChanges::IItemChangeVisitor* a_visitor); + void ExecuteAugmentVisitorOnWorn(RE::InventoryChanges* thisPtr, RE::IItemChangeVisitorAugment* a_visitor); + }// namespace InventoryChangesAugments + + namespace AIProcessAugments { + enum class Flag : std::uint8_t { + kNone = 0, + kUnk01 = 1 << 0, + kUnk02 = 1 << 1, + kUnk03 = 1 << 2, + kDrawHead = 1 << 3, + kMobile = 1 << 4, + kReset = 1 << 5 + }; + void SetEquipFlag(RE::AIProcess* thisPtr, Flag a_flag); + void UpdateEquipment(RE::AIProcess* thisPtr, Actor* a_actor); + }// namespace AIProcessAugments + + namespace TESObjectARMOAugments { + bool ApplyArmorAddon(RE::TESObjectARMO* thisPtr, TESRace* a_race, ActorWeightModel* a_model, bool a_isFemale); + bool TestBodyPartByIndex(RE::TESObjectARMO* thisPtr, std::uint32_t a_index); + }// namespace TESObjectARMOAugments +}// namespace RE + +#endif//SKYRIMOUTFITSYSTEMSE_INCLUDE_RE_REAUGMENTS_H diff --git a/include/SOS_PCH.h b/include/SOS_PCH.h index 8d6e504..d728328 100644 --- a/include/SOS_PCH.h +++ b/include/SOS_PCH.h @@ -8,72 +8,39 @@ #include "version.h" #pragma warning(push) -#if SKYRIM_VERSION_IS_AE #include "SKSE/Impl/PCH.h" #include #include #include -#include -#include -#include "RE/T/TESObjectARMO.h" -#include "RE/P/PlayerCharacter.h" -#include "RE/A/ActorEquipManager.h" -#include "RE/I/InventoryChanges.h" -#include "RE/I/InventoryEntryData.h" -#include "RE/P/PlayerCharacter.h" -#include "RE/T/TESObjectARMO.h" -#include "RE/T/TESObjectREFR.h" -#include "RE/I/IVirtualMachine.h" -#include "RE/T/TESForm.h" -#include "RE/I/IVirtualMachine.h" -#elif SKYRIM_VERSION_IS_PRE_AE -#include "SKSE/Impl/PCH.h" -#include -#include -#include -#include -#include -#include "RE/T/TESObjectARMO.h" -#include "RE/P/PlayerCharacter.h" -#include "RE/A/ActorEquipManager.h" -#include "RE/I/InventoryChanges.h" -#include "RE/I/InventoryEntryData.h" -#include "RE/P/PlayerCharacter.h" -#include "RE/T/TESObjectARMO.h" -#include "RE/T/TESObjectREFR.h" -#include "RE/I/IVirtualMachine.h" -#include "RE/T/TESForm.h" -#include "RE/I/IVirtualMachine.h" -#endif -#include "fmt/compile.h" +#include "INIReader.h" #ifdef NDEBUG -# include +#include #else -# include +#include #endif #pragma warning(pop) using namespace std::literals; -#define LOG(a_type, ...) spdlog::log(spdlog::source_loc(__FILE__, __LINE__, __FUNCTION__), spdlog::level::a_type, fmt::format(__VA_ARGS__)) +#define LOG(a_type, ...) spdlog::log(spdlog::source_loc(__FILE__, __LINE__, __FUNCTION__), spdlog::level::a_type, __VA_ARGS__) namespace util { using SKSE::stl::report_and_fail; } -#define DllExport __declspec( dllexport ) +#define DllExport __declspec(dllexport) namespace Plugin { using namespace std::literals; -#if SKYRIM_VERSION_IS_AE inline constexpr REL::Version VERSION{SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR, SKYRIMOUTFITSYSTEMSE_VERSION_MINOR, SKYRIMOUTFITSYSTEMSE_VERSION_PATCH}; -#elif SKYRIM_VERSION_IS_PRE_AE - inline constexpr REL::Version VERSION{SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR, SKYRIMOUTFITSYSTEMSE_VERSION_MINOR, SKYRIMOUTFITSYSTEMSE_VERSION_PATCH, 0}; -#endif inline constexpr auto NAME = "SkyrimOutfitSystemSE"sv; +}// namespace Plugin + +namespace RE { + using RawActorHandle = RE::ActorHandle::native_handle_type; } -#endif //SKYRIMOUTFITSYSTEMSE_SOS_PCH_H +#endif//SKYRIMOUTFITSYSTEMSE_SOS_PCH_H diff --git a/include/Utility.h b/include/Utility.h index a9dc79b..7c1f622 100644 --- a/include/Utility.h +++ b/include/Utility.h @@ -5,3 +5,22 @@ std::string GetRuntimeName(); const std::string& GetRuntimeDirectory(); + +class Settings { +public: + Settings(); + ~Settings(); + INIReader reader; + static INIReader* Instance(); +}; + +class LogExit { +public: + std::string_view m_string; + LogExit(std::string_view name) : m_string(name) { + LOG(trace, "Enter {}", m_string); + }; + ~LogExit() { + LOG(trace, "Exit {}", m_string); + }; +}; diff --git a/include/hooking/Hooks.hpp b/include/hooking/Hooks.hpp new file mode 100644 index 0000000..f392c90 --- /dev/null +++ b/include/hooking/Hooks.hpp @@ -0,0 +1,25 @@ +// +// Created by m on 9/11/2022. +// + +#ifndef SKYRIMOUTFITSYSTEMSE_SRC_HOOKING_HOOKS_AE_HPP +#define SKYRIMOUTFITSYSTEMSE_SRC_HOOKING_HOOKS_AE_HPP + +namespace SKSE { + class Trampoline; +} + +namespace Hooking { + extern SKSE::Trampoline* g_localTrampoline; + extern SKSE::Trampoline* g_branchTrampoline; +}// namespace Hooking + +namespace HookingPREAE { + void ApplyPlayerSkinningHooks(); +} + +namespace HookingAE { + void ApplyPlayerSkinningHooks(); +} + +#endif//SKYRIMOUTFITSYSTEMSE_SRC_HOOKING_HOOKS_AE_HPP diff --git a/include/hooking/Patches.hpp b/include/hooking/Patches.hpp new file mode 100644 index 0000000..47d552b --- /dev/null +++ b/include/hooking/Patches.hpp @@ -0,0 +1,40 @@ +// +// Created by m on 9/11/2022. +// + +#ifndef SKYRIMOUTFITSYSTEMSE_INCLUDE_HOOKING_PATCHES_HPP +#define SKYRIMOUTFITSYSTEMSE_INCLUDE_HOOKING_PATCHES_HPP + +#include "RE/REAugments.h" + +namespace Hooking { + bool ShouldOverrideSkinning(RE::TESObjectREFR* target); + + namespace DontVanillaSkinPlayer { + bool ShouldOverride(RE::TESObjectARMO* armor, RE::TESObjectREFR* target); + } + + namespace ShimWornFlags { + std::uint32_t OverrideWornFlags(RE::InventoryChanges* inventory, RE::TESObjectREFR* target); + } + + namespace CustomSkinPlayer { + void Custom(RE::Actor* target, RE::ActorWeightModel* actorWeightModel); + } + + namespace FixEquipConflictCheck { + void Inner(std::uint32_t bodyPartForNewItem, RE::Actor* target); + bool ShouldOverride(RE::TESForm* item); + }// namespace FixEquipConflictCheck + + namespace FixSkillLeveling { + struct Visitor; + bool Inner(RE::BipedAnim* biped, Visitor* bipedVisitor); + }// namespace FixSkillLeveling + + namespace RTTIPrinter { + void Print_RTTI(RE::InventoryChanges::IItemChangeVisitor* target); + } +}// namespace Hooking + +#endif//SKYRIMOUTFITSYSTEMSE_INCLUDE_HOOKING_PATCHES_HPP diff --git a/include/version.h b/include/version.h index 31536a2..f43423d 100644 --- a/include/version.h +++ b/include/version.h @@ -1,13 +1,13 @@ -#ifndef SKYRIMOUTFITSYSTEMSE_VERSION_INCLUDED +#ifndef SKYRIMOUTFITSYSTEMSE_VERSION_INCLUDED #define SKYRIMOUTFITSYSTEMSE_VERSION_INCLUDED #define SKYRIMOUTFITSYSTEMSE_MAKE_STR_HELPER(x) #x #define SKYRIMOUTFITSYSTEMSE_MAKE_STR(x) SKYRIMOUTFITSYSTEMSE_MAKE_STR_HELPER(x) -#define SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR 0 -#define SKYRIMOUTFITSYSTEMSE_VERSION_MINOR 3 -#define SKYRIMOUTFITSYSTEMSE_VERSION_PATCH 1 -#define SKYRIMOUTFITSYSTEMSE_VERSION_BETA 0 -#define SKYRIMOUTFITSYSTEMSE_VERSION_VERSTRING SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_MINOR) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_PATCH) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_BETA) +#define SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR 0 +#define SKYRIMOUTFITSYSTEMSE_VERSION_MINOR 4 +#define SKYRIMOUTFITSYSTEMSE_VERSION_PATCH 0 +#define SKYRIMOUTFITSYSTEMSE_VERSION_BETA 0 +#define SKYRIMOUTFITSYSTEMSE_VERSION_VERSTRING SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_MAJOR) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_MINOR) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_PATCH) "." SKYRIMOUTFITSYSTEMSE_MAKE_STR(SKYRIMOUTFITSYSTEMSE_VERSION_BETA) #endif diff --git a/mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.ini b/mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.ini new file mode 100644 index 0000000..17cdbf0 --- /dev/null +++ b/mod_files/SKSE/Plugins/SkyrimOutfitSystemSE.ini @@ -0,0 +1,2 @@ +[Debug] +ExtraLogging = off diff --git a/mod_files/interface/translations/skyrimoutfitsystem_english.txt b/mod_files/interface/translations/skyrimoutfitsystem_english.txt index 398dfa10f8da1e881158620fe986e6d464b83f74..163ba1067c06e6e25aa74b57a865a23ae8278004 100644 GIT binary patch delta 878 zcmbPLd#{agLmuPg1KeER;S7lkMGScinGAUh=?oqWsSJq>DGaF$MGTw_t_(p8K@9#3 zK@3(53Jd`ZISi!?>0p%#41Nsx3?&Q-3_c9`P#r0g7qZAt4v=A+T%f?WxrwoW#nh1@ znW2Oslc9_ul>x%eXDC9m#F-%j?1V(H+Ej+3$rHt8K~D1xW=LhoVMqmwBXlE+W-{b6 zGNJxQXKp_bVJ%|X1SIm&ekjnsKC4)ma z2OQ=gUxQ-+?BWC5T$VvdVTs?@$SM(j=2Au$N0`9LfFwRSR!CYO)zu)E5exz5Vg@~M OMmWIDwfPh43JU;0OtMh` delta 26 icmccD#yG2XLmuPg3$kpJ7x40J{>E6qvUwZV3JU<51Pb8* diff --git a/src/ArmorAddonOverrideService.cpp b/src/ArmorAddonOverrideService.cpp index 1260996..0781b91 100644 --- a/src/ArmorAddonOverrideService.cpp +++ b/src/ArmorAddonOverrideService.cpp @@ -5,593 +5,597 @@ //#include "skse64/Serialization.h" void _assertWrite(bool result, const char* err) { - if (!result) - throw ArmorAddonOverrideService::save_error(err); + if (!result) + throw ArmorAddonOverrideService::save_error(err); } void _assertRead(bool result, const char* err) { - if (!result) - throw ArmorAddonOverrideService::load_error(err); + if (!result) + throw ArmorAddonOverrideService::load_error(err); } bool Outfit::conflictsWith(RE::TESObjectARMO* test) const { - if (!test) - return false; - const auto mask = static_cast(test->GetSlotMask()); - for (auto it = this->armors.cbegin(); it != this->armors.cend(); ++it) { - RE::TESObjectARMO* armor = *it; - if (armor) - if ((mask & static_cast(armor->GetSlotMask())) - != static_cast(RE::BGSBipedObjectForm::FirstPersonFlag::kNone)) - return true; - } - return false; + if (!test) + return false; + const auto mask = static_cast(test->GetSlotMask()); + for (auto it = this->armors.cbegin(); it != this->armors.cend(); ++it) { + RE::TESObjectARMO* armor = *it; + if (armor) + if ((mask & static_cast(armor->GetSlotMask())) + != static_cast(RE::BGSBipedObjectForm::FirstPersonFlag::kNone)) + return true; + } + return false; } bool Outfit::hasShield() const { - auto& list = this->armors; - for (auto it = list.cbegin(); it != list.cend(); ++it) { - RE::TESObjectARMO* armor = *it; - if (armor) { - if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) - return true; - } - } - return false; + auto& list = this->armors; + for (auto it = list.cbegin(); it != list.cend(); ++it) { + RE::TESObjectARMO* armor = *it; + if (armor) { + if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) + return true; + } + } + return false; }; std::unordered_set Outfit::computeDisplaySet(const std::unordered_set& equipped) { - std::unordered_set result; + std::unordered_set result; - // Handle whatever combination of flags we have for how passthrough and equip requirements work. - if (!allowsPassthrough && !requiresEquipped) { - // The usual behavior: Don't allow passthrough of any slot and unconditionally apply the outfit items. + // Handle whatever combination of flags we have for how passthrough and equip requirements work. + if (!allowsPassthrough && !requiresEquipped) { + // The usual behavior: Don't allow passthrough of any slot and unconditionally apply the outfit items. - // The result is just my own armors... - result = armors; - } else if (allowsPassthrough && !requiresEquipped) { - // The much requested behavior. Allow compatible armors to show through the outfit + // The result is just my own armors... + result = armors; + } else if (allowsPassthrough && !requiresEquipped) { + // The much requested behavior. Allow compatible armors to show through the outfit - // The result is just my own armors... - result = armors; + // The result is just my own armors... + result = armors; - // And now append equipped armors that *are compatible*. - for (const auto candidate : equipped) { - if (!conflictsWith(candidate)) { - result.emplace(candidate); - } - } - } else if (!allowsPassthrough && requiresEquipped) { - // No passthrough, but do require that items are equipped to show their overrides. + // And now append equipped armors that *are compatible*. + for (const auto candidate : equipped) { + if (!conflictsWith(candidate)) { + result.emplace(candidate); + } + } + } else if (!allowsPassthrough && requiresEquipped) { + // No passthrough, but do require that items are equipped to show their overrides. - // Get the effective mask of all equipped armors - std::uint32_t equippedMask = 0; - for (const auto armor : equipped) { - equippedMask |= static_cast(armor->GetSlotMask()); - } + // Get the effective mask of all equipped armors + std::uint32_t equippedMask = 0; + for (const auto armor : equipped) { + equippedMask |= static_cast(armor->GetSlotMask()); + } - // Only add my armors if they are covered by the equip mask - for (const auto armor : armors) { - auto armorMask = static_cast(armor->GetSlotMask()); - if ((armorMask & equippedMask) == armorMask) { - result.emplace(armor); - } - } - } else if (allowsPassthrough && requiresEquipped) { - // Allow passthrough and also require that outfit items are equipped to show. + // Only add my armors if they are covered by the equip mask + for (const auto armor : armors) { + auto armorMask = static_cast(armor->GetSlotMask()); + if ((armorMask & equippedMask) == armorMask) { + result.emplace(armor); + } + } + } else if (allowsPassthrough && requiresEquipped) { + // Allow passthrough and also require that outfit items are equipped to show. - // Get the effective mask of all equipped armors - std::uint32_t equippedMask = 0; - for (const auto armor : equipped) { - equippedMask |= static_cast(armor->GetSlotMask()); - } + // Get the effective mask of all equipped armors + std::uint32_t equippedMask = 0; + for (const auto armor : equipped) { + equippedMask |= static_cast(armor->GetSlotMask()); + } - // Only add my armors if they are covered by the equip mask - for (const auto armor : armors) { - std::uint32_t armorMask = static_cast(armor->GetSlotMask()); - if ((armorMask & equippedMask) == armorMask) { - result.emplace(armor); - } - } + // Only add my armors if they are covered by the equip mask + for (const auto armor : armors) { + std::uint32_t armorMask = static_cast(armor->GetSlotMask()); + if ((armorMask & equippedMask) == armorMask) { + result.emplace(armor); + } + } - // And now append equipped armors that *are compatible*. - for (const auto candidate : equipped) { - if (!conflictsWith(candidate)) { - result.emplace(candidate); - } - } - } + // And now append equipped armors that *are compatible*. + for (const auto candidate : equipped) { + if (!conflictsWith(candidate)) { + result.emplace(candidate); + } + } + } - // IN ALL CASES -- SPECIAL SHIELD HANDLING - // Check if the equipped and outfit have shields and save them. - std::optional equippedShield; - std::optional outfitShield; - for (const auto armor : equipped) { - if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { - equippedShield = armor; - break; - } - } - for (const auto armor : armors) { - if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { - outfitShield = armor; - break; - } - } + // IN ALL CASES -- SPECIAL SHIELD HANDLING + // Check if the equipped and outfit have shields and save them. + std::optional equippedShield; + std::optional outfitShield; + for (const auto armor : equipped) { + if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { + equippedShield = armor; + break; + } + } + for (const auto armor : armors) { + if ((armor->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { + outfitShield = armor; + break; + } + } - // Unconditionally erase shields from the result, so that we can apply the correct one (if any) - for (auto it = result.begin(); it != result.end();) { - if (((*it)->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { - it = result.erase(it); - } else { - ++it; - } - } + // Unconditionally erase shields from the result, so that we can apply the correct one (if any) + for (auto it = result.begin(); it != result.end();) { + if (((*it)->formFlags & RE::TESObjectARMO::RecordFlags::kShield) != 0) { + it = result.erase(it); + } else { + ++it; + } + } - // Add the correct shield to the outfit. - if (!equippedShield.has_value()) { - // No equipped shield. No shield will be added. - } else if (equippedShield.has_value() && !outfitShield.has_value()) { - // Equipped shield, but nothing in the outfit. Use the equipped shield. - result.emplace(equippedShield.value()); - } else if (equippedShield.has_value() && outfitShield.has_value()) { - // Equipped shield, and outfit has shield. Use the outfit shield. - result.emplace(outfitShield.value()); - } - // END SHIELD HANDLING + // Add the correct shield to the outfit. + if (!equippedShield.has_value()) { + // No equipped shield. No shield will be added. + } else if (equippedShield.has_value() && !outfitShield.has_value()) { + // Equipped shield, but nothing in the outfit. Use the equipped shield. + result.emplace(equippedShield.value()); + } else if (equippedShield.has_value() && outfitShield.has_value()) { + // Equipped shield, and outfit has shield. Use the outfit shield. + result.emplace(outfitShield.value()); + } + // END SHIELD HANDLING - return result; + return result; }; void Outfit::load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version) { - std::uint32_t size = 0; - _assertRead(intfc->ReadRecordData(size), "Failed to read an outfit's armor count."); - for (std::uint32_t i = 0; i < size; i++) { - RE::FormID formID = 0; - _assertRead(intfc->ReadRecordData(formID), "Failed to read an outfit's armor."); - RE::FormID fixedID; - if (intfc->ResolveFormID(formID, fixedID)) { - auto armor = skyrim_cast(RE::TESForm::LookupByID(fixedID)); - if (armor) - this->armors.insert(armor); - } - } - if (version >= ArmorAddonOverrideService::kSaveVersionV1) { - _assertRead(intfc->ReadRecordData(isFavorite), "Failed to read an outfit's favorite status."); - } else { - this->isFavorite = false; - } + std::uint32_t size = 0; + _assertRead(intfc->ReadRecordData(size), "Failed to read an outfit's armor count."); + for (std::uint32_t i = 0; i < size; i++) { + RE::FormID formID = 0; + _assertRead(intfc->ReadRecordData(formID), "Failed to read an outfit's armor."); + RE::FormID fixedID; + if (intfc->ResolveFormID(formID, fixedID)) { + auto armor = skyrim_cast(RE::TESForm::LookupByID(fixedID)); + if (armor) + this->armors.insert(armor); + } + } + if (version >= ArmorAddonOverrideService::kSaveVersionV1) { + _assertRead(intfc->ReadRecordData(isFavorite), "Failed to read an outfit's favorite status."); + } else { + this->isFavorite = false; + } } void Outfit::load(const proto::Outfit& proto, const SKSE::SerializationInterface* intfc) { - this->name = proto.name(); - for (const auto& formID : proto.armors()) { - RE::FormID fixedID; - if (intfc->ResolveFormID(formID, fixedID)) { - auto armor = skyrim_cast(RE::TESForm::LookupByID(fixedID)); - if (armor) - this->armors.insert(armor); - } - } - this->isFavorite = proto.is_favorite(); - this->allowsPassthrough = proto.allows_passthrough(); - this->requiresEquipped = proto.requires_equipped(); + this->name = proto.name(); + for (const auto& formID : proto.armors()) { + RE::FormID fixedID; + if (intfc->ResolveFormID(formID, fixedID)) { + auto armor = skyrim_cast(RE::TESForm::LookupByID(fixedID)); + if (armor) + this->armors.insert(armor); + } + } + this->isFavorite = proto.is_favorite(); + this->allowsPassthrough = proto.allows_passthrough(); + this->requiresEquipped = proto.requires_equipped(); } proto::Outfit Outfit::save() const { - proto::Outfit out; - out.set_name(this->name); - for (const auto& armor : this->armors) { - if (armor) - out.add_armors(armor->formID); - } - out.set_is_favorite(this->isFavorite); - out.set_allows_passthrough(this->allowsPassthrough); - out.set_requires_equipped(this->requiresEquipped); - return out; + proto::Outfit out; + out.set_name(this->name); + for (const auto& armor : this->armors) { + if (armor) + out.add_armors(armor->formID); + } + out.set_is_favorite(this->isFavorite); + out.set_allows_passthrough(this->allowsPassthrough); + out.set_requires_equipped(this->requiresEquipped); + return out; } void ArmorAddonOverrideService::_validateNameOrThrow(const char* outfitName) { - if (strcmp(outfitName, g_noOutfitName) == 0) - throw bad_name("Outfits can't use a blank name."); - if (strlen(outfitName) > ce_outfitNameMaxLength) - throw bad_name("The outfit's name is too long."); + if (strcmp(outfitName, g_noOutfitName) == 0) + throw bad_name("Outfits can't use a blank name."); + if (strlen(outfitName) > ce_outfitNameMaxLength) + throw bad_name("The outfit's name is too long."); } // Outfit& ArmorAddonOverrideService::getOutfit(const char* name) { - return this->outfits.at(name); + return this->outfits.at(name); } Outfit& ArmorAddonOverrideService::getOrCreateOutfit(const char* name) { - _validateNameOrThrow(name); - return this->outfits.emplace(name, name).first->second; + _validateNameOrThrow(name); + return this->outfits.emplace(name, name).first->second; } // void ArmorAddonOverrideService::addOutfit(const char* name) { - _validateNameOrThrow(name); - this->outfits.emplace(name, name); + _validateNameOrThrow(name); + this->outfits.emplace(name, name); } void ArmorAddonOverrideService::addOutfit(const char* name, std::vector armors) { - _validateNameOrThrow(name); - auto& created = this->outfits.emplace(name, name).first->second; - for (auto it = armors.begin(); it != armors.end(); ++it) { - auto armor = *it; - if (armor) - created.armors.insert(armor); - } + _validateNameOrThrow(name); + auto& created = this->outfits.emplace(name, name).first->second; + for (auto it = armors.begin(); it != armors.end(); ++it) { + auto armor = *it; + if (armor) + created.armors.insert(armor); + } } -Outfit& ArmorAddonOverrideService::currentOutfit(RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return g_noOutfit; - if (this->actorOutfitAssignments.at(target).currentOutfitName == g_noOutfitName) - return g_noOutfit; - try { - return outfits.at(actorOutfitAssignments.at(target).currentOutfitName); - } catch (std::out_of_range) { - return g_noOutfit; - } +Outfit& ArmorAddonOverrideService::currentOutfit(RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return g_noOutfit; + if (this->actorOutfitAssignments.at(target).currentOutfitName == g_noOutfitName) + return g_noOutfit; + try { + return outfits.at(actorOutfitAssignments.at(target).currentOutfitName); + } catch (std::out_of_range) { + return g_noOutfit; + } }; bool ArmorAddonOverrideService::hasOutfit(const char* name) const { - try { - static_cast(this->outfits.at(name)); - return true; - } catch (std::out_of_range) { - return false; - } + try { + static_cast(this->outfits.at(name)); + return true; + } catch (std::out_of_range) { + return false; + } } void ArmorAddonOverrideService::deleteOutfit(const char* name) { - this->outfits.erase(name); - for (auto& assn : actorOutfitAssignments) { - if (assn.second.currentOutfitName == name) - assn.second.currentOutfitName = g_noOutfitName; - // If the outfit is assigned as a location outfit, remove it there as well. - for (auto it = assn.second.locationOutfits.begin(); it != assn.second.locationOutfits.end(); ++it) { - if (it->second == name) { - assn.second.locationOutfits.erase(it); - break; - } - } - } + this->outfits.erase(name); + for (auto& assn : actorOutfitAssignments) { + if (assn.second.currentOutfitName == name) + assn.second.currentOutfitName = g_noOutfitName; + // If the outfit is assigned as a location outfit, remove it there as well. + for (auto it = assn.second.locationOutfits.begin(); it != assn.second.locationOutfits.end(); ++it) { + if (it->second == name) { + assn.second.locationOutfits.erase(it); + break; + } + } + } } void ArmorAddonOverrideService::setFavorite(const char* name, bool favorite) { - auto outfit = this->outfits.find(name); - if (outfit != this->outfits.end()) - outfit->second.isFavorite = favorite; + auto outfit = this->outfits.find(name); + if (outfit != this->outfits.end()) + outfit->second.isFavorite = favorite; } void ArmorAddonOverrideService::setOutfitPassthrough(const char* name, bool allowPassthrough) { - auto outfit = this->outfits.find(name); - if (outfit != this->outfits.end()) - outfit->second.allowsPassthrough = allowPassthrough; + auto outfit = this->outfits.find(name); + if (outfit != this->outfits.end()) + outfit->second.allowsPassthrough = allowPassthrough; } void ArmorAddonOverrideService::setOutfitEquipRequired(const char* name, bool requiresEquipped) { - auto outfit = this->outfits.find(name); - if (outfit != this->outfits.end()) - outfit->second.requiresEquipped = requiresEquipped; + auto outfit = this->outfits.find(name); + if (outfit != this->outfits.end()) + outfit->second.requiresEquipped = requiresEquipped; } void ArmorAddonOverrideService::modifyOutfit(const char* name, - std::vector& add, - std::vector& remove, - bool createIfMissing) { - try { - Outfit& target = this->getOutfit(name); - for (auto it = add.begin(); it != add.end(); ++it) { - auto armor = *it; - if (armor) - target.armors.insert(armor); - } - for (auto it = remove.begin(); it != remove.end(); ++it) { - auto armor = *it; - if (armor) - target.armors.erase(armor); - } - } catch (std::out_of_range) { - if (createIfMissing) { - this->addOutfit(name); - this->modifyOutfit(name, add, remove); - } - } + std::vector& add, + std::vector& remove, + bool createIfMissing) { + try { + Outfit& target = this->getOutfit(name); + for (auto it = add.begin(); it != add.end(); ++it) { + auto armor = *it; + if (armor) + target.armors.insert(armor); + } + for (auto it = remove.begin(); it != remove.end(); ++it) { + auto armor = *it; + if (armor) + target.armors.erase(armor); + } + } catch (std::out_of_range) { + if (createIfMissing) { + this->addOutfit(name); + this->modifyOutfit(name, add, remove); + } + } } void ArmorAddonOverrideService::renameOutfit(const char* oldName, const char* newName) { - _validateNameOrThrow(newName); - try { - static_cast(this->outfits.at(newName)); - throw name_conflict(""); - } catch (std::out_of_range) { - Outfit& renamed = (this->outfits[newName] = this->outfits - .at(oldName)); // don't try-catch this "at" call; let the caller catch the exception - renamed.name = newName; - this->outfits.erase(oldName); - for (auto& assn : actorOutfitAssignments) { - if (assn.second.currentOutfitName == oldName) - assn.second.currentOutfitName = newName; - // If the outfit is assigned as a location outfit, remove it there as well. - for (auto& locationOutfit : assn.second.locationOutfits) { - if (locationOutfit.second == oldName) { - assn.second.locationOutfits[locationOutfit.first] = newName; - break; - } - } - } - } + _validateNameOrThrow(newName); + try { + static_cast(this->outfits.at(newName)); + throw name_conflict(""); + } catch (std::out_of_range) { + Outfit& renamed = (this->outfits[newName] = this->outfits + .at(oldName));// don't try-catch this "at" call; let the caller catch the exception + renamed.name = newName; + this->outfits.erase(oldName); + for (auto& assn : actorOutfitAssignments) { + if (assn.second.currentOutfitName == oldName) + assn.second.currentOutfitName = newName; + // If the outfit is assigned as a location outfit, remove it there as well. + for (auto& locationOutfit : assn.second.locationOutfits) { + if (locationOutfit.second == oldName) { + assn.second.locationOutfits[locationOutfit.first] = newName; + break; + } + } + } + } } -void ArmorAddonOverrideService::setOutfit(const char* name, RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return; - if (strcmp(name, g_noOutfitName) == 0) { - this->actorOutfitAssignments.at(target).currentOutfitName = g_noOutfitName; - return; - } - try { - this->getOutfit(name); - this->actorOutfitAssignments.at(target).currentOutfitName = name; - } catch (std::out_of_range) { - LOG(info, - "ArmorAddonOverrideService: Tried to set non-existent outfit %s as active. Switching the system off for now.", - name); - this->actorOutfitAssignments.at(target).currentOutfitName = g_noOutfitName; - } +void ArmorAddonOverrideService::setOutfit(const char* name, RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return; + if (strcmp(name, g_noOutfitName) == 0) { + this->actorOutfitAssignments.at(target).currentOutfitName = g_noOutfitName; + return; + } + try { + this->getOutfit(name); + this->actorOutfitAssignments.at(target).currentOutfitName = name; + } catch (std::out_of_range) { + LOG(info, + "ArmorAddonOverrideService: Tried to set non-existent outfit %s as active. Switching the system off for now.", + name); + this->actorOutfitAssignments.at(target).currentOutfitName = g_noOutfitName; + } } -void ArmorAddonOverrideService::addActor(RE::Actor* target) { - if (actorOutfitAssignments.count(target) == 0) - actorOutfitAssignments.emplace(target, ActorOutfitAssignments()); +void ArmorAddonOverrideService::addActor(RE::RawActorHandle target) { + if (actorOutfitAssignments.count(target) == 0) + actorOutfitAssignments.emplace(target, ActorOutfitAssignments()); } -void ArmorAddonOverrideService::removeActor(RE::Actor* target) { - if (target == RE::PlayerCharacter::GetSingleton()) - return; - actorOutfitAssignments.erase(target); +void ArmorAddonOverrideService::removeActor(RE::RawActorHandle target) { + if (target == RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()) + return; + actorOutfitAssignments.erase(target); +} + +std::unordered_set ArmorAddonOverrideService::listActors() { + std::unordered_set actors; + for (auto& assignment : actorOutfitAssignments) { + actors.insert(assignment.first); + } + return actors; } void ArmorAddonOverrideService::setLocationBasedAutoSwitchEnabled(bool newValue) noexcept { - locationBasedAutoSwitchEnabled = newValue; + locationBasedAutoSwitchEnabled = newValue; } -void ArmorAddonOverrideService::setOutfitUsingLocation(LocationType location, RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return; - auto it = this->actorOutfitAssignments.at(target).locationOutfits.find(location); - if (it != this->actorOutfitAssignments.at(target).locationOutfits.end()) { - this->setOutfit(it->second.c_str(), target); - } +void ArmorAddonOverrideService::setOutfitUsingLocation(LocationType location, RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return; + auto it = this->actorOutfitAssignments.at(target).locationOutfits.find(location); + if (it != this->actorOutfitAssignments.at(target).locationOutfits.end()) { + this->setOutfit(it->second.c_str(), target); + } } -void ArmorAddonOverrideService::setLocationOutfit(LocationType location, const char* name, RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return; - if (!std::string(name).empty()) { // Can never set outfit to the "" outfit. Use unsetLocationOutfit instead. - this->actorOutfitAssignments.at(target).locationOutfits[location] = name; - } +void ArmorAddonOverrideService::setLocationOutfit(LocationType location, const char* name, RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return; + if (!std::string(name).empty()) {// Can never set outfit to the "" outfit. Use unsetLocationOutfit instead. + this->actorOutfitAssignments.at(target).locationOutfits[location] = name; + } } -void ArmorAddonOverrideService::unsetLocationOutfit(LocationType location, RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return; - this->actorOutfitAssignments.at(target).locationOutfits.erase(location); +void ArmorAddonOverrideService::unsetLocationOutfit(LocationType location, RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return; + this->actorOutfitAssignments.at(target).locationOutfits.erase(location); } -std::optional ArmorAddonOverrideService::getLocationOutfit(LocationType location, RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return std::optional();; - auto it = this->actorOutfitAssignments.at(target).locationOutfits.find(location); - if (it != this->actorOutfitAssignments.at(target).locationOutfits.end()) { - return std::optional(it->second); - } else { - return std::optional(); - } +std::optional ArmorAddonOverrideService::getLocationOutfit(LocationType location, RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return std::optional(); + ; + auto it = this->actorOutfitAssignments.at(target).locationOutfits.find(location); + if (it != this->actorOutfitAssignments.at(target).locationOutfits.end()) { + return std::optional(it->second); + } else { + return std::optional(); + } } -#define CHECK_LOCATION(TYPE, CHECK_CODE) if (this->actorOutfitAssignments.at(target).locationOutfits.count(LocationType::TYPE) && (CHECK_CODE)) return std::optional(LocationType::TYPE); +#define CHECK_LOCATION(TYPE, CHECK_CODE) \ + if (this->actorOutfitAssignments.at(target).locationOutfits.count(LocationType::TYPE) && (CHECK_CODE)) \ + return std::optional(LocationType::TYPE); std::optional ArmorAddonOverrideService::checkLocationType(const std::unordered_set& keywords, - const WeatherFlags& weather_flags, - RE::Actor* target) { - if (this->actorOutfitAssignments.count(target) == 0) - return std::optional(); + const WeatherFlags& weather_flags, + RE::RawActorHandle target) { + if (this->actorOutfitAssignments.count(target) == 0) + return std::optional(); - CHECK_LOCATION(CitySnowy, keywords.count("LocTypeCity") && weather_flags.snowy); - CHECK_LOCATION(CityRainy, keywords.count("LocTypeCity") && weather_flags.rainy); - CHECK_LOCATION(City, keywords.count("LocTypeCity")); + CHECK_LOCATION(CitySnowy, keywords.count("LocTypeCity") && weather_flags.snowy); + CHECK_LOCATION(CityRainy, keywords.count("LocTypeCity") && weather_flags.rainy); + CHECK_LOCATION(City, keywords.count("LocTypeCity")); - // A city is considered a town, so it will use the town outfit unless a city one is selected. - CHECK_LOCATION(TownSnowy, keywords.count("LocTypeTown") + keywords.count("LocTypeCity") && weather_flags.snowy); - CHECK_LOCATION(TownRainy, keywords.count("LocTypeTown") + keywords.count("LocTypeCity") && weather_flags.rainy); - CHECK_LOCATION(Town, keywords.count("LocTypeTown") + keywords.count("LocTypeCity")); + // A city is considered a town, so it will use the town outfit unless a city one is selected. + CHECK_LOCATION(TownSnowy, keywords.count("LocTypeTown") + keywords.count("LocTypeCity") && weather_flags.snowy); + CHECK_LOCATION(TownRainy, keywords.count("LocTypeTown") + keywords.count("LocTypeCity") && weather_flags.rainy); + CHECK_LOCATION(Town, keywords.count("LocTypeTown") + keywords.count("LocTypeCity")); - CHECK_LOCATION(DungeonSnowy, keywords.count("LocTypeDungeon") && weather_flags.snowy); - CHECK_LOCATION(DungeonRainy, keywords.count("LocTypeDungeon") && weather_flags.rainy); - CHECK_LOCATION(Dungeon, keywords.count("LocTypeDungeon")); + CHECK_LOCATION(DungeonSnowy, keywords.count("LocTypeDungeon") && weather_flags.snowy); + CHECK_LOCATION(DungeonRainy, keywords.count("LocTypeDungeon") && weather_flags.rainy); + CHECK_LOCATION(Dungeon, keywords.count("LocTypeDungeon")); - CHECK_LOCATION(WorldSnowy, weather_flags.snowy); - CHECK_LOCATION(WorldRainy, weather_flags.rainy); - CHECK_LOCATION(World, true); + CHECK_LOCATION(WorldSnowy, weather_flags.snowy); + CHECK_LOCATION(WorldRainy, weather_flags.rainy); + CHECK_LOCATION(World, true); - return std::optional(); + return std::optional(); } -bool ArmorAddonOverrideService::shouldOverride(RE::Actor* target) const noexcept { - if (!this->enabled) - return false; - if (this->actorOutfitAssignments.count(target) == 0) - return false; - if (this->actorOutfitAssignments.at(target).currentOutfitName == g_noOutfitName) - return false; - return true; +bool ArmorAddonOverrideService::shouldOverride(RE::RawActorHandle target) const noexcept { + if (!this->enabled) + return false; + if (this->actorOutfitAssignments.count(target) == 0) + return false; + if (this->actorOutfitAssignments.at(target).currentOutfitName == g_noOutfitName) + return false; + return true; } void ArmorAddonOverrideService::getOutfitNames(std::vector& out, bool favoritesOnly) const { - out.clear(); - auto& list = this->outfits; - out.reserve(list.size()); - for (auto it = list.cbegin(); it != list.cend(); ++it) - if (!favoritesOnly || it->second.isFavorite) - out.push_back(it->second.name); + out.clear(); + auto& list = this->outfits; + out.reserve(list.size()); + for (auto it = list.cbegin(); it != list.cend(); ++it) + if (!favoritesOnly || it->second.isFavorite) + out.push_back(it->second.name); } void ArmorAddonOverrideService::setEnabled(bool flag) noexcept { - this->enabled = flag; + this->enabled = flag; } // void ArmorAddonOverrideService::reset() { - this->enabled = true; - this->actorOutfitAssignments.clear(); - this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()] = ActorOutfitAssignments(); - this->outfits.clear(); - this->locationBasedAutoSwitchEnabled = false; + this->enabled = true; + this->actorOutfitAssignments.clear(); + this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()] = ActorOutfitAssignments(); + this->outfits.clear(); + this->locationBasedAutoSwitchEnabled = false; } void ArmorAddonOverrideService::load_legacy(const SKSE::SerializationInterface* intfc, std::uint32_t version) { - this->reset(); - // - std::string selectedOutfitName; - _assertWrite(intfc->ReadRecordData(this->enabled), "Failed to read the enable state."); - { // current outfit name - // - // we can't call WriteData directly on this->currentOutfitName because it's - // a cobb::istring, and SKSE only templated WriteData for std::string in - // specific; other basic_string classes break it. - // - std::uint32_t size = 0; - char buf[257]; - memset(buf, '\0', sizeof(buf)); - _assertRead(intfc->ReadRecordData(size), "Failed to read the selected outfit name."); - _assertRead(size < 257, "The selected outfit name is too long."); - if (size) { - _assertRead(intfc->ReadRecordData(buf, size), "Failed to read the selected outfit name."); - } - selectedOutfitName = buf; - } - std::uint32_t size; - _assertRead(intfc->ReadRecordData(size), "Failed to read the outfit count."); - for (std::uint32_t i = 0; i < size; i++) { - std::string name; - _assertRead(intfc->ReadRecordData(name), "Failed to read an outfit's name."); - auto& outfit = this->getOrCreateOutfit(name.c_str()); - outfit.load_legacy(intfc, version); - } - this->setOutfit(selectedOutfitName.c_str(), RE::PlayerCharacter::GetSingleton()); - if (version >= ArmorAddonOverrideService::kSaveVersionV3) { - _assertWrite(intfc->ReadRecordData(this->locationBasedAutoSwitchEnabled), - "Failed to read the autoswitch enable state."); - std::uint32_t autoswitchSize = - static_cast(this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()) - .locationOutfits.size()); - _assertRead(intfc->ReadRecordData(autoswitchSize), "Failed to read the number of autoswitch slots."); - for (std::uint32_t i = 0; i < autoswitchSize; i++) { - // get location outfit - // - // we can't call WriteData directly on this->currentOutfitName because it's - // a cobb::istring, and SKSE only templated WriteData for std::string in - // specific; other basic_string classes break it. - // - LocationType autoswitchSlot; - _assertRead(intfc->ReadRecordData(autoswitchSlot), "Failed to read the an autoswitch slot ID."); - std::uint32_t locationOutfitNameSize = 0; - char locationOutfitName[257]; - memset(locationOutfitName, '\0', sizeof(locationOutfitName)); - _assertRead(intfc->ReadRecordData(locationOutfitNameSize), "Failed to read the an autoswitch outfit name."); - _assertRead(locationOutfitNameSize < 257, "The autoswitch outfit name is too long."); - if (locationOutfitNameSize) { - _assertRead(intfc->ReadRecordData(locationOutfitName, locationOutfitNameSize), - "Failed to read the selected outfit name."); - this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()).locationOutfits - .emplace(autoswitchSlot, locationOutfitName); - } - } - } else { - this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()).locationOutfits = - std::map(); - } + this->reset(); + // + std::string selectedOutfitName; + _assertWrite(intfc->ReadRecordData(this->enabled), "Failed to read the enable state."); + {// current outfit name + // + // we can't call WriteData directly on this->currentOutfitName because it's + // a cobb::istring, and SKSE only templated WriteData for std::string in + // specific; other basic_string classes break it. + // + std::uint32_t size = 0; + char buf[257]; + memset(buf, '\0', sizeof(buf)); + _assertRead(intfc->ReadRecordData(size), "Failed to read the selected outfit name."); + _assertRead(size < 257, "The selected outfit name is too long."); + if (size) { + _assertRead(intfc->ReadRecordData(buf, size), "Failed to read the selected outfit name."); + } + selectedOutfitName = buf; + } + std::uint32_t size; + _assertRead(intfc->ReadRecordData(size), "Failed to read the outfit count."); + for (std::uint32_t i = 0; i < size; i++) { + std::string name; + _assertRead(intfc->ReadRecordData(name), "Failed to read an outfit's name."); + auto& outfit = this->getOrCreateOutfit(name.c_str()); + outfit.load_legacy(intfc, version); + } + this->setOutfit(selectedOutfitName.c_str(), RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()); + if (version >= ArmorAddonOverrideService::kSaveVersionV3) { + _assertWrite(intfc->ReadRecordData(this->locationBasedAutoSwitchEnabled), + "Failed to read the autoswitch enable state."); + std::uint32_t autoswitchSize = + static_cast(this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()) + .locationOutfits.size()); + _assertRead(intfc->ReadRecordData(autoswitchSize), "Failed to read the number of autoswitch slots."); + for (std::uint32_t i = 0; i < autoswitchSize; i++) { + // get location outfit + // + // we can't call WriteData directly on this->currentOutfitName because it's + // a cobb::istring, and SKSE only templated WriteData for std::string in + // specific; other basic_string classes break it. + // + LocationType autoswitchSlot; + _assertRead(intfc->ReadRecordData(autoswitchSlot), "Failed to read the an autoswitch slot ID."); + std::uint32_t locationOutfitNameSize = 0; + char locationOutfitName[257]; + memset(locationOutfitName, '\0', sizeof(locationOutfitName)); + _assertRead(intfc->ReadRecordData(locationOutfitNameSize), "Failed to read the an autoswitch outfit name."); + _assertRead(locationOutfitNameSize < 257, "The autoswitch outfit name is too long."); + if (locationOutfitNameSize) { + _assertRead(intfc->ReadRecordData(locationOutfitName, locationOutfitNameSize), + "Failed to read the selected outfit name."); + this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()).locationOutfits.emplace(autoswitchSlot, locationOutfitName); + } + } + } else { + this->actorOutfitAssignments.at(RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()).locationOutfits = + std::map(); + } } void ArmorAddonOverrideService::load(const SKSE::SerializationInterface* intfc, const proto::OutfitSystem& data) { - this->reset(); - // Extract data from the protobuf struct. - this->enabled = data.enabled(); - std::map actorOutfitAssignmentsLocal; - for (const auto& actorAssn : data.actor_outfit_assignments()) { - // Lookup the actor - std::uint64_t handle; - RE::NiPointer actor; - if (!intfc->ResolveHandle(actorAssn.first, handle)) - continue; - if (!RE::LookupReferenceByHandle(static_cast(handle), actor)) - continue; + this->reset(); + // Extract data from the protobuf struct. + this->enabled = data.enabled(); + std::map actorOutfitAssignmentsLocal; + for (const auto& actorAssn : data.actor_outfit_assignments()) { + // Lookup the actor + std::uint64_t handle; + RE::NiPointer actor; + if (!intfc->ResolveHandle(actorAssn.first, handle)) + continue; - ActorOutfitAssignments assignments; - assignments.currentOutfitName = - cobb::istring(actorAssn.second.current_outfit_name().data(), actorAssn.second.current_outfit_name().size()); - for (const auto& locOutfitData : actorAssn.second.location_based_outfits()) { - assignments.locationOutfits.emplace(LocationType(locOutfitData.first), - cobb::istring(locOutfitData.second.data(), - locOutfitData.second.size())); - } - actorOutfitAssignmentsLocal[actor.get()] = assignments; - } - this->actorOutfitAssignments = actorOutfitAssignmentsLocal; - for (const auto& outfitData : data.outfits()) { - Outfit outfit; - outfit.load(outfitData, intfc); - this->outfits.emplace(cobb::istring(outfitData.name().data(), outfitData.name().size()), outfit); - } - this->locationBasedAutoSwitchEnabled = data.location_based_auto_switch_enabled(); + ActorOutfitAssignments assignments; + assignments.currentOutfitName = + cobb::istring(actorAssn.second.current_outfit_name().data(), actorAssn.second.current_outfit_name().size()); + for (const auto& locOutfitData : actorAssn.second.location_based_outfits()) { + assignments.locationOutfits.emplace(LocationType(locOutfitData.first), + cobb::istring(locOutfitData.second.data(), + locOutfitData.second.size())); + } + actorOutfitAssignmentsLocal[static_cast(handle)] = assignments; + } + this->actorOutfitAssignments = actorOutfitAssignmentsLocal; + for (const auto& outfitData : data.outfits()) { + Outfit outfit; + outfit.load(outfitData, intfc); + this->outfits.emplace(cobb::istring(outfitData.name().data(), outfitData.name().size()), outfit); + } + this->locationBasedAutoSwitchEnabled = data.location_based_auto_switch_enabled(); - // If the loaded actor assignments are empty, then we know we loaded the old format... convert from that instead. - if (actorOutfitAssignmentsLocal.empty()) { - this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()].currentOutfitName = - cobb::istring(data.obsolete_current_outfit_name().data(), data.obsolete_current_outfit_name().size()); - for (const auto& locOutfitData : data.obsolete_location_based_outfits()) { - this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()].locationOutfits - .emplace(LocationType(locOutfitData.first), - cobb::istring(locOutfitData.second.data(), locOutfitData.second.size())); - } - } + // If the loaded actor assignments are empty, then we know we loaded the old format... convert from that instead. + if (actorOutfitAssignmentsLocal.empty()) { + this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()].currentOutfitName = + cobb::istring(data.obsolete_current_outfit_name().data(), data.obsolete_current_outfit_name().size()); + for (const auto& locOutfitData : data.obsolete_location_based_outfits()) { + this->actorOutfitAssignments[RE::PlayerCharacter::GetSingleton()->GetHandle().native_handle()].locationOutfits.emplace(LocationType(locOutfitData.first), + cobb::istring(locOutfitData.second.data(), locOutfitData.second.size())); + } + } } proto::OutfitSystem ArmorAddonOverrideService::save() { - proto::OutfitSystem out; - out.set_enabled(this->enabled); - for (const auto& actorAssn : actorOutfitAssignments) { - // Store a reference to the actor - std::uint64_t handle; - RE::RefHandle handleTmp; - RE::CreateRefHandle(handleTmp, actorAssn.first); - handle = handleTmp; - - proto::ActorOutfitAssignment assnOut; - assnOut.set_current_outfit_name(actorAssn.second.currentOutfitName.data(), - actorAssn.second.currentOutfitName.size()); - for (const auto& lbo : actorAssn.second.locationOutfits) { - assnOut.mutable_location_based_outfits() - ->insert({ static_cast(lbo.first), std::string(lbo.second.data(), lbo.second.size()) }); - } - out.mutable_actor_outfit_assignments()->insert({ handle, assnOut }); - } - for (const auto& outfit : this->outfits) { - auto newOutfit = out.add_outfits(); - *newOutfit = outfit.second.save(); - } - out.set_location_based_auto_switch_enabled(this->locationBasedAutoSwitchEnabled); - return out; + proto::OutfitSystem out; + out.set_enabled(this->enabled); + for (const auto& actorAssn : actorOutfitAssignments) { + // Store a reference to the actor + std::uint64_t handle; + handle = actorAssn.first; + proto::ActorOutfitAssignment assnOut; + assnOut.set_current_outfit_name(actorAssn.second.currentOutfitName.data(), + actorAssn.second.currentOutfitName.size()); + for (const auto& lbo : actorAssn.second.locationOutfits) { + assnOut.mutable_location_based_outfits() + ->insert({static_cast(lbo.first), std::string(lbo.second.data(), lbo.second.size())}); + } + out.mutable_actor_outfit_assignments()->insert({handle, assnOut}); + } + for (const auto& outfit : this->outfits) { + auto newOutfit = out.add_outfits(); + *newOutfit = outfit.second.save(); + } + out.set_location_based_auto_switch_enabled(this->locationBasedAutoSwitchEnabled); + return out; } // void ArmorAddonOverrideService::dump() const { - LOG(info, "Dumping all state for ArmorAddonOverrideService..."); - LOG(info, "Enabled: %d", this->enabled); - LOG(info, "We have %d outfits. Enumerating...", this->outfits.size()); - for (auto it = this->outfits.begin(); it != this->outfits.end(); ++it) { - LOG(info, " - Key: %s", it->first.c_str()); - LOG(info, " - Name: %s", it->second.name.c_str()); - LOG(info, " - Armors:"); - auto& list = it->second.armors; - for (auto jt = list.begin(); jt != list.end(); ++jt) { - auto ptr = *jt; - if (ptr) { - LOG(info, " - (TESObjectARMO*){} == [ARMO:{}]", (void*)ptr, ptr->formID); - } else { - LOG(info, " - nullptr"); - } - } - } - LOG(info, "All state has been dumped."); + LOG(info, "Dumping all state for ArmorAddonOverrideService..."); + LOG(info, "Enabled: %d", this->enabled); + LOG(info, "We have %d outfits. Enumerating...", this->outfits.size()); + for (auto it = this->outfits.begin(); it != this->outfits.end(); ++it) { + LOG(info, " - Key: %s", it->first.c_str()); + LOG(info, " - Name: %s", it->second.name.c_str()); + LOG(info, " - Armors:"); + auto& list = it->second.armors; + for (auto jt = list.begin(); jt != list.end(); ++jt) { + auto ptr = *jt; + if (ptr) { + LOG(info, " - (TESObjectARMO*){} == [ARMO:{}]", (void*) ptr, ptr->formID); + } else { + LOG(info, " - nullptr"); + } + } + } + LOG(info, "All state has been dumped."); } diff --git a/src/OutfitSystem.cpp b/src/OutfitSystem.cpp index 301303b..6c76864 100644 --- a/src/OutfitSystem.cpp +++ b/src/OutfitSystem.cpp @@ -4,1174 +4,1249 @@ #include "ArmorAddonOverrideService.h" +#include "RE/REAugments.h" #include "cobb/strings.h" -#include "cobb/utf8string.h" #include "cobb/utf8naturalsort.h" +#include "cobb/utf8string.h" #include #include "google/protobuf/util/json_util.h" -#define ERROR_AND_RETURN_EXPR_IF(condition, message, valueExpr, registry, stackId) \ - if (condition) \ - { \ - registry->TraceStack(message, stackId, RE::BSScript::IVirtualMachine::Severity::kError); \ - return (valueExpr); \ +#define ERROR_AND_RETURN_EXPR_IF(condition, message, valueExpr, registry, stackId) \ + if (condition) { \ + registry->TraceStack(message, stackId, RE::BSScript::IVirtualMachine::Severity::kError); \ + return (valueExpr); \ } -#define ERROR_AND_RETURN_IF(condition, message, registry, stackId) \ - if (condition) \ - { \ - registry->TraceStack(message, stackId, RE::BSScript::IVirtualMachine::Severity::kError); \ - return; \ +#define ERROR_AND_RETURN_IF(condition, message, registry, stackId) \ + if (condition) { \ + registry->TraceStack(message, stackId, RE::BSScript::IVirtualMachine::Severity::kError); \ + return; \ } namespace OutfitSystem { - std::int32_t GetOutfitNameMaxLength(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - return ArmorAddonOverrideService::ce_outfitNameMaxLength; - } - std::vector GetCarriedArmor(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::Actor* target_skse) { - std::vector result; - auto target = (RE::Actor*)(target_skse); - if (target == nullptr) { - registry->TraceStack("Cannot retrieve data for a None RE::Actor.", - stackId, - RE::BSScript::IVirtualMachine::Severity::kError); - std::vector empty; - return empty; - } - // - class _Visitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // If the player has a shield equipped, and if we're not overriding that - // shield, then we need to grab the equipped shield's worn-flags. - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - // Return true to continue, or else false to break. - const auto form = data->object; - if (form && form->formType == RE::FormType::Armor) - this->list.push_back(reinterpret_cast(form)); - return ReturnType::kContinue; - }; + std::int32_t GetOutfitNameMaxLength(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("GetOutfitNameMaxLength"sv); + return ArmorAddonOverrideService::ce_outfitNameMaxLength; + } + std::vector GetCarriedArmor(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* target) { + LogExit exitPrint("GetCarriedArmor"sv); + std::vector result; + if (target == nullptr) { + registry->TraceStack("Cannot retrieve data for a None RE::Actor.", + stackId, + RE::BSScript::IVirtualMachine::Severity::kError); + std::vector empty; + return empty; + } + // + class _Visitor: public RE::IItemChangeVisitorAugment { + // + // If the player has a shield equipped, and if we're not overriding that + // shield, then we need to grab the equipped shield's worn-flags. + // + public: + virtual VisitorReturn Visit(RE::InventoryEntryData* data) override { + // Return true to continue, or else false to break. + const auto form = data->object; + if (form && form->formType == RE::FormType::Armor) { + auto armor = skyrim_cast(form); + if (armor) this->list.push_back(armor); + } + return VisitorReturn::kContinue; + }; - std::vector& list; - // - _Visitor(std::vector& l) : list(l) {}; - }; - auto inventory = target->GetInventoryChanges(); - if (inventory) { - _Visitor visitor(result); - inventory->ExecuteVisitor(&visitor); - } - std::vector converted_result; - converted_result.reserve(result.size()); - for (const auto ptr : result) { - converted_result.push_back((RE::TESObjectARMO*)ptr); - } - return converted_result; - } - std::vector GetWornItems( - RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::Actor* target_skse) { - std::vector result; - auto target = (RE::Actor*)(target_skse); - if (target == nullptr) { - registry->TraceStack("Cannot retrieve data for a None RE::Actor.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - std::vector empty; - return empty; - } - // - class _Visitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // If the player has a shield equipped, and if we're not overriding that - // shield, then we need to grab the equipped shield's worn-flags. - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - auto form = data->object; - if (form && form->formType == RE::FormType::Armor) - this->list.push_back(reinterpret_cast(form)); - return ReturnType::kContinue; - }; + std::vector& list; + // + _Visitor(std::vector& l) : list(l){}; + }; + auto inventory = target->GetInventoryChanges(); + if (inventory) { + _Visitor visitor(result); + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + } + std::vector converted_result; + converted_result.reserve(result.size()); + for (const auto ptr : result) { + converted_result.push_back((RE::TESObjectARMO*) ptr); + } + return converted_result; + } + std::vector GetWornItems( + RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* target) { + LogExit exitPrint("GetWornItems"sv); + std::vector result; + if (target == nullptr) { + registry->TraceStack("Cannot retrieve data for a None RE::Actor.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + std::vector empty; + return empty; + } + // + class _Visitor: public RE::IItemChangeVisitorAugment { + // + // If the player has a shield equipped, and if we're not overriding that + // shield, then we need to grab the equipped shield's worn-flags. + // + public: + virtual VisitorReturn Visit(RE::InventoryEntryData* data) override { + auto form = data->object; + if (form && form->formType == RE::FormType::Armor) { + auto armor = skyrim_cast(form); + if (armor) this->list.push_back(armor); + } + return VisitorReturn::kContinue; + }; - std::vector& list; - // - _Visitor(std::vector& l) : list(l) {}; - }; - auto inventory = target->GetInventoryChanges(); - if (inventory) { - _Visitor visitor(result); - inventory->ExecuteVisitorOnWorn(&visitor); - } - std::vector converted_result; - converted_result.reserve(result.size()); - for (const auto ptr : result) { - converted_result.push_back((RE::TESObjectARMO*)ptr); - } - return converted_result; - } - void RefreshArmorFor(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::Actor* target_skse) { - auto target = (RE::Actor*)(target_skse); - ERROR_AND_RETURN_IF(target == nullptr, "Cannot refresh armor on a None RE::Actor.", registry, stackId); - auto pm = target->currentProcess; - if (pm) { - // - // "SetEquipFlag" tells the process manager that the RE::Actor's - // equipment has changed, and that their ArmorAddons should - // be updated. If you need to find it in Skyrim Special, you - // should see a call near the start of EquipManager's func- - // tion to equip an item. - // - // NOTE: AIProcess is also called as RE::ActorProcessManager - pm->SetEquipFlag(RE::AIProcess::Flag::kUnk01); - pm->UpdateEquipment(target); - } - } - std::vector ActorsNearPC(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto pc = RE::PlayerCharacter::GetSingleton(); - ERROR_AND_RETURN_EXPR_IF(pc == nullptr, "Could not get PC Singleton.", result, registry, stackId); - auto pcCell = pc->GetParentCell(); - ERROR_AND_RETURN_EXPR_IF(pcCell == nullptr, "Could not get cell of PC.", result, registry, stackId); - result.reserve(pcCell->references.size()); - for (const auto& ref : pcCell->references) { - RE::TESObjectREFR* objectRefPtr = ref.get(); - auto actorCastedPtr = skyrim_cast(objectRefPtr); - if (actorCastedPtr) - result.push_back(actorCastedPtr); - } - result.shrink_to_fit(); - return result; - } + std::vector& list; + // + _Visitor(std::vector& l) : list(l){}; + }; + auto inventory = target->GetInventoryChanges(); + if (inventory) { + _Visitor visitor(result); + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + } + std::vector converted_result; + converted_result.reserve(result.size()); + for (const auto ptr : result) { + converted_result.push_back((RE::TESObjectARMO*) ptr); + } + return converted_result; + } + void RefreshArmorFor(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* target) { + LogExit exitPrint("RefreshArmorFor"sv); + ERROR_AND_RETURN_IF(target == nullptr, "Cannot refresh armor on a None RE::Actor.", registry, stackId); + auto pm = target->GetActorRuntimeData().currentProcess; + if (pm) { + // + // "SetEquipFlag" tells the process manager that the RE::Actor's + // equipment has changed, and that their ArmorAddons should + // be updated. If you need to find it in Skyrim Special, you + // should see a call near the start of EquipManager's func- + // tion to equip an item. + // + // NOTE: AIProcess is also called as RE::ActorProcessManager + RE::AIProcessAugments::SetEquipFlag(pm, RE::AIProcessAugments::Flag::kUnk01); + RE::AIProcessAugments::UpdateEquipment(pm, target); + } + } + void RefreshArmorForAllConfiguredActors(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("RefreshArmorForAllConfiguredActors"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + auto actors = service.listActors(); + for (auto& actorRef : actors) { + auto actor = RE::Actor::LookupByHandle(actorRef); + if (!actor) + continue; + auto pm = actor->GetActorRuntimeData().currentProcess; + if (pm) { + // + // "SetEquipFlag" tells the process manager that the RE::Actor's + // equipment has changed, and that their ArmorAddons should + // be updated. If you need to find it in Skyrim Special, you + // should see a call near the start of EquipManager's func- + // tion to equip an item. + // + // NOTE: AIProcess is also called as RE::ActorProcessManager + RE::AIProcessAugments::SetEquipFlag(pm, RE::AIProcessAugments::Flag::kUnk01); + RE::AIProcessAugments::UpdateEquipment(pm, actor.get()); + } + } + } - // - namespace ArmorFormSearchUtils { - static struct { - std::vector names; - std::vector armors; - // - void setup(std::string nameFilter, bool mustBePlayable) { - auto data = RE::TESDataHandler::GetSingleton(); - auto& list = data->GetFormArray(RE::FormType::Armor); - const auto size = list.size(); - this->names.reserve(size); - this->armors.reserve(size); - for (std::uint32_t i = 0; i < size; i++) { - const auto form = list[i]; - if (form && form->formType == RE::FormType::Armor) { - auto armor = static_cast(form); - if (armor->templateArmor) // filter out predefined enchanted variants, to declutter the list - continue; - if (mustBePlayable && !!(armor->formFlags & RE::TESObjectARMO::RecordFlags::kNonPlayable)) - continue; - std::string armorName; - { // get name - auto tfn = skyrim_cast(armor); - if (tfn) - armorName = tfn->fullName.data(); - } - if (armorName.empty()) // skip nameless armor - continue; - if (!nameFilter.empty()) { - auto it = std::search( - armorName.begin(), armorName.end(), - nameFilter.begin(), nameFilter.end(), - [](char a, char b) { return toupper(a) == toupper(b); } - ); - if (it == armorName.end()) - continue; - } - this->armors.push_back(armor); - this->names.push_back(armorName.c_str()); - } - } - } - void clear() { - this->names.clear(); - this->armors.clear(); - } - } data; - // - // - void Prep(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString filter, - bool mustBePlayable) { - data.setup(filter.data(), mustBePlayable); - } - std::vector GetForms(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto& list = data.armors; - for (auto it = list.begin(); it != list.end(); it++) - result.push_back(*it); - std::vector converted_result; - converted_result.reserve(result.size()); - for (const auto ptr : result) { - converted_result.push_back((RE::TESObjectARMO*)ptr); - } - return converted_result; - } - std::vector GetNames(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto& list = data.names; - for (auto it = list.begin(); it != list.end(); it++) - result.push_back(it->c_str()); - return result; - } - void Clear(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { - data.clear(); - } - } - namespace BodySlotListing { - enum { - kBodySlotMin = 30, - kBodySlotMax = 61, - }; - static struct { - std::vector bodySlots; - std::vector armorNames; - std::vector armors; - } data; - // - void Clear(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { - data.bodySlots.clear(); - data.armorNames.clear(); - data.armors.clear(); - } - void Prep(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString name) { - data.bodySlots.clear(); - data.armorNames.clear(); - data.armors.clear(); - // - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - auto& armors = outfit.armors; - for (std::uint8_t i = kBodySlotMin; i <= kBodySlotMax; i++) { - std::uint32_t mask = 1 << (i - kBodySlotMin); - for (auto it = armors.begin(); it != armors.end(); it++) { - RE::TESObjectARMO* armor = *it; - if (armor && (static_cast(armor->GetSlotMask()) & mask)) { - data.bodySlots.push_back(i); - data.armors.push_back(armor); - { // name - // TESFullName* pFullName = DYNAMIC_CAST(armor, RE::TESObjectARMO, TESFullName); - auto pFullName = skyrim_cast(armor); - if (pFullName) - data.armorNames.emplace_back(pFullName->fullName.data()); - else - data.armorNames.emplace_back(""); - } - } - } - } - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - } - std::vector GetArmorForms(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto& list = data.armors; - for (auto it = list.begin(); it != list.end(); it++) - result.push_back(*it); - std::vector converted_result; - converted_result.reserve(result.size()); - for (const auto ptr : result) { - converted_result.push_back((RE::TESObjectARMO*)ptr); - } - return converted_result; - } - std::vector GetArmorNames(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto& list = data.armorNames; - for (auto it = list.begin(); it != list.end(); it++) - result.push_back(it->c_str()); - return result; - } - std::vector GetSlotIndices(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - auto& list = data.bodySlots; - for (auto it = list.begin(); it != list.end(); it++) - result.push_back(*it); - return result; - } - } - namespace StringSorts { - std::vector NaturalSort_ASCII(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - std::vector arr, - bool descending) { - std::vector result = arr; - std::sort( - result.begin(), - result.end(), - [descending](const RE::BSFixedString& x, const RE::BSFixedString& y) { - std::string a(x.data()); - std::string b(y.data()); - if (descending) - std::swap(a, b); - return cobb::utf8::naturalcompare(a, b) > 0; - } - ); - return result; - } + std::vector ActorsNearPC(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("RefreshArmorForAllConfiguredActors"sv); + std::vector result; + auto pc = RE::PlayerCharacter::GetSingleton(); + ERROR_AND_RETURN_EXPR_IF(pc == nullptr, "Could not get PC Singleton.", result, registry, stackId); + auto pcCell = pc->GetParentCell(); + ERROR_AND_RETURN_EXPR_IF(pcCell == nullptr, "Could not get cell of PC.", result, registry, stackId); + result.reserve(pcCell->GetRuntimeData().references.size()); + for (const auto& ref : pcCell->GetRuntimeData().references) { + RE::TESObjectREFR* objectRefPtr = ref.get(); + auto actorCastedPtr = skyrim_cast(objectRefPtr); + if (actorCastedPtr) + result.push_back(actorCastedPtr); + } + result.shrink_to_fit(); + return result; + } - // TODO: You need to change all the papyrus scripts that were assuming behavior here. - template std::vector NaturalSortPair_ASCII( - RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - std::vector arr, // Array of string - std::vector second, // Array of forms (T) - bool descending) { - std::size_t size = arr.size(); - if (size != second.size()) { - registry->TraceStack("The two arrays must be the same length.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return second; - } - // - typedef std::pair _pair; - std::vector<_pair> pairs; - // - std::vector result; - { // Copy input array into output array - result.reserve(size); - for (std::uint32_t i = 0; i < size; i++) { - pairs.emplace_back(arr[i], second[i]); - } - } - std::sort( - pairs.begin(), - pairs.end(), - [descending](const _pair& x, const _pair& y) { - std::string a(x.first.data()); - std::string b(y.first.data()); - if (descending) - std::swap(a, b); - return cobb::utf8::naturalcompare(a, b) > 0; - } - ); - for (std::uint32_t i = 0; i < size; i++) { - result.push_back(pairs[i].first); - second[i] = pairs[i].second; - } - return second; - } - } - namespace Utility { - std::uint32_t HexToInt32(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString str) { - const char* s = str.data(); - char* discard; - return strtoul(s, &discard, 16); - } - RE::BSFixedString ToHex(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + // + namespace ArmorFormSearchUtils { + static struct { + std::vector names; + std::vector armors; + // + void setup(std::string nameFilter, bool mustBePlayable) { + LogExit exitPrint("ArmorFormSearchUtils.setup"sv); + auto data = RE::TESDataHandler::GetSingleton(); + auto& list = data->GetFormArray(RE::FormType::Armor); + const auto size = list.size(); + this->names.reserve(size); + this->armors.reserve(size); + for (std::uint32_t i = 0; i < size; i++) { + const auto form = list[i]; + if (form && form->formType == RE::FormType::Armor) { + auto armor = skyrim_cast(form); + if (!armor) continue; + if (armor->templateArmor)// filter out predefined enchanted variants, to declutter the list + continue; + if (mustBePlayable && !!(armor->formFlags & RE::TESObjectARMO::RecordFlags::kNonPlayable)) + continue; + std::string armorName; + {// get name + auto tfn = skyrim_cast(armor); + if (tfn) + armorName = tfn->fullName.data(); + } + if (armorName.empty())// skip nameless armor + continue; + if (!nameFilter.empty()) { + auto it = std::search( + armorName.begin(), armorName.end(), + nameFilter.begin(), nameFilter.end(), + [](char a, char b) { return toupper(a) == toupper(b); }); + if (it == armorName.end()) + continue; + } + this->armors.push_back(armor); + this->names.push_back(armorName.c_str()); + } + } + } + void clear() { + this->names.clear(); + this->armors.clear(); + } + } data; + // + // + void Prep(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString filter, + bool mustBePlayable) { + LogExit exitPrint("ArmorFormSearchUtils.Prep"sv); + data.setup(filter.data(), mustBePlayable); + } + std::vector GetForms(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("ArmorFormSearchUtils.GetForms"sv); + std::vector result; + auto& list = data.armors; + for (auto it = list.begin(); it != list.end(); it++) + result.push_back(*it); + std::vector converted_result; + converted_result.reserve(result.size()); + for (const auto ptr : result) { + converted_result.push_back((RE::TESObjectARMO*) ptr); + } + return converted_result; + } + std::vector GetNames(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("ArmorFormSearchUtils.GetNames"sv); + std::vector result; + auto& list = data.names; + for (auto it = list.begin(); it != list.end(); it++) + result.push_back(it->c_str()); + return result; + } + void Clear(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { + LogExit exitPrint("ArmorFormSearchUtils.Clear"sv); + data.clear(); + } + }// namespace ArmorFormSearchUtils + namespace BodySlotListing { + enum { + kBodySlotMin = 30, + kBodySlotMax = 61, + }; + static struct { + std::vector bodySlots; + std::vector armorNames; + std::vector armors; + } data; + // + void Clear(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { + LogExit exitPrint("BodySlotListing.Clear"sv); + data.bodySlots.clear(); + data.armorNames.clear(); + data.armors.clear(); + } + void Prep(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("BodySlotListing.Prep"sv); + data.bodySlots.clear(); + data.armorNames.clear(); + data.armors.clear(); + // + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + auto& armors = outfit.armors; + for (std::uint8_t i = kBodySlotMin; i <= kBodySlotMax; i++) { + std::uint32_t mask = 1 << (i - kBodySlotMin); + for (auto it = armors.begin(); it != armors.end(); it++) { + RE::TESObjectARMO* armor = *it; + if (armor && (static_cast(armor->GetSlotMask()) & mask)) { + data.bodySlots.push_back(i); + data.armors.push_back(armor); + {// name + // TESFullName* pFullName = DYNAMIC_CAST(armor, RE::TESObjectARMO, TESFullName); + auto pFullName = skyrim_cast(armor); + if (pFullName) + data.armorNames.emplace_back(pFullName->fullName.data()); + else + data.armorNames.emplace_back(""); + } + } + } + } + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + } + std::vector GetArmorForms(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("BodySlotListing.GetArmorForms"sv); + std::vector result; + auto& list = data.armors; + for (auto it = list.begin(); it != list.end(); it++) + result.push_back(*it); + std::vector converted_result; + converted_result.reserve(result.size()); + for (const auto ptr : result) { + converted_result.push_back((RE::TESObjectARMO*) ptr); + } + return converted_result; + } + std::vector GetArmorNames(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("BodySlotListing.GetArmorNames"sv); + std::vector result; + auto& list = data.armorNames; + for (auto it = list.begin(); it != list.end(); it++) + result.push_back(it->c_str()); + return result; + } + std::vector GetSlotIndices(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("BodySlotListing.GetSlotIndices"sv); + std::vector result; + auto& list = data.bodySlots; + for (auto it = list.begin(); it != list.end(); it++) + result.push_back(*it); + return result; + } + }// namespace BodySlotListing + namespace StringSorts { + std::vector NaturalSort_ASCII(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + std::vector arr, + bool descending) { + LogExit exitPrint("StringSorts.NaturalSort_ASCII"sv); + std::vector result = arr; + std::sort( + result.begin(), + result.end(), + [descending](const RE::BSFixedString& x, const RE::BSFixedString& y) { + std::string a(x.data()); + std::string b(y.data()); + if (descending) + std::swap(a, b); + return cobb::utf8::naturalcompare(a, b) > 0; + }); + return result; + } - std::uint32_t value, - std::int32_t length) { - if (length < 1) { - registry->TraceStack( - "Cannot format a hexadecimal valueinteger to a negative number of digits. Defaulting to eight.", - stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - length = 8; - } else if (length > 8) { - registry->TraceStack("Cannot format a hexadecimal integer longer than eight digits.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - length = 8; - } - char hex[9]; - memset(hex, '0', sizeof(hex)); - hex[length] = '\0'; - while (value > 0 && length--) { - std::uint8_t digit = value % 0x10; - value /= 0x10; - if (digit < 0xA) { - hex[length] = digit + '0'; - } else { - hex[length] = digit + 0x37; - } - } - return hex; // passes through RE::BSFixedString constructor, which I believe caches the string, so returning local vars should be fine - } - } - // - void AddArmorToOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + template + std::vector NaturalSortPair_ASCII( + RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + std::vector arr,// Array of string + std::vector second, // Array of forms (T) + bool descending) { + LogExit exitPrint("StringSorts.NaturalSortPair_ASCII"sv); + std::size_t size = arr.size(); + if (size != second.size()) { + registry->TraceStack("The two arrays must be the same length.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return second; + } + // + typedef std::pair _pair; + std::vector<_pair> pairs; + // + std::vector result; + {// Copy input array into output array + result.reserve(size); + for (std::uint32_t i = 0; i < size; i++) { + pairs.emplace_back(arr[i], second[i]); + } + } + std::sort( + pairs.begin(), + pairs.end(), + [descending](const _pair& x, const _pair& y) { + std::string a(x.first.data()); + std::string b(y.first.data()); + if (descending) + std::swap(a, b); + return cobb::utf8::naturalcompare(a, b) > 0; + }); + for (std::uint32_t i = 0; i < size; i++) { + result.push_back(pairs[i].first); + second[i] = pairs[i].second; + } + return second; + } + }// namespace StringSorts + namespace Utility { + std::uint32_t HexToInt32(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString str) { + LogExit exitPrint("Utility.HexToInt32"sv); + const char* s = str.data(); + char* discard; + return strtoul(s, &discard, 16); + } + RE::BSFixedString ToHex(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - RE::BSFixedString name, - RE::TESObjectARMO* armor_skse) { - auto armor = (RE::TESObjectARMO*)(armor_skse); - ERROR_AND_RETURN_IF(armor == nullptr, "Cannot add a None armor to an outfit.", registry, stackId); - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - outfit.armors.insert(armor); - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - } - bool ArmorConflictsWithOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + std::uint32_t value, + std::int32_t length) { + LogExit exitPrint("Utility.ToHex"sv); + if (length < 1) { + registry->TraceStack( + "Cannot format a hexadecimal valueinteger to a negative number of digits. Defaulting to eight.", + stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + length = 8; + } else if (length > 8) { + registry->TraceStack("Cannot format a hexadecimal integer longer than eight digits.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + length = 8; + } + char hex[9]; + memset(hex, '0', sizeof(hex)); + hex[length] = '\0'; + while (value > 0 && length--) { + std::uint8_t digit = value % 0x10; + value /= 0x10; + if (digit < 0xA) { + hex[length] = digit + '0'; + } else { + hex[length] = digit + 0x37; + } + } + return hex;// passes through RE::BSFixedString constructor, which I believe caches the string, so returning local vars should be fine + } + }// namespace Utility + // + void AddArmorToOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - RE::TESObjectARMO* armor_skse, - RE::BSFixedString name) { - auto armor = (RE::TESObjectARMO*)(armor_skse); - if (armor == nullptr) { - registry->TraceStack("A None armor can't conflict with anything in an outfit.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - return false; - } - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - return outfit.conflictsWith(armor); - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - return false; - } - } - void CreateOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - service.addOutfit(name.data()); - } catch (ArmorAddonOverrideService::bad_name) { - registry->TraceStack("Invalid outfit name specified.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return; - } - } - void DeleteOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.deleteOutfit(name.data()); - } - std::vector GetOutfitContents(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name, + RE::TESObjectARMO* armor_skse) { + LogExit exitPrint("AddArmorToOutfit"sv); + auto armor = (RE::TESObjectARMO*) (armor_skse); + ERROR_AND_RETURN_IF(armor == nullptr, "Cannot add a None armor to an outfit.", registry, stackId); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + outfit.armors.insert(armor); + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + } + bool ArmorConflictsWithOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name) { - std::vector result; - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - auto& armors = outfit.armors; - for (auto it = armors.begin(); it != armors.end(); ++it) - result.push_back(*it); - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - std::vector converted_result; - converted_result.reserve(result.size()); - for (const auto ptr : result) { - converted_result.push_back((RE::TESObjectARMO*)ptr); - } - return converted_result; - } - bool GetOutfitFavoriteStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::TESObjectARMO* armor_skse, + RE::BSFixedString name) { + LogExit exitPrint("ArmorConflictsWithOutfit"sv); + auto armor = (RE::TESObjectARMO*) (armor_skse); + if (armor == nullptr) { + registry->TraceStack("A None armor can't conflict with anything in an outfit.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + return false; + } + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + return outfit.conflictsWith(armor); + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + return false; + } + } + void CreateOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("CreateOutfit"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + service.addOutfit(name.data()); + } catch (ArmorAddonOverrideService::bad_name) { + registry->TraceStack("Invalid outfit name specified.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return; + } + } + void DeleteOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("DeleteOutfit"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.deleteOutfit(name.data()); + } + std::vector GetOutfitContents(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - bool result = false; - try { - auto& outfit = service.getOutfit(name.data()); - result = outfit.isFavorite; - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - return result; - } - bool GetOutfitPassthroughStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("GetOutfitContents"sv); + std::vector result; + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + auto& armors = outfit.armors; + for (auto it = armors.begin(); it != armors.end(); ++it) + result.push_back(*it); + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + std::vector converted_result; + converted_result.reserve(result.size()); + for (const auto ptr : result) { + converted_result.push_back((RE::TESObjectARMO*) ptr); + } + return converted_result; + } + bool GetOutfitFavoriteStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - bool result = false; - try { - auto& outfit = service.getOutfit(name.data()); - result = outfit.allowsPassthrough; - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - return result; - } - bool GetOutfitEquipRequiredStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("GetOutfitFavoriteStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + bool result = false; + try { + auto& outfit = service.getOutfit(name.data()); + result = outfit.isFavorite; + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + return result; + } + bool GetOutfitPassthroughStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - bool result = false; - try { - auto& outfit = service.getOutfit(name.data()); - result = outfit.requiresEquipped; - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - return result; - } - RE::BSFixedString GetSelectedOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - auto& service = ArmorAddonOverrideService::GetInstance(); - return service.currentOutfit(RE::PlayerCharacter::GetSingleton()).name.c_str(); - } - bool IsEnabled(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { - auto& service = ArmorAddonOverrideService::GetInstance(); - return service.enabled; - } - std::vector ListOutfits(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("GetOutfitPassthroughStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + bool result = false; + try { + auto& outfit = service.getOutfit(name.data()); + result = outfit.allowsPassthrough; + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + return result; + } + bool GetOutfitEquipRequiredStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - bool favoritesOnly) { - auto& service = ArmorAddonOverrideService::GetInstance(); - std::vector result; - std::vector intermediate; - service.getOutfitNames(intermediate, favoritesOnly); - result.reserve(intermediate.size()); - for (auto it = intermediate.begin(); it != intermediate.end(); ++it) - result.push_back(it->c_str()); - return result; - } - void RemoveArmorFromOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::BSFixedString name) { + LogExit exitPrint("GetOutfitEquipRequiredStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + bool result = false; + try { + auto& outfit = service.getOutfit(name.data()); + result = outfit.requiresEquipped; + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + return result; + } + RE::BSFixedString GetSelectedOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* actor) { + LogExit exitPrint("GetSelectedOutfit"sv); + if (!actor) + return RE::BSFixedString(""); + auto& service = ArmorAddonOverrideService::GetInstance(); + return service.currentOutfit(actor->GetHandle().native_handle()).name.c_str(); + } + bool IsEnabled(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { + LogExit exitPrint("IsEnabled"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + return service.enabled; + } + std::vector ListOutfits(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name, - RE::TESObjectARMO* armor_skse) { - auto armor = (RE::TESObjectARMO*)(armor_skse); - ERROR_AND_RETURN_IF(armor == nullptr, "Cannot remove a None armor from an outfit.", registry, stackId); - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - outfit.armors.erase(armor); - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); - } - } - void RemoveConflictingArmorsFrom(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + bool favoritesOnly) { + LogExit exitPrint("ListOutfits"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + std::vector result; + std::vector intermediate; + service.getOutfitNames(intermediate, favoritesOnly); + result.reserve(intermediate.size()); + for (auto it = intermediate.begin(); it != intermediate.end(); ++it) + result.push_back(it->c_str()); + return result; + } + void RemoveArmorFromOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - RE::TESObjectARMO* armor_skse, - RE::BSFixedString name) { - auto armor = (RE::TESObjectARMO*)(armor_skse); - ERROR_AND_RETURN_IF(armor == nullptr, - "A None armor can't conflict with anything in an outfit.", - registry, - stackId); - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOutfit(name.data()); - auto& armors = outfit.armors; - std::vector conflicts; - const auto candidateMask = armor->GetSlotMask(); - for (auto it = armors.begin(); it != armors.end(); ++it) { - RE::TESObjectARMO* existing = *it; - if (existing) { - const auto mask = existing->GetSlotMask(); - if ((static_cast(mask) & static_cast(candidateMask)) - != static_cast(RE::BGSBipedObjectForm::FirstPersonFlag::kNone)) - conflicts.push_back(existing); - } - } - for (auto it = conflicts.begin(); it != conflicts.end(); ++it) - armors.erase(*it); - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return; - } - } - bool RenameOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::BSFixedString name, + RE::TESObjectARMO* armor_skse) { + LogExit exitPrint("RemoveArmorFromOutfit"sv); + auto armor = (RE::TESObjectARMO*) (armor_skse); + ERROR_AND_RETURN_IF(armor == nullptr, "Cannot remove a None armor from an outfit.", registry, stackId); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + outfit.armors.erase(armor); + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kWarning); + } + } + void RemoveConflictingArmorsFrom(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name, - RE::BSFixedString changeTo) { - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - service.renameOutfit(name.data(), changeTo.data()); - } catch (ArmorAddonOverrideService::bad_name) { - registry->TraceStack("The desired name is invalid.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return false; - } catch (ArmorAddonOverrideService::name_conflict) { - registry->TraceStack("The desired name is taken.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return false; - } catch (std::out_of_range) { - registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return false; - } - return true; - } - void SetOutfitFavoriteStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::TESObjectARMO* armor_skse, + RE::BSFixedString name) { + LogExit exitPrint("RemoveConflictingArmorsFrom"sv); + auto armor = (RE::TESObjectARMO*) (armor_skse); + ERROR_AND_RETURN_IF(armor == nullptr, + "A None armor can't conflict with anything in an outfit.", + registry, + stackId); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOutfit(name.data()); + auto& armors = outfit.armors; + std::vector conflicts; + const auto candidateMask = armor->GetSlotMask(); + for (auto it = armors.begin(); it != armors.end(); ++it) { + RE::TESObjectARMO* existing = *it; + if (existing) { + const auto mask = existing->GetSlotMask(); + if ((static_cast(mask) & static_cast(candidateMask)) + != static_cast(RE::BGSBipedObjectForm::FirstPersonFlag::kNone)) + conflicts.push_back(existing); + } + } + for (auto it = conflicts.begin(); it != conflicts.end(); ++it) + armors.erase(*it); + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return; + } + } + bool RenameOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - RE::BSFixedString name, - bool favorite) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.setFavorite(name.data(), favorite); - } - void SetOutfitPassthroughStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name, + RE::BSFixedString changeTo) { + LogExit exitPrint("RenameOutfit"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + service.renameOutfit(name.data(), changeTo.data()); + } catch (ArmorAddonOverrideService::bad_name) { + registry->TraceStack("The desired name is invalid.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return false; + } catch (ArmorAddonOverrideService::name_conflict) { + registry->TraceStack("The desired name is taken.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return false; + } catch (std::out_of_range) { + registry->TraceStack("The specified outfit does not exist.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return false; + } + return true; + } + void SetOutfitFavoriteStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name, - bool allowsPassthrough) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.setOutfitPassthrough(name.data(), allowsPassthrough); - } - void SetOutfitEquipRequiredStatus(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name, + bool favorite) { + LogExit exitPrint("SetOutfitFavoriteStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.setFavorite(name.data(), favorite); + } + void SetOutfitPassthroughStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name, - bool equipRequired) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.setOutfitEquipRequired(name.data(), equipRequired); - } - bool OutfitExists(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::BSFixedString name, + bool allowsPassthrough) { + LogExit exitPrint("SetOutfitPassthroughStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.setOutfitPassthrough(name.data(), allowsPassthrough); + } + void SetOutfitEquipRequiredStatus(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - return service.hasOutfit(name.data()); - } - void OverwriteOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString name, - std::vector armors - ) { - auto& service = ArmorAddonOverrideService::GetInstance(); - try { - auto& outfit = service.getOrCreateOutfit(name.data()); - outfit.armors.clear(); - auto count = armors.size(); - for (std::uint32_t i = 0; i < count; i++) { - RE::TESObjectARMO* ptr = nullptr; - ptr = armors.at(i); - if (ptr) - outfit.armors.insert(ptr); - } - } catch (ArmorAddonOverrideService::bad_name) { - registry->TraceStack("Invalid outfit name specified.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); - return; - } - } - void SetEnabled(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - bool state) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.setEnabled(state); - } - void SetSelectedOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::BSFixedString name) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.setOutfit(name.data(), RE::PlayerCharacter::GetSingleton()); - } - void AddActor(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::Actor* target) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.addActor((RE::Actor*)target); - } - void RemoveActor(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - RE::Actor* target) { - auto& service = ArmorAddonOverrideService::GetInstance(); - service.removeActor((RE::Actor*)target); - } - void SetLocationBasedAutoSwitchEnabled(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + RE::BSFixedString name, + bool equipRequired) { + LogExit exitPrint("SetOutfitEquipRequiredStatus"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.setOutfitEquipRequired(name.data(), equipRequired); + } + bool OutfitExists(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - bool value) { - ArmorAddonOverrideService::GetInstance().setLocationBasedAutoSwitchEnabled(value); - } - bool GetLocationBasedAutoSwitchEnabled(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - return ArmorAddonOverrideService::GetInstance().locationBasedAutoSwitchEnabled; - } - std::vector GetAutoSwitchLocationArray(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*) { - std::vector result; - for (LocationType i : { - LocationType::World, - LocationType::WorldSnowy, - LocationType::WorldRainy, - LocationType::City, - LocationType::CitySnowy, - LocationType::CityRainy, - LocationType::Town, - LocationType::TownSnowy, - LocationType::TownRainy, - LocationType::Dungeon, - LocationType::DungeonSnowy, - LocationType::DungeonRainy - }) { - result.push_back(std::uint32_t(i)); - } - return result; - } - std::optional identifyLocation(RE::BGSLocation* location, RE::TESWeather* weather) { - // Just a helper function to classify a location. - // TODO: Think of a better place than this since we're not exposing it to Papyrus. - auto& service = ArmorAddonOverrideService::GetInstance(); + RE::BSFixedString name) { + LogExit exitPrint("OutfitExists"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + return service.hasOutfit(name.data()); + } + void OverwriteOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::BSFixedString name, + std::vector armors) { + LogExit exitPrint("OverwriteOutfit"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + try { + auto& outfit = service.getOrCreateOutfit(name.data()); + outfit.armors.clear(); + auto count = armors.size(); + for (std::uint32_t i = 0; i < count; i++) { + RE::TESObjectARMO* ptr = nullptr; + ptr = armors.at(i); + if (ptr) + outfit.armors.insert(ptr); + } + } catch (ArmorAddonOverrideService::bad_name) { + registry->TraceStack("Invalid outfit name specified.", stackId, RE::BSScript::IVirtualMachine::Severity::kError); + return; + } + } + void SetEnabled(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + bool state) { + LogExit exitPrint("SetEnabled"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.setEnabled(state); + } + void SetSelectedOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* actor, + RE::BSFixedString name) { + LogExit exitPrint("SetSelectedOutfit"sv); + if (!actor) + return; + auto& service = ArmorAddonOverrideService::GetInstance(); + service.setOutfit(name.data(), actor->GetHandle().native_handle()); + } + void AddActor(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* target) { + LogExit exitPrint("AddActor"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.addActor(target->GetHandle().native_handle()); + } + void RemoveActor(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* target) { + LogExit exitPrint("RemoveActor"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + service.removeActor(target->GetHandle().native_handle()); + } + std::vector ListActors(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("ListActors"sv); + auto& service = ArmorAddonOverrideService::GetInstance(); + auto actors = service.listActors(); + std::vector actorVec; + for (auto& actorRef : actors) { + auto actor = RE::Actor::LookupByHandle(actorRef); + if (!actor) continue; +#if _DEBUG + LOG(debug, "INNER: Actor {} has refcount {}", actor->GetDisplayFullName(), actor->QRefCount()); +#endif + if (actor->QRefCount() == 1) { + LOG(warn, "ListActors will return an actor {} with refcount of 1. This may crash.", actor->GetDisplayFullName()); + } + actorVec.push_back(actor.get()); + } + std::sort( + actorVec.begin(), + actorVec.end(), + [](const RE::Actor* x, const RE::Actor* y) { + return x < y; + }); +#if _DEBUG + for (const auto& actor : actorVec) { + LOG(debug, "Actor {} has refcount {}", actor->GetDisplayFullName(), actor->QRefCount()); + } +#endif + return actorVec; + } + void SetLocationBasedAutoSwitchEnabled(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - // Collect weather information. - WeatherFlags weather_flags; - if (weather) { - weather_flags.snowy = weather->data.flags.any(RE::TESWeather::WeatherDataFlag::kSnow); - weather_flags.rainy = weather->data.flags.any(RE::TESWeather::WeatherDataFlag::kRainy); - } + bool value) { + LogExit exitPrint("SetLocationBasedAutoSwitchEnabled"sv); + ArmorAddonOverrideService::GetInstance().setLocationBasedAutoSwitchEnabled(value); + } + bool GetLocationBasedAutoSwitchEnabled(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("GetLocationBasedAutoSwitchEnabled"sv); + return ArmorAddonOverrideService::GetInstance().locationBasedAutoSwitchEnabled; + } + std::vector GetAutoSwitchLocationArray(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*) { + LogExit exitPrint("GetAutoSwitchLocationArray"sv); + std::vector result; + for (LocationType i : { + LocationType::World, + LocationType::WorldSnowy, + LocationType::WorldRainy, + LocationType::City, + LocationType::CitySnowy, + LocationType::CityRainy, + LocationType::Town, + LocationType::TownSnowy, + LocationType::TownRainy, + LocationType::Dungeon, + LocationType::DungeonSnowy, + LocationType::DungeonRainy}) { + result.push_back(std::uint32_t(i)); + } + return result; + } + std::optional identifyLocation(RE::BGSLocation* location, RE::TESWeather* weather) { + LogExit exitPrint("identifyLocation"sv); + // Just a helper function to classify a location. + // TODO: Think of a better place than this since we're not exposing it to Papyrus. + auto& service = ArmorAddonOverrideService::GetInstance(); - // Collect location keywords - std::unordered_set keywords; - keywords.reserve(20); - while (location) { - std::uint32_t max = location->GetNumKeywords(); - for (std::uint32_t i = 0; i < max; i++) { - RE::BGSKeyword* keyword = location->GetKeywordAt(i).value(); - /* - char message[100]; - LOG(info, "SOS: Location has Keyword %s", keyword->GetFormEditorID()); - sprintf(message, "SOS: Location has keyword %s", keyword->GetFormEditorID()); - RE::DebugNotification(message, nullptr, false); - */ - keywords.emplace(keyword->GetFormEditorID()); - } - location = location->parentLoc; - } - return service.checkLocationType(keywords, weather_flags, RE::PlayerCharacter::GetSingleton()); - } - std::uint32_t IdentifyLocationType(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, + // Collect weather information. + WeatherFlags weather_flags; + if (weather) { + weather_flags.snowy = weather->data.flags.any(RE::TESWeather::WeatherDataFlag::kSnow); + weather_flags.rainy = weather->data.flags.any(RE::TESWeather::WeatherDataFlag::kRainy); + } - RE::BGSLocation* location_skse, - RE::TESWeather* weather_skse) { - // NOTE: Identify the location for Papyrus. In the event no location is identified, we lie to Papyrus and say "World". - // Therefore, Papyrus cannot assume that locations returned have an outfit assigned, at least not for "World". - return static_cast(identifyLocation((RE::BGSLocation*)location_skse, - (RE::TESWeather*)weather_skse) - .value_or(LocationType::World)); - } - void SetOutfitUsingLocation(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + // Collect location keywords + std::unordered_set keywords; + keywords.reserve(20); + while (location) { + std::uint32_t max = location->GetNumKeywords(); + for (std::uint32_t i = 0; i < max; i++) { + RE::BGSKeyword* keyword = location->GetKeywordAt(i).value(); + /* + char message[100]; + LOG(info, "SOS: Location has Keyword %s", keyword->GetFormEditorID()); + sprintf(message, "SOS: Location has keyword %s", keyword->GetFormEditorID()); + RE::DebugNotification(message, nullptr, false); + */ + keywords.emplace(keyword->GetFormEditorID()); + } + location = location->parentLoc; + } + return service.checkLocationType(keywords, weather_flags, RE::PlayerCharacter::GetSingleton()->CreateRefHandle().native_handle()); + } + std::uint32_t IdentifyLocationType(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, - RE::BGSLocation* location_skse, - RE::TESWeather* weather_skse) { - // NOTE: Location can be NULL. - auto& service = ArmorAddonOverrideService::GetInstance(); - - if (service.locationBasedAutoSwitchEnabled) { - auto location = identifyLocation((RE::BGSLocation*)location_skse, (RE::TESWeather*)weather_skse); - // Debug notifications for location classification. - /* - const char* locationName = locationTypeStrings[static_cast(location)]; - char message[100]; - sprintf_s(message, "SOS: This location is a %s.", locationName); - RE::DebugNotification(message, nullptr, false); - */ - if (location.has_value()) { - service.setOutfitUsingLocation(location.value(), RE::PlayerCharacter::GetSingleton()); - } - } - - } - void SetLocationOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - - std::uint32_t location, - RE::BSFixedString name) { - if (strcmp(name.data(), "") == 0) { - // Location outfit assignment is never allowed to be empty string. Use unset instead. - return; - } - return ArmorAddonOverrideService::GetInstance() - .setLocationOutfit(LocationType(location), name.data(), RE::PlayerCharacter::GetSingleton()); - } - void UnsetLocationOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, - - std::uint32_t location) { - return ArmorAddonOverrideService::GetInstance() - .unsetLocationOutfit(LocationType(location), RE::PlayerCharacter::GetSingleton()); - } - RE::BSFixedString GetLocationOutfit(RE::BSScript::IVirtualMachine* registry, - std::uint32_t stackId, - RE::StaticFunctionTag*, - - std::uint32_t location) { - auto outfit = ArmorAddonOverrideService::GetInstance() - .getLocationOutfit(LocationType(location), RE::PlayerCharacter::GetSingleton()); - if (outfit.has_value()) { - return RE::BSFixedString(outfit.value().c_str()); - } else { - // Empty string means "no outfit assigned" for this location type. - return RE::BSFixedString(""); - } - } - bool ExportSettings(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { - std::string outputFile = GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\OutfitSystemData.json"; - auto& service = ArmorAddonOverrideService::GetInstance(); - proto::OutfitSystem data = service.save(); - std::string output; - google::protobuf::util::JsonPrintOptions options; - options.add_whitespace = true; - google::protobuf::util::MessageToJsonString(data, &output, options); - std::ofstream file(outputFile); - if (file) { - file << output; - } else { - RE::DebugNotification("Failed to open config for writing", nullptr, false); - return false; - } - if (file.good()) { - std::string message = "Wrote JSON config to " + outputFile; - RE::DebugNotification(message.c_str(), nullptr, false); - return true; - } else { - RE::DebugNotification("Failed to write config", nullptr, false); - return false; - } - } - bool ImportSettings(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { - std::string inputFile = GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\OutfitSystemData.json"; - std::ifstream file(inputFile); - if (!file) { - RE::DebugNotification("Failed to open config for reading", nullptr, false); - return false; - } - std::stringstream input; - input << file.rdbuf(); - if (!file.good()) { - RE::DebugNotification("Failed to read config data", nullptr, false); - return false; - } - proto::OutfitSystem data; - auto status = google::protobuf::util::JsonStringToMessage(input.str(), &data); - if (!status.ok()) { - RE::DebugNotification("Failed to parse config data. Invalid syntax.", nullptr, false); - return false; - } - auto& service = ArmorAddonOverrideService::GetInstance(); - service.load(SKSE::GetSerializationInterface(), data); - std::string message = "Read JSON config from " + inputFile; - RE::DebugNotification(message.c_str(), nullptr, false); - return true; - } -} + RE::BGSLocation* location_skse, + RE::TESWeather* weather_skse) { + LogExit exitPrint("IdentifyLocationType"sv); + // NOTE: Identify the location for Papyrus. In the event no location is identified, we lie to Papyrus and say "World". + // Therefore, Papyrus cannot assume that locations returned have an outfit assigned, at least not for "World". + return static_cast(identifyLocation((RE::BGSLocation*) location_skse, + (RE::TESWeather*) weather_skse) + .value_or(LocationType::World)); + } + void SetOutfitUsingLocation(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::Actor* actor, + RE::BGSLocation* location_skse, + RE::TESWeather* weather_skse) { + LogExit exitPrint("SetOutfitUsingLocation"sv); + // NOTE: Location can be NULL. + auto& service = ArmorAddonOverrideService::GetInstance(); + if (!actor) + return; + if (service.locationBasedAutoSwitchEnabled) { + auto location = identifyLocation(location_skse, weather_skse); + // Debug notifications for location classification. + /* + const char* locationName = locationTypeStrings[static_cast(location)]; + char message[100]; + sprintf_s(message, "SOS: This location is a %s.", locationName); + RE::DebugNotification(message, nullptr, false); + */ + if (location.has_value()) { + service.setOutfitUsingLocation(location.value(), actor->GetHandle().native_handle()); + } + } + } + void SetLocationOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::Actor* actor, + std::uint32_t location, + RE::BSFixedString name) { + LogExit exitPrint("SetLocationOutfit"sv); + if (!actor) + return; + if (strcmp(name.data(), "") == 0) { + // Location outfit assignment is never allowed to be empty string. Use unset instead. + return; + } + return ArmorAddonOverrideService::GetInstance() + .setLocationOutfit(LocationType(location), name.data(), actor->GetHandle().native_handle()); + } + void UnsetLocationOutfit(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*, + RE::Actor* actor, + std::uint32_t location) { + LogExit exitPrint("UnsetLocationOutfit"sv); + if (!actor) + return; + return ArmorAddonOverrideService::GetInstance() + .unsetLocationOutfit(LocationType(location), actor->GetHandle().native_handle()); + } + RE::BSFixedString GetLocationOutfit(RE::BSScript::IVirtualMachine* registry, + std::uint32_t stackId, + RE::StaticFunctionTag*, + RE::Actor* actor, + std::uint32_t location) { + LogExit exitPrint("GetLocationOutfit"sv); + if (!actor) + return RE::BSFixedString(""); + auto outfit = ArmorAddonOverrideService::GetInstance() + .getLocationOutfit(LocationType(location), actor->GetHandle().native_handle()); + if (outfit.has_value()) { + return RE::BSFixedString(outfit.value().c_str()); + } else { + // Empty string means "no outfit assigned" for this location type. + return RE::BSFixedString(""); + } + } + bool ExportSettings(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { + LogExit exitPrint("ExportSettings"sv); + std::string outputFile = GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\OutfitSystemData.json"; + auto& service = ArmorAddonOverrideService::GetInstance(); + proto::OutfitSystem data = service.save(); + std::string output; + google::protobuf::util::JsonPrintOptions options; + options.add_whitespace = true; + google::protobuf::util::MessageToJsonString(data, &output, options); + std::ofstream file(outputFile); + if (file) { + file << output; + } else { + RE::DebugNotification("Failed to open config for writing", nullptr, false); + return false; + } + if (file.good()) { + std::string message = "Wrote JSON config to " + outputFile; + RE::DebugNotification(message.c_str(), nullptr, false); + return true; + } else { + RE::DebugNotification("Failed to write config", nullptr, false); + return false; + } + } + bool ImportSettings(RE::BSScript::IVirtualMachine* registry, std::uint32_t stackId, RE::StaticFunctionTag*) { + LogExit exitPrint("ImportSettings"sv); + std::string inputFile = GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\OutfitSystemData.json"; + std::ifstream file(inputFile); + if (!file) { + RE::DebugNotification("Failed to open config for reading", nullptr, false); + return false; + } + std::stringstream input; + input << file.rdbuf(); + if (!file.good()) { + RE::DebugNotification("Failed to read config data", nullptr, false); + return false; + } + proto::OutfitSystem data; + auto status = google::protobuf::util::JsonStringToMessage(input.str(), &data); + if (!status.ok()) { + RE::DebugNotification("Failed to parse config data. Invalid syntax.", nullptr, false); + return false; + } + auto& service = ArmorAddonOverrideService::GetInstance(); + service.load(SKSE::GetSerializationInterface(), data); + std::string message = "Read JSON config from " + inputFile; + RE::DebugNotification(message.c_str(), nullptr, false); + return true; + } +}// namespace OutfitSystem bool OutfitSystem::RegisterPapyrus(RE::BSScript::IVirtualMachine* registry) { - registry->RegisterFunction("GetOutfitNameMaxLength", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitNameMaxLength); - registry->RegisterFunction( - "GetOutfitNameMaxLength", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitNameMaxLength, - true - ); - registry->RegisterFunction( - "GetCarriedArmor", - "SkyrimOutfitSystemNativeFuncs", - GetCarriedArmor - ); - registry->RegisterFunction( - "GetWornItems", - "SkyrimOutfitSystemNativeFuncs", - GetWornItems - ); - registry->RegisterFunction( - "RefreshArmorFor", - "SkyrimOutfitSystemNativeFuncs", - RefreshArmorFor - ); - registry->RegisterFunction( - "ActorNearPC", - "SkyrimOutfitSystemNativeFuncs", - ActorsNearPC - ); - // - { // armor form search utils - registry->RegisterFunction( - "PrepArmorSearch", - "SkyrimOutfitSystemNativeFuncs", - ArmorFormSearchUtils::Prep - ); - registry->RegisterFunction( - "GetArmorSearchResultForms", - "SkyrimOutfitSystemNativeFuncs", - ArmorFormSearchUtils::GetForms - ); - registry->RegisterFunction( - "GetArmorSearchResultNames", - "SkyrimOutfitSystemNativeFuncs", - ArmorFormSearchUtils::GetNames - ); - registry->RegisterFunction( - "ClearArmorSearch", - "SkyrimOutfitSystemNativeFuncs", - ArmorFormSearchUtils::Clear - ); - } - { // body slot data - registry->RegisterFunction( - "PrepOutfitBodySlotListing", - "SkyrimOutfitSystemNativeFuncs", - BodySlotListing::Prep - ); - registry->RegisterFunction( - "GetOutfitBodySlotListingArmorForms", - "SkyrimOutfitSystemNativeFuncs", - BodySlotListing::GetArmorForms - ); - registry->RegisterFunction( - "GetOutfitBodySlotListingArmorNames", - "SkyrimOutfitSystemNativeFuncs", - BodySlotListing::GetArmorNames - ); - registry->RegisterFunction( - "GetOutfitBodySlotListingSlotIndices", - "SkyrimOutfitSystemNativeFuncs", - BodySlotListing::GetSlotIndices - ); - registry->RegisterFunction( - "ClearOutfitBodySlotListing", - "SkyrimOutfitSystemNativeFuncs", - BodySlotListing::Clear - ); - } - { // string sorts - registry->RegisterFunction( - "NaturalSort_ASCII", - "SkyrimOutfitSystemNativeFuncs", - StringSorts::NaturalSort_ASCII, - true - ); - registry->RegisterFunction( - "NaturalSortPairArmor_ASCII", - "SkyrimOutfitSystemNativeFuncs", - StringSorts::NaturalSortPair_ASCII, - true - ); - } - { // Utility - registry->RegisterFunction( - "HexToInt32", - "SkyrimOutfitSystemNativeFuncs", - Utility::HexToInt32, - true - ); - registry->RegisterFunction( - "ToHex", - "SkyrimOutfitSystemNativeFuncs", - Utility::ToHex, - true - ); - } - // - registry->RegisterFunction( - "AddArmorToOutfit", - "SkyrimOutfitSystemNativeFuncs", - AddArmorToOutfit - ); - registry->RegisterFunction( - "ArmorConflictsWithOutfit", - "SkyrimOutfitSystemNativeFuncs", - ArmorConflictsWithOutfit - ); - registry->RegisterFunction( - "CreateOutfit", - "SkyrimOutfitSystemNativeFuncs", - CreateOutfit - ); - registry->RegisterFunction( - "DeleteOutfit", - "SkyrimOutfitSystemNativeFuncs", - DeleteOutfit - ); - registry->RegisterFunction( - "GetOutfitContents", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitContents - ); - registry->RegisterFunction( - "GetOutfitFavoriteStatus", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitFavoriteStatus - ); - registry->RegisterFunction( - "GetOutfitPassthroughStatus", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitPassthroughStatus - ); - registry->RegisterFunction( - "GetOutfitEquipRequiredStatus", - "SkyrimOutfitSystemNativeFuncs", - GetOutfitEquipRequiredStatus - ); - registry->RegisterFunction( - "SetOutfitFavoriteStatus", - "SkyrimOutfitSystemNativeFuncs", - SetOutfitFavoriteStatus - ); - registry->RegisterFunction( - "SetOutfitPassthroughStatus", - "SkyrimOutfitSystemNativeFuncs", - SetOutfitPassthroughStatus - ); - registry->RegisterFunction( - "SetOutfitEquipRequiredStatus", - "SkyrimOutfitSystemNativeFuncs", - SetOutfitEquipRequiredStatus - ); - registry->RegisterFunction( - "IsEnabled", - "SkyrimOutfitSystemNativeFuncs", - IsEnabled - ); - registry->RegisterFunction( - "GetSelectedOutfit", - "SkyrimOutfitSystemNativeFuncs", - GetSelectedOutfit - ); - registry->RegisterFunction( - "ListOutfits", - "SkyrimOutfitSystemNativeFuncs", - ListOutfits - ); - registry->RegisterFunction( - "RemoveArmorFromOutfit", - "SkyrimOutfitSystemNativeFuncs", - RemoveArmorFromOutfit - ); - registry->RegisterFunction( - "RemoveConflictingArmorsFrom", - "SkyrimOutfitSystemNativeFuncs", - RemoveConflictingArmorsFrom - ); - registry->RegisterFunction( - "RenameOutfit", - "SkyrimOutfitSystemNativeFuncs", - RenameOutfit - ); - registry->RegisterFunction( - "OutfitExists", - "SkyrimOutfitSystemNativeFuncs", - OutfitExists - ); - registry->RegisterFunction( - "OverwriteOutfit", - "SkyrimOutfitSystemNativeFuncs", - OverwriteOutfit - ); - registry->RegisterFunction( - "SetEnabled", - "SkyrimOutfitSystemNativeFuncs", - SetEnabled - ); - registry->RegisterFunction( - "SetSelectedOutfit", - "SkyrimOutfitSystemNativeFuncs", - SetSelectedOutfit - ); - registry->RegisterFunction( - "AddActor", - "SkyrimOutfitSystemNativeFuncs", - AddActor - ); - registry->RegisterFunction( - "RemoveActor", - "SkyrimOutfitSystemNativeFuncs", - RemoveActor - ); - registry->RegisterFunction( - "SetLocationBasedAutoSwitchEnabled", - "SkyrimOutfitSystemNativeFuncs", - SetLocationBasedAutoSwitchEnabled - ); - registry->RegisterFunction( - "GetLocationBasedAutoSwitchEnabled", - "SkyrimOutfitSystemNativeFuncs", - GetLocationBasedAutoSwitchEnabled - ); - registry->RegisterFunction( - "GetAutoSwitchLocationArray", - "SkyrimOutfitSystemNativeFuncs", - GetAutoSwitchLocationArray - ); - registry->RegisterFunction( - "IdentifyLocationType", - "SkyrimOutfitSystemNativeFuncs", - IdentifyLocationType - ); - registry->RegisterFunction( - "SetOutfitUsingLocation", - "SkyrimOutfitSystemNativeFuncs", - SetOutfitUsingLocation - ); - registry->RegisterFunction( - "SetLocationOutfit", - "SkyrimOutfitSystemNativeFuncs", - SetLocationOutfit - ); - registry->RegisterFunction( - "UnsetLocationOutfit", - "SkyrimOutfitSystemNativeFuncs", - UnsetLocationOutfit - ); - registry->RegisterFunction( - "GetLocationOutfit", - "SkyrimOutfitSystemNativeFuncs", - GetLocationOutfit - ); - registry->RegisterFunction( - "ExportSettings", - "SkyrimOutfitSystemNativeFuncs", - ExportSettings - ); - registry->RegisterFunction( - "ImportSettings", - "SkyrimOutfitSystemNativeFuncs", - ImportSettings - ); + registry->RegisterFunction("GetOutfitNameMaxLength", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitNameMaxLength); + registry->RegisterFunction( + "GetOutfitNameMaxLength", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitNameMaxLength, + true); + registry->RegisterFunction( + "GetCarriedArmor", + "SkyrimOutfitSystemNativeFuncs", + GetCarriedArmor); + registry->RegisterFunction( + "GetWornItems", + "SkyrimOutfitSystemNativeFuncs", + GetWornItems); + registry->RegisterFunction( + "RefreshArmorFor", + "SkyrimOutfitSystemNativeFuncs", + RefreshArmorFor); + registry->RegisterFunction( + "RefreshArmorForAllConfiguredActors", + "SkyrimOutfitSystemNativeFuncs", + RefreshArmorForAllConfiguredActors); + registry->RegisterFunction( + "ActorNearPC", + "SkyrimOutfitSystemNativeFuncs", + ActorsNearPC); + // + {// armor form search utils + registry->RegisterFunction( + "PrepArmorSearch", + "SkyrimOutfitSystemNativeFuncs", + ArmorFormSearchUtils::Prep); + registry->RegisterFunction( + "GetArmorSearchResultForms", + "SkyrimOutfitSystemNativeFuncs", + ArmorFormSearchUtils::GetForms); + registry->RegisterFunction( + "GetArmorSearchResultNames", + "SkyrimOutfitSystemNativeFuncs", + ArmorFormSearchUtils::GetNames); + registry->RegisterFunction( + "ClearArmorSearch", + "SkyrimOutfitSystemNativeFuncs", + ArmorFormSearchUtils::Clear); + } + {// body slot data + registry->RegisterFunction( + "PrepOutfitBodySlotListing", + "SkyrimOutfitSystemNativeFuncs", + BodySlotListing::Prep); + registry->RegisterFunction( + "GetOutfitBodySlotListingArmorForms", + "SkyrimOutfitSystemNativeFuncs", + BodySlotListing::GetArmorForms); + registry->RegisterFunction( + "GetOutfitBodySlotListingArmorNames", + "SkyrimOutfitSystemNativeFuncs", + BodySlotListing::GetArmorNames); + registry->RegisterFunction( + "GetOutfitBodySlotListingSlotIndices", + "SkyrimOutfitSystemNativeFuncs", + BodySlotListing::GetSlotIndices); + registry->RegisterFunction( + "ClearOutfitBodySlotListing", + "SkyrimOutfitSystemNativeFuncs", + BodySlotListing::Clear); + } + {// string sorts + registry->RegisterFunction( + "NaturalSort_ASCII", + "SkyrimOutfitSystemNativeFuncs", + StringSorts::NaturalSort_ASCII, + true); + registry->RegisterFunction( + "NaturalSortPairArmor_ASCII", + "SkyrimOutfitSystemNativeFuncs", + StringSorts::NaturalSortPair_ASCII, + true); + } + {// Utility + registry->RegisterFunction( + "HexToInt32", + "SkyrimOutfitSystemNativeFuncs", + Utility::HexToInt32, + true); + registry->RegisterFunction( + "ToHex", + "SkyrimOutfitSystemNativeFuncs", + Utility::ToHex, + true); + } + // + registry->RegisterFunction( + "AddArmorToOutfit", + "SkyrimOutfitSystemNativeFuncs", + AddArmorToOutfit); + registry->RegisterFunction( + "ArmorConflictsWithOutfit", + "SkyrimOutfitSystemNativeFuncs", + ArmorConflictsWithOutfit); + registry->RegisterFunction( + "CreateOutfit", + "SkyrimOutfitSystemNativeFuncs", + CreateOutfit); + registry->RegisterFunction( + "DeleteOutfit", + "SkyrimOutfitSystemNativeFuncs", + DeleteOutfit); + registry->RegisterFunction( + "GetOutfitContents", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitContents); + registry->RegisterFunction( + "GetOutfitFavoriteStatus", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitFavoriteStatus); + registry->RegisterFunction( + "GetOutfitPassthroughStatus", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitPassthroughStatus); + registry->RegisterFunction( + "GetOutfitEquipRequiredStatus", + "SkyrimOutfitSystemNativeFuncs", + GetOutfitEquipRequiredStatus); + registry->RegisterFunction( + "SetOutfitFavoriteStatus", + "SkyrimOutfitSystemNativeFuncs", + SetOutfitFavoriteStatus); + registry->RegisterFunction( + "SetOutfitPassthroughStatus", + "SkyrimOutfitSystemNativeFuncs", + SetOutfitPassthroughStatus); + registry->RegisterFunction( + "SetOutfitEquipRequiredStatus", + "SkyrimOutfitSystemNativeFuncs", + SetOutfitEquipRequiredStatus); + registry->RegisterFunction( + "IsEnabled", + "SkyrimOutfitSystemNativeFuncs", + IsEnabled); + registry->RegisterFunction( + "GetSelectedOutfit", + "SkyrimOutfitSystemNativeFuncs", + GetSelectedOutfit); + registry->RegisterFunction( + "ListOutfits", + "SkyrimOutfitSystemNativeFuncs", + ListOutfits); + registry->RegisterFunction( + "RemoveArmorFromOutfit", + "SkyrimOutfitSystemNativeFuncs", + RemoveArmorFromOutfit); + registry->RegisterFunction( + "RemoveConflictingArmorsFrom", + "SkyrimOutfitSystemNativeFuncs", + RemoveConflictingArmorsFrom); + registry->RegisterFunction( + "RenameOutfit", + "SkyrimOutfitSystemNativeFuncs", + RenameOutfit); + registry->RegisterFunction( + "OutfitExists", + "SkyrimOutfitSystemNativeFuncs", + OutfitExists); + registry->RegisterFunction( + "OverwriteOutfit", + "SkyrimOutfitSystemNativeFuncs", + OverwriteOutfit); + registry->RegisterFunction( + "SetEnabled", + "SkyrimOutfitSystemNativeFuncs", + SetEnabled); + registry->RegisterFunction( + "SetSelectedOutfit", + "SkyrimOutfitSystemNativeFuncs", + SetSelectedOutfit); + registry->RegisterFunction( + "AddActor", + "SkyrimOutfitSystemNativeFuncs", + AddActor); + registry->RegisterFunction( + "RemoveActor", + "SkyrimOutfitSystemNativeFuncs", + RemoveActor); + registry->RegisterFunction( + "ListActors", + "SkyrimOutfitSystemNativeFuncs", + ListActors); + registry->RegisterFunction( + "SetLocationBasedAutoSwitchEnabled", + "SkyrimOutfitSystemNativeFuncs", + SetLocationBasedAutoSwitchEnabled); + registry->RegisterFunction( + "GetLocationBasedAutoSwitchEnabled", + "SkyrimOutfitSystemNativeFuncs", + GetLocationBasedAutoSwitchEnabled); + registry->RegisterFunction( + "GetAutoSwitchLocationArray", + "SkyrimOutfitSystemNativeFuncs", + GetAutoSwitchLocationArray); + registry->RegisterFunction( + "IdentifyLocationType", + "SkyrimOutfitSystemNativeFuncs", + IdentifyLocationType); + registry->RegisterFunction( + "SetOutfitUsingLocation", + "SkyrimOutfitSystemNativeFuncs", + SetOutfitUsingLocation); + registry->RegisterFunction( + "SetLocationOutfit", + "SkyrimOutfitSystemNativeFuncs", + SetLocationOutfit); + registry->RegisterFunction( + "UnsetLocationOutfit", + "SkyrimOutfitSystemNativeFuncs", + UnsetLocationOutfit); + registry->RegisterFunction( + "GetLocationOutfit", + "SkyrimOutfitSystemNativeFuncs", + GetLocationOutfit); + registry->RegisterFunction( + "ExportSettings", + "SkyrimOutfitSystemNativeFuncs", + ExportSettings); + registry->RegisterFunction( + "ImportSettings", + "SkyrimOutfitSystemNativeFuncs", + ImportSettings); - return true; + return true; } \ No newline at end of file diff --git a/src/RE/REAugments.cpp b/src/RE/REAugments.cpp new file mode 100644 index 0000000..be2285e --- /dev/null +++ b/src/RE/REAugments.cpp @@ -0,0 +1,52 @@ +// +// Created by m on 10/8/2022. +// + +#include "RE/REAugments.h" + +void RE::InventoryChangesAugments::ExecuteVisitor(RE::InventoryChanges* thisPtr, RE::InventoryChanges::IItemChangeVisitor * a_visitor) { + using func_t = decltype(&RE::InventoryChangesAugments::ExecuteVisitor); + REL::Relocation func{ RELOCATION_ID(15855, 16095) }; + return func(thisPtr, a_visitor); +} + +void RE::InventoryChangesAugments::ExecuteAugmentVisitor(RE::InventoryChanges* thisPtr, RE::IItemChangeVisitorAugment * a_visitor) { + using func_t = decltype(&RE::InventoryChangesAugments::ExecuteAugmentVisitor); + REL::Relocation func{ RELOCATION_ID(15855, 16095) }; + return func(thisPtr, a_visitor); +} + + +void RE::InventoryChangesAugments::ExecuteVisitorOnWorn(RE::InventoryChanges* thisPtr, RE::InventoryChanges::IItemChangeVisitor * a_visitor) { + using func_t = decltype(&RE::InventoryChangesAugments::ExecuteVisitorOnWorn); + REL::Relocation func{ RELOCATION_ID(15856, 16096) }; + return func(thisPtr, a_visitor); +} + +void RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(RE::InventoryChanges* thisPtr, RE::IItemChangeVisitorAugment * a_visitor) { + using func_t = decltype(&RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn); + REL::Relocation func{ RELOCATION_ID(15856, 16096) }; + return func(thisPtr, a_visitor); +} + +void RE::AIProcessAugments::SetEquipFlag(RE::AIProcess* thisPtr, RE::AIProcessAugments::Flag a_flag) { + using func_t = decltype(&RE::AIProcessAugments::SetEquipFlag); + REL::Relocation func{ RELOCATION_ID(38867, 39907) }; + return func(thisPtr, a_flag);} + +void RE::AIProcessAugments::UpdateEquipment(RE::AIProcess* thisPtr, RE::Actor* a_actor) { + using func_t = decltype(&RE::AIProcessAugments::UpdateEquipment); + REL::Relocation func{ RELOCATION_ID(38404, 39395) }; + return func(thisPtr, a_actor); +} + +bool RE::TESObjectARMOAugments::ApplyArmorAddon(RE::TESObjectARMO* thisPtr, RE::TESRace* a_race, RE::ActorWeightModel* a_model, bool a_isFemale) { + using func_t = decltype(&RE::TESObjectARMOAugments::ApplyArmorAddon); + REL::Relocation func{ RELOCATION_ID(17392, 17792) }; + return func(thisPtr, a_race, a_model, a_isFemale);} + +bool RE::TESObjectARMOAugments::TestBodyPartByIndex(RE::TESObjectARMO* thisPtr, std::uint32_t a_index) { + using func_t = decltype(&RE::TESObjectARMOAugments::TestBodyPartByIndex); + REL::Relocation func{ RELOCATION_ID(17395, 17795) }; + return func(thisPtr, a_index); +} diff --git a/src/Utility.cpp b/src/Utility.cpp index b040698..7d54516 100644 --- a/src/Utility.cpp +++ b/src/Utility.cpp @@ -6,44 +6,66 @@ #include "SKSE/SKSE.h" +#undef GetModuleFileName +#undef GetModuleHandle + std::string GetRuntimePath() { - static char appPath[4096] = { 0 }; + static char appPath[4096] = {0}; - if (appPath[0]) - return appPath; + if (appPath[0]) + return appPath; - if (!SKSE::WinAPI::GetModuleFileName(SKSE::WinAPI::GetModuleHandle((const char*) nullptr), appPath, sizeof(appPath))) { - SKSE::stl::report_and_fail("Failed to get runtime path"); - } + if (!SKSE::WinAPI::GetModuleFileName(SKSE::WinAPI::GetModuleHandle((const char*) nullptr), appPath, sizeof(appPath))) { + SKSE::stl::report_and_fail("Failed to get runtime path"); + } - return appPath; + return appPath; } std::string GetRuntimeName() { - std::string appPath = GetRuntimePath(); + std::string appPath = GetRuntimePath(); - std::string::size_type slashOffset = appPath.rfind('\\'); - if (slashOffset == std::string::npos) - return appPath; + std::string::size_type slashOffset = appPath.rfind('\\'); + if (slashOffset == std::string::npos) + return appPath; - return appPath.substr(slashOffset + 1); + return appPath.substr(slashOffset + 1); } const std::string& GetRuntimeDirectory() { - static std::string s_runtimeDirectory; + static std::string s_runtimeDirectory; - if (s_runtimeDirectory.empty()) { - std::string runtimePath = GetRuntimePath(); + if (s_runtimeDirectory.empty()) { + std::string runtimePath = GetRuntimePath(); - // truncate at last slash - std::string::size_type lastSlash = runtimePath.rfind('\\'); - if (lastSlash != std::string::npos) // if we don't find a slash something is VERY WRONG - { - s_runtimeDirectory = runtimePath.substr(0, lastSlash + 1); - } else { - LOG(critical, "no slash in runtime path? (%s)", runtimePath.c_str()); - } - } + // truncate at last slash + std::string::size_type lastSlash = runtimePath.rfind('\\'); + if (lastSlash != std::string::npos)// if we don't find a slash something is VERY WRONG + { + s_runtimeDirectory = runtimePath.substr(0, lastSlash + 1); + } else { + LOG(critical, "no slash in runtime path? (%s)", runtimePath.c_str()); + } + } - return s_runtimeDirectory; + return s_runtimeDirectory; +} + +Settings::Settings() : reader(GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\SkyrimOutfitSystemSE.ini") { + if (reader.ParseError() != 0) { + // Failed to load INI. We proceed without it. + LOG(info, "Could not load INI file from {}. Continuing without it.", GetRuntimeDirectory() + "Data\\SKSE\\Plugins\\SkyrimOutfitSystemSE.ini"); + return; + } else { + LOG(info, "INI file was successfully loaded."); + } +} + +Settings::~Settings() {} + +static Settings* settings; + +INIReader* Settings::Instance() { + if (!settings) settings = new Settings(); + return &settings->reader; } diff --git a/src/cobb/utf8naturalsort.cpp b/src/cobb/utf8naturalsort.cpp index e3c17cb..86d0e22 100644 --- a/src/cobb/utf8naturalsort.cpp +++ b/src/cobb/utf8naturalsort.cpp @@ -3,57 +3,57 @@ #include inline static bool isNumber(const char c) { - return c >= '0' && c <= '9'; + return c >= '0' && c <= '9'; } namespace cobb { - namespace utf8 { - int32_t naturalcompare(const std::string& a, const std::string& b) { - auto i_a = a.begin(); - auto i_b = b.begin(); - for (; i_a != a.end() && i_b != b.end(); ++i_a, ++i_b) { - unicodechar c_a = utf8::get(a, i_a); - unicodechar c_b = utf8::get(b, i_b); - if (!isNumber(c_a) || !isNumber(c_b)) { - // - // For non-numeric characters, just do a character code comparison. - // This won't internationalize -- it won't even handle accented - // characters in English. - // - c_a = std::towlower(c_a); // not sure about this; seems to use two-byte chars when unicode can be up to four bytes, no? - c_b = std::towlower(c_b); // then again, anything past the two-byte mark probably isn't even a "letter" as Westerners conceive of them... - if (c_a == c_b) - continue; - return c_b - c_a; - } - std::uint32_t n_a = c_a - '0'; - std::uint32_t n_b = c_b - '0'; - ++i_a; - ++i_b; - for (; i_a != a.end(); ++i_a) { - c_a = utf8::get(a, i_a); - if (!isNumber(c_a)) - break; - n_a = (n_a * 10) + (c_a - '0'); - } - for (; i_b != b.end(); ++i_b) { - c_b = utf8::get(b, i_b); - if (!isNumber(c_b)) - break; - n_b = (n_b * 10) + (c_b - '0'); - } - if (n_a != n_b) - return n_b - n_a; - } - // - // If we've reached this point, then we've reached the end of ONE OF the - // strings (and they don't end in digits). - // - if (i_a != a.end()) - return 1; - if (i_b != b.end()) - return -1; - return 0; - } - } -} \ No newline at end of file + namespace utf8 { + int32_t naturalcompare(const std::string& a, const std::string& b) { + auto i_a = a.begin(); + auto i_b = b.begin(); + for (; i_a != a.end() && i_b != b.end(); ++i_a, ++i_b) { + unicodechar c_a = utf8::get(a, i_a); + unicodechar c_b = utf8::get(b, i_b); + if (!isNumber(c_a) || !isNumber(c_b)) { + // + // For non-numeric characters, just do a character code comparison. + // This won't internationalize -- it won't even handle accented + // characters in English. + // + c_a = std::towlower(c_a);// not sure about this; seems to use two-byte chars when unicode can be up to four bytes, no? + c_b = std::towlower(c_b);// then again, anything past the two-byte mark probably isn't even a "letter" as Westerners conceive of them... + if (c_a == c_b) + continue; + return c_b - c_a; + } + std::uint32_t n_a = c_a - '0'; + std::uint32_t n_b = c_b - '0'; + ++i_a; + ++i_b; + for (; i_a != a.end(); ++i_a) { + c_a = utf8::get(a, i_a); + if (!isNumber(c_a)) + break; + n_a = (n_a * 10) + (c_a - '0'); + } + for (; i_b != b.end(); ++i_b) { + c_b = utf8::get(b, i_b); + if (!isNumber(c_b)) + break; + n_b = (n_b * 10) + (c_b - '0'); + } + if (n_a != n_b) + return n_b - n_a; + } + // + // If we've reached this point, then we've reached the end of ONE OF the + // strings (and they don't end in digits). + // + if (i_a != a.end()) + return 1; + if (i_b != b.end()) + return -1; + return 0; + } + }// namespace utf8 +}// namespace cobb \ No newline at end of file diff --git a/src/cobb/utf8string.cpp b/src/cobb/utf8string.cpp index 95ca435..dec22b7 100644 --- a/src/cobb/utf8string.cpp +++ b/src/cobb/utf8string.cpp @@ -1,194 +1,194 @@ #include "cobb/utf8string.h" namespace cobb { - namespace utf8 { - inline static bool _is_bytecount(char c) { - return c < 0xF8; - } - inline static bool _is_continuation(char c) { - return ((c & 0xC0) == 0x80); - } - // - void advance(const std::string& container, std::string::iterator& iterator) { - if (iterator == container.end()) - return; - unsigned char c; - // - c = iterator[0]; - ++iterator; - if (c < 0x80) // single-byte glyph - return; - if (!_is_bytecount(c)) // error - return; - // - c = iterator[0]; - ++iterator; - if (!_is_continuation(c)) // error - return; - // - c = iterator[0]; - ++iterator; - if (!_is_continuation(c)) // error - return; - // - c = iterator[0]; - ++iterator; - } - void append(std::string& target, const unicodechar c) { - if (c < 0x80) { - target.push_back(c); - return; - } - if (c < 0x800) { - target.push_back(0xE0 | ((c >> 6) & 0x1F)); - target.push_back(0x80 | (c & 0x3F)); - return; - } - if (c < 0x10000) { - target.push_back(0xE0 | ((c >> 12) & 0xF)); - target.push_back(0x80 | ((c >> 6) & 0x3F)); - target.push_back(0x80 | (c & 0x3F)); - return; - } - target.push_back(0xE0 | ((c >> 18) & 0x7)); - target.push_back(0x80 | ((c >> 12) & 0x3F)); - target.push_back(0x80 | ((c >> 6) & 0x3F)); - target.push_back(0x80 | (c & 0x1F)); - } - size_t count(std::string& container) { - return count_from(container, container.begin()); - } - size_t count_from(const std::string& container, const std::string::iterator& iterator) { - std::string::iterator working = iterator; - size_t count = 0; - while (working != container.end()) { - advance(container, working); - count++; - } - return count; - } - unicodechar get(const std::string& container, const std::string::const_iterator& iterator) { - if (iterator == container.end()) - return 0; - unsigned char a = iterator[0]; - if (a < 0x80) - return a; - if (!_is_bytecount(a)) - return invalid_glyph; - // - // Two bytes: - // - std::uint32_t result; - char b = iterator[1]; - if (!_is_continuation(b)) - return invalid_glyph; - if ((a & 0xE0) == 0xC0) { - result = (a & 0x1F) << 6; - result |= (b & 0x3F); - return result; - } - // - // Three bytes: - // - char c = iterator[2]; - if (!_is_continuation(c)) - return invalid_glyph; - if ((a & 0xF0) == 0xE0) { // three bytes - result = (a & 0xF) << 12; - result |= (b & 0x3F) << 6; - result |= (c & 0x3F); - if (result >= 0xD800 && result <= 0xDFFF) - return invalid_glyph; - return result; - } - // - // Four bytes: - // - char d = iterator[3]; - if (!_is_continuation(d)) - return invalid_glyph; - result = (a & 7) << 18; - result |= (b & 0x3F) << 12; - result |= (c & 0x3F) << 6; - result |= (d & 0x3F); - if (result > 0x10FFFF) - return invalid_glyph; - return result; - } - unicodechar get(const std::string& container, const std::string::iterator& iterator) { - if (iterator == container.end()) - return 0; - unsigned char a = iterator[0]; - if (a < 0x80) - return a; - if (!_is_bytecount(a)) - return invalid_glyph; - // - // Two bytes: - // - std::uint32_t result; - char b = iterator[1]; - if (!_is_continuation(b)) - return invalid_glyph; - if ((a & 0xE0) == 0xC0) { - result = (a & 0x1F) << 6; - result |= (b & 0x3F); - return result; - } - // - // Three bytes: - // - char c = iterator[2]; - if (!_is_continuation(c)) - return invalid_glyph; - if ((a & 0xF0) == 0xE0) { // three bytes - result = (a & 0xF) << 12; - result |= (b & 0x3F) << 6; - result |= (c & 0x3F); - if (result >= 0xD800 && result <= 0xDFFF) - return invalid_glyph; - return result; - } - // - // Four bytes: - // - char d = iterator[3]; - if (!_is_continuation(d)) - return invalid_glyph; - result = (a & 7) << 18; - result |= (b & 0x3F) << 12; - result |= (c & 0x3F) << 6; - result |= (d & 0x3F); - if (result > 0x10FFFF) - return invalid_glyph; - return result; - } - rawchar get_raw(const std::string& container, const std::string::iterator& iterator) { - if (iterator == container.end()) - return 0; - char a = iterator[0]; - char b = iterator[1]; - char c = iterator[2]; - char d = iterator[3]; - if (!_is_bytecount(a)) - return invalid_glyph; - { // 2 bytes - if (!_is_continuation(b)) - return invalid_glyph; - if ((a & 0xE0) == 0xC0) - return (a << 0x08) | b; - } - { // 3 bytes - if (!_is_continuation(c)) - return invalid_glyph; - if ((a & 0xF0) == 0xE0) - return (a << 0x10) | (b << 0x08) | c; - } - { // 4 bytes - if (!_is_continuation(d)) - return invalid_glyph; - return (a << 0x18) | (b << 0x10) | (c << 0x08) | d; - } - } - } -}; \ No newline at end of file + namespace utf8 { + inline static bool _is_bytecount(char c) { + return c < 0xF8; + } + inline static bool _is_continuation(char c) { + return ((c & 0xC0) == 0x80); + } + // + void advance(const std::string& container, std::string::iterator& iterator) { + if (iterator == container.end()) + return; + unsigned char c; + // + c = iterator[0]; + ++iterator; + if (c < 0x80)// single-byte glyph + return; + if (!_is_bytecount(c))// error + return; + // + c = iterator[0]; + ++iterator; + if (!_is_continuation(c))// error + return; + // + c = iterator[0]; + ++iterator; + if (!_is_continuation(c))// error + return; + // + c = iterator[0]; + ++iterator; + } + void append(std::string& target, const unicodechar c) { + if (c < 0x80) { + target.push_back(c); + return; + } + if (c < 0x800) { + target.push_back(0xE0 | ((c >> 6) & 0x1F)); + target.push_back(0x80 | (c & 0x3F)); + return; + } + if (c < 0x10000) { + target.push_back(0xE0 | ((c >> 12) & 0xF)); + target.push_back(0x80 | ((c >> 6) & 0x3F)); + target.push_back(0x80 | (c & 0x3F)); + return; + } + target.push_back(0xE0 | ((c >> 18) & 0x7)); + target.push_back(0x80 | ((c >> 12) & 0x3F)); + target.push_back(0x80 | ((c >> 6) & 0x3F)); + target.push_back(0x80 | (c & 0x1F)); + } + size_t count(std::string& container) { + return count_from(container, container.begin()); + } + size_t count_from(const std::string& container, const std::string::iterator& iterator) { + std::string::iterator working = iterator; + size_t count = 0; + while (working != container.end()) { + advance(container, working); + count++; + } + return count; + } + unicodechar get(const std::string& container, const std::string::const_iterator& iterator) { + if (iterator == container.end()) + return 0; + unsigned char a = iterator[0]; + if (a < 0x80) + return a; + if (!_is_bytecount(a)) + return invalid_glyph; + // + // Two bytes: + // + std::uint32_t result; + char b = iterator[1]; + if (!_is_continuation(b)) + return invalid_glyph; + if ((a & 0xE0) == 0xC0) { + result = (a & 0x1F) << 6; + result |= (b & 0x3F); + return result; + } + // + // Three bytes: + // + char c = iterator[2]; + if (!_is_continuation(c)) + return invalid_glyph; + if ((a & 0xF0) == 0xE0) {// three bytes + result = (a & 0xF) << 12; + result |= (b & 0x3F) << 6; + result |= (c & 0x3F); + if (result >= 0xD800 && result <= 0xDFFF) + return invalid_glyph; + return result; + } + // + // Four bytes: + // + char d = iterator[3]; + if (!_is_continuation(d)) + return invalid_glyph; + result = (a & 7) << 18; + result |= (b & 0x3F) << 12; + result |= (c & 0x3F) << 6; + result |= (d & 0x3F); + if (result > 0x10FFFF) + return invalid_glyph; + return result; + } + unicodechar get(const std::string& container, const std::string::iterator& iterator) { + if (iterator == container.end()) + return 0; + unsigned char a = iterator[0]; + if (a < 0x80) + return a; + if (!_is_bytecount(a)) + return invalid_glyph; + // + // Two bytes: + // + std::uint32_t result; + char b = iterator[1]; + if (!_is_continuation(b)) + return invalid_glyph; + if ((a & 0xE0) == 0xC0) { + result = (a & 0x1F) << 6; + result |= (b & 0x3F); + return result; + } + // + // Three bytes: + // + char c = iterator[2]; + if (!_is_continuation(c)) + return invalid_glyph; + if ((a & 0xF0) == 0xE0) {// three bytes + result = (a & 0xF) << 12; + result |= (b & 0x3F) << 6; + result |= (c & 0x3F); + if (result >= 0xD800 && result <= 0xDFFF) + return invalid_glyph; + return result; + } + // + // Four bytes: + // + char d = iterator[3]; + if (!_is_continuation(d)) + return invalid_glyph; + result = (a & 7) << 18; + result |= (b & 0x3F) << 12; + result |= (c & 0x3F) << 6; + result |= (d & 0x3F); + if (result > 0x10FFFF) + return invalid_glyph; + return result; + } + rawchar get_raw(const std::string& container, const std::string::iterator& iterator) { + if (iterator == container.end()) + return 0; + char a = iterator[0]; + char b = iterator[1]; + char c = iterator[2]; + char d = iterator[3]; + if (!_is_bytecount(a)) + return invalid_glyph; + {// 2 bytes + if (!_is_continuation(b)) + return invalid_glyph; + if ((a & 0xE0) == 0xC0) + return (a << 0x08) | b; + } + {// 3 bytes + if (!_is_continuation(c)) + return invalid_glyph; + if ((a & 0xF0) == 0xE0) + return (a << 0x10) | (b << 0x08) | c; + } + {// 4 bytes + if (!_is_continuation(d)) + return invalid_glyph; + return (a << 0x18) | (b << 0x10) | (c << 0x08) | d; + } + } + }// namespace utf8 +}; // namespace cobb \ No newline at end of file diff --git a/src/hooking/Hooks_AE.cpp b/src/hooking/Hooks_AE.cpp new file mode 100644 index 0000000..30f00cc --- /dev/null +++ b/src/hooking/Hooks_AE.cpp @@ -0,0 +1,426 @@ +// +// Created by m on 9/11/2022. +// +#include "hooking/Hooks.hpp" + +#include + +#include "hooking/Patches.hpp" + +namespace HookingAE { + using namespace Hooking; + + namespace DontVanillaSkinPlayer { + using namespace Hooking::DontVanillaSkinPlayer; + + REL::ID DontVanillaSkinPlayer_Hook_ID(24736); + std::uintptr_t DontVanillaSkinPlayer_Hook(DontVanillaSkinPlayer_Hook_ID.address() + 0x302);// 0x00364652 in 1.5.73 + + REL::ID TESObjectARMO_ApplyArmorAddon(17792);// 0x00228AD0 in 1.5.73 + + void Apply() { + LOG(info, "Patching vanilla player skinning"); + LOG(info, "TESObjectARMO_ApplyArmorAddon = {:x}", TESObjectARMO_ApplyArmorAddon.address() - REL::Module::get().base()); + LOG(info, "DontVanillaSkinPlayer_Hook = {:x}", DontVanillaSkinPlayer_Hook - REL::Module::get().base()); + { + struct DontVanillaSkinPlayer_Code: Xbyak::CodeGenerator { + DontVanillaSkinPlayer_Code() { + Xbyak::Label j_Out; + Xbyak::Label f_ApplyArmorAddon; + Xbyak::Label f_ShouldOverride; + + // armor in rcx, target in r13 + push(rcx); + push(rdx); + push(r9); + push(r8); + mov(rdx, r13); + sub(rsp, 0x40); + call(ptr[rip + f_ShouldOverride]); + add(rsp, 0x40); + pop(r8); + pop(r9); + pop(rdx); + pop(rcx); + test(al, al); + jnz(j_Out); + call(ptr[rip + f_ApplyArmorAddon]); + L(j_Out); + jmp(ptr[rip]); + dq(DontVanillaSkinPlayer_Hook + 0x5); + + L(f_ApplyArmorAddon); + dq(TESObjectARMO_ApplyArmorAddon.address()); + + L(f_ShouldOverride); + dq(uintptr_t(ShouldOverride)); + } + }; + DontVanillaSkinPlayer_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "Patching vanilla player skinning at addr = {:x}. base = {:x}", DontVanillaSkinPlayer_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(DontVanillaSkinPlayer_Hook, code); + } + LOG(info, "Done"); + } + }// namespace DontVanillaSkinPlayer + + namespace ShimWornFlags { + using namespace Hooking::ShimWornFlags; + + REL::ID ShimWornFlags_Hook_ID(24724); + std::uintptr_t ShimWornFlags_Hook(ShimWornFlags_Hook_ID.address() + 0x80);// 0x00362F0C in 1.5.73 + + REL::ID InventoryChanges_GetWornMask(16044);// 0x001D9040 in 1.5.73 + + void Apply() { + LOG(info, "Patching shim worn flags"); + LOG(info, "ShimWornFlags_Hook = {:x}", ShimWornFlags_Hook - REL::Module::get().base()); + LOG(info, "InventoryChanges_GetWornMask = {:x}", InventoryChanges_GetWornMask.address() - REL::Module::get().base()); + { + struct ShimWornFlags_Code: Xbyak::CodeGenerator { + ShimWornFlags_Code() { + Xbyak::Label j_SuppressVanilla; + Xbyak::Label j_Out; + Xbyak::Label f_ShouldOverrideSkinning; + Xbyak::Label f_GetWornMask; + Xbyak::Label f_OverrideWornFlags; + + // target in rbx + push(rcx); + mov(rcx, rbx); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverrideSkinning]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + test(al, al); + jnz(j_SuppressVanilla); + call(ptr[rip + f_GetWornMask]); + jmp(j_Out); + + L(j_SuppressVanilla); + push(rdx); + mov(rdx, rbx); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_OverrideWornFlags]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rdx); + + L(j_Out); + jmp(ptr[rip]); + dq(ShimWornFlags_Hook + 0x5); + + L(f_ShouldOverrideSkinning); + dq(uintptr_t(ShouldOverrideSkinning)); + + L(f_GetWornMask); + dq(InventoryChanges_GetWornMask.address()); + + L(f_OverrideWornFlags); + dq(uintptr_t(OverrideWornFlags)); + } + }; + ShimWornFlags_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "Patching shim worn flags at addr = {:x}. base = {:x}", ShimWornFlags_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(ShimWornFlags_Hook, code); + } + LOG(info, "Done"); + } + }// namespace ShimWornFlags + + namespace CustomSkinPlayer { + using namespace Hooking::CustomSkinPlayer; + + // This hook is completely different from pre-AE. + // The function we wanted to patch (AE 24735 + 0x81) was inlined into AE 24725. + // We might consider hooking both? + REL::ID CustomSkinPlayer_Hook_ID(24725); + std::uintptr_t CustomSkinPlayer_Hook(CustomSkinPlayer_Hook_ID.address() + 0x1EF);// 0x00364301 in 1.5.73 + + REL::ID InventoryChanges_ExecuteVisitorOnWorn(16096);// 0x001E51D0 in 1.5.73 + + void Apply() { + LOG(info, "Patching custom skin player"); + LOG(info, "CustomSkinPlayer_Hook = {:x}", CustomSkinPlayer_Hook - REL::Module::get().base()); + LOG(info, "InventoryChanges_ExecuteVisitorOnWorn = {:x}", InventoryChanges_ExecuteVisitorOnWorn.address() - REL::Module::get().base()); + { + struct CustomSkinPlayer_Code: Xbyak::CodeGenerator { + CustomSkinPlayer_Code() { + Xbyak::Label j_Out; + Xbyak::Label f_Custom; + Xbyak::Label f_ExecuteVisitorOnWorn; + Xbyak::Label f_ShouldOverrideSkinning; + + // call original function + call(ptr[rip + f_ExecuteVisitorOnWorn]); + + push(rcx); + mov(rcx, rbx); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverrideSkinning]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + + test(al, al); + jz(j_Out); + + push(rdx); + push(rcx); + mov(rcx, rbx); + mov(rdx, r15); + sub(rsp, 0x20); + call(ptr[rip + f_Custom]); + add(rsp, 0x20); + pop(rcx); + pop(rdx); + + L(j_Out); + jmp(ptr[rip]); + dq(CustomSkinPlayer_Hook + 0x5); + + L(f_Custom); + dq(uintptr_t(Custom)); + + L(f_ExecuteVisitorOnWorn); + dq(InventoryChanges_ExecuteVisitorOnWorn.address()); + + L(f_ShouldOverrideSkinning); + dq(uintptr_t(ShouldOverrideSkinning)); + } + }; + CustomSkinPlayer_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching custom skin player at addr = {}. base = {}", CustomSkinPlayer_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(CustomSkinPlayer_Hook, code); + } + LOG(info, "Done"); + } + }// namespace CustomSkinPlayer + + namespace FixEquipConflictCheck { + using namespace Hooking::FixEquipConflictCheck; + + REL::ID FixEquipConflictCheck_Hook_ID(38004); + std::uintptr_t FixEquipConflictCheck_Hook(FixEquipConflictCheck_Hook_ID.address() + 0x97);// 0x0060CAC7 in 1.5.73 + + REL::ID BGSBipedObjectForm_TestBodyPartByIndex(14119);// 0x001820A0 in 1.5.73 + + void Apply() { + LOG(info, "Patching fix for equip conflict check"); + LOG(info, "FixEquipConflictCheck_Hook = {:x}", FixEquipConflictCheck_Hook - REL::Module::get().base()); + LOG(info, "BGSBipedObjectForm_TestBodyPartByIndex = {:x}", BGSBipedObjectForm_TestBodyPartByIndex.address() - REL::Module::get().base()); + { + struct FixEquipConflictCheck_Code: Xbyak::CodeGenerator { + FixEquipConflictCheck_Code() { + Xbyak::Label j_Out; + Xbyak::Label j_Exit; + Xbyak::Label f_Inner; + Xbyak::Label f_TestBodyPartByIndex; + Xbyak::Label f_ShouldOverride; + + call(ptr[rip + f_TestBodyPartByIndex]); + test(al, al); + jz(j_Exit); + + // rsp+0x10: item + // rdi: Actor + // rbx: Body Slot + push(rcx); + // Set RCX to the RDX argument of the patched function + // RSP + Argument offset rel to original entry + Offset from push above + // + Offset from pushes in original entry + mov(rcx, ptr[rsp + 0x10 + 0x08 + 0xC8]); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverride]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + test(al, al); + mov(rax, 1); + jz(j_Out); + push(rcx); + push(rdx); + mov(rcx, rbx); + mov(rdx, rdi); + sub(rsp, 0x20); + call(ptr[rip + f_Inner]); + add(rsp, 0x20); + pop(rdx); + pop(rcx); + + L(j_Exit); + xor_(al, al); + + L(j_Out); + jmp(ptr[rip]); + dq(FixEquipConflictCheck_Hook + 0x5); + + L(f_TestBodyPartByIndex); + dq(BGSBipedObjectForm_TestBodyPartByIndex.address()); + + L(f_ShouldOverride); + dq(uintptr_t(ShouldOverride)); + + L(f_Inner); + dq(uintptr_t(Inner)); + } + }; + FixEquipConflictCheck_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching fix equip conflict check at addr = {:x}. base = {:x}", FixEquipConflictCheck_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(FixEquipConflictCheck_Hook, code); + } + LOG(info, "Done"); + } + }// namespace FixEquipConflictCheck + + namespace FixSkillLeveling { + using namespace Hooking::FixSkillLeveling; + + // Technically, we wanted 38539, but that guy got inlined into here. + REL::ID SkillMutationFunction_Hook_ID(38627); + std::uintptr_t SkillMutationFunction_Hook(SkillMutationFunction_Hook_ID.address() + 0x373); + + REL::ID BipedAnim_GetActorRaceBodyArmorPtrPtr(15696); + + void Apply() { + LOG(info, "Patching fix for skill level"); + LOG(info, "SkillMutationFunction_Hook_ID = {:x}", SkillMutationFunction_Hook - REL::Module::get().base()); + LOG(info, "BipedAnim_GetActorRaceBodyArmorPtrPtr = {:x}", BipedAnim_GetActorRaceBodyArmorPtrPtr.address() - REL::Module::get().base()); + { + struct FixSkillLeveling_Code: Xbyak::CodeGenerator { + FixSkillLeveling_Code() { + Xbyak::Label f_Inner; + Xbyak::Label f_GetActorRaceBodyArmorPtrPtr; + Xbyak::Label j_ResumeNormally; + Xbyak::Label j_SkipLoop; + + // In the original code, the Visitor lives exclusively + // in the registers. The mapping is + // RE::TESObjectARMO** shield = r13 + // RE::TESObjectARMO** torso = rbx + // std::uint32_t light = r15d (32-bit) + // std::uint32_t heavy = ebp (32-bit) + // All these registers are non-volatile + + push(rcx);// 8 + // We now push the register data onto the stack, laid out as the struct would be. + sub(rsp, 0x4); // Fake a push of the ebp + mov(ptr[rsp], ebp); // 4 + sub(rsp, 0x4); // Fake a push of the r15d + mov(ptr[rsp], r15d);// 4 + push(rbx); // 8 + push(r13); // 8 + // We don't need to fixup rsp because the pushes above should leave it 16-byte aligned + // rcx is already the right value, so just set rdx to point to the data we pushed + mov(rdx, rsp);// rsp points to the start of the pushed data + sub(rsp, 0x20); + call(ptr[rip + f_Inner]); + add(rsp, 0x20); + pop(r13); + pop(rbx); + mov(r15d, ptr[rsp]);// Fake a pop of r15d + add(rsp, 0x4); + mov(ebp, ptr[rsp]);// Fake a pop of ebp + add(rsp, 0x4); + pop(rcx); + test(al, al); + jnz(j_SkipLoop); + call(ptr[rip + f_GetActorRaceBodyArmorPtrPtr]); + + L(j_ResumeNormally); + jmp(ptr[rip]); + dq(SkillMutationFunction_Hook + 0x5); + + L(j_SkipLoop); + mov(r12, 0x0); + jmp(ptr[rip]); + dq(SkillMutationFunction_Hook + 0x7b); + + L(f_GetActorRaceBodyArmorPtrPtr); + dq(BipedAnim_GetActorRaceBodyArmorPtrPtr.address()); + + L(f_Inner); + dq(uintptr_t(Inner)); + } + }; + FixSkillLeveling_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching skill mutation logic at addr = {:x}. base = {:x}", SkillMutationFunction_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(SkillMutationFunction_Hook, code); + } + LOG(info, "Done"); + } + }// namespace FixSkillLeveling + + namespace RTTIPrinter { + using namespace Hooking::RTTIPrinter; + + REL::ID InventoryChanges_ExecuteVisitorOnWorn(16096);// 0x001E51D0 in 1.5.73 + std::uintptr_t InventoryChanges_ExecuteVisitorOnWorn_Hook( + InventoryChanges_ExecuteVisitorOnWorn.address() + 0x2A);// 0x00364301 in 1.5.73 + + void Apply() { + LOG(info, "Patching RTTI print"); + LOG(info, "InventoryChanges_ExecuteVisitorOnWorn_Hook = {:x}", + InventoryChanges_ExecuteVisitorOnWorn_Hook - REL::Module::get().base()); + { + struct RTTI_Code: Xbyak::CodeGenerator { + RTTI_Code() { + Xbyak::Label f_PrintRTTI; + + push(rcx); + push(rdx); + push(rax); + mov(rcx, rdx); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_PrintRTTI]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rax); + pop(rdx); + pop(rcx); + + jmp(ptr[rip]); + dq(InventoryChanges_ExecuteVisitorOnWorn_Hook + 0x6); + + L(f_PrintRTTI); + dq(uintptr_t(Print_RTTI)); + } + }; + RTTI_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching RTTI print at addr = {:x}. base = {:x}", + InventoryChanges_ExecuteVisitorOnWorn_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<6>( + InventoryChanges_ExecuteVisitorOnWorn_Hook, code); + } + } + }// namespace RTTIPrinter + + void ApplyPlayerSkinningHooks() { + DontVanillaSkinPlayer::Apply(); + ShimWornFlags::Apply(); + CustomSkinPlayer::Apply(); + FixEquipConflictCheck::Apply(); + FixSkillLeveling::Apply(); +#if _DEBUG + RTTIPrinter::Apply(); +#endif + } +}// namespace HookingAE diff --git a/src/hooking/Hooks_PRE_AE.cpp b/src/hooking/Hooks_PRE_AE.cpp new file mode 100644 index 0000000..1277493 --- /dev/null +++ b/src/hooking/Hooks_PRE_AE.cpp @@ -0,0 +1,348 @@ +// +// Created by m on 9/11/2022. +// + +#include "hooking/Hooks.hpp" + +#include + +#include "hooking/Patches.hpp" + +namespace HookingPREAE { + using namespace Hooking; + + namespace DontVanillaSkinPlayer { + using namespace Hooking::DontVanillaSkinPlayer; + + REL::ID DontVanillaSkinPlayer_Hook_ID(24232); + std::uintptr_t DontVanillaSkinPlayer_Hook(DontVanillaSkinPlayer_Hook_ID.address() + 0x302);// 0x00364652 in 1.5.73 + + REL::ID TESObjectARMO_ApplyArmorAddon(17392);// 0x00228AD0 in 1.5.73 + + void Apply() { + LOG(info, "Patching vanilla player skinning"); + LOG(info, "TESObjectARMO_ApplyArmorAddon = {:x}", TESObjectARMO_ApplyArmorAddon.address() - REL::Module::get().base()); + LOG(info, "DontVanillaSkinPlayer_Hook = {:x}", DontVanillaSkinPlayer_Hook - REL::Module::get().base()); + { + struct DontVanillaSkinPlayer_Code: Xbyak::CodeGenerator { + DontVanillaSkinPlayer_Code() { + Xbyak::Label j_Out; + Xbyak::Label f_ApplyArmorAddon; + Xbyak::Label f_ShouldOverride; + + // armor in rcx, target in r13 + push(rcx); + push(rdx); + push(r9); + push(r8); + mov(rdx, r13); + sub(rsp, 0x40); + call(ptr[rip + f_ShouldOverride]); + add(rsp, 0x40); + pop(r8); + pop(r9); + pop(rdx); + pop(rcx); + test(al, al); + jnz(j_Out); + call(ptr[rip + f_ApplyArmorAddon]); + L(j_Out); + jmp(ptr[rip]); + dq(DontVanillaSkinPlayer_Hook + 0x5); + + L(f_ApplyArmorAddon); + dq(TESObjectARMO_ApplyArmorAddon.address()); + + L(f_ShouldOverride); + dq(uintptr_t(ShouldOverride)); + } + }; + DontVanillaSkinPlayer_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "Patching vanilla player skinning at addr = {:x}. base = {:x}", DontVanillaSkinPlayer_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(DontVanillaSkinPlayer_Hook, code); + } + LOG(info, "Done"); + } + }// namespace DontVanillaSkinPlayer + + namespace ShimWornFlags { + using namespace Hooking::ShimWornFlags; + + REL::ID ShimWornFlags_Hook_ID(24220); + std::uintptr_t ShimWornFlags_Hook(ShimWornFlags_Hook_ID.address() + 0x7C);// 0x00362F0C in 1.5.73 + + REL::ID InventoryChanges_GetWornMask(15806);// 0x001D9040 in 1.5.73 + + void Apply() { + LOG(info, "Patching shim worn flags"); + LOG(info, "ShimWornFlags_Hook = {:x}", ShimWornFlags_Hook - REL::Module::get().base()); + LOG(info, "InventoryChanges_GetWornMask = {:x}", InventoryChanges_GetWornMask.address() - REL::Module::get().base()); + { + struct ShimWornFlags_Code: Xbyak::CodeGenerator { + ShimWornFlags_Code() { + Xbyak::Label j_SuppressVanilla; + Xbyak::Label j_Out; + Xbyak::Label f_ShouldOverrideSkinning; + Xbyak::Label f_GetWornMask; + Xbyak::Label f_OverrideWornFlags; + + // target in rsi + push(rcx); + mov(rcx, rsi); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverrideSkinning]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + test(al, al); + jnz(j_SuppressVanilla); + call(ptr[rip + f_GetWornMask]); + jmp(j_Out); + + L(j_SuppressVanilla); + push(rdx); + mov(rdx, rsi); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_OverrideWornFlags]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rdx); + + L(j_Out); + jmp(ptr[rip]); + dq(ShimWornFlags_Hook + 0x5); + + L(f_ShouldOverrideSkinning); + dq(uintptr_t(ShouldOverrideSkinning)); + + L(f_GetWornMask); + dq(InventoryChanges_GetWornMask.address()); + + L(f_OverrideWornFlags); + dq(uintptr_t(OverrideWornFlags)); + } + }; + ShimWornFlags_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "Patching shim worn flags at addr = {:x}. base = {:x}", ShimWornFlags_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(ShimWornFlags_Hook, code); + } + LOG(info, "Done"); + } + }// namespace ShimWornFlags + + namespace CustomSkinPlayer { + using namespace Hooking::CustomSkinPlayer; + + REL::ID CustomSkinPlayer_Hook_ID(24231); + std::uintptr_t CustomSkinPlayer_Hook(CustomSkinPlayer_Hook_ID.address() + 0x81);// 0x00364301 in 1.5.73 + + REL::ID InventoryChanges_ExecuteVisitorOnWorn(15856);// 0x001E51D0 in 1.5.73 + + void Apply() { + LOG(info, "Patching custom skin player"); + LOG(info, "CustomSkinPlayer_Hook = {:x}", CustomSkinPlayer_Hook - REL::Module::get().base()); + LOG(info, "InventoryChanges_ExecuteVisitorOnWorn = {:x}", InventoryChanges_ExecuteVisitorOnWorn.address() - REL::Module::get().base()); + { + struct CustomSkinPlayer_Code: Xbyak::CodeGenerator { + CustomSkinPlayer_Code() { + Xbyak::Label j_Out; + Xbyak::Label f_Custom; + Xbyak::Label f_ExecuteVisitorOnWorn; + Xbyak::Label f_ShouldOverrideSkinning; + + // call original function + call(ptr[rip + f_ExecuteVisitorOnWorn]); + + push(rcx); + mov(rcx, rbx); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverrideSkinning]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + + test(al, al); + jz(j_Out); + + push(rdx); + push(rcx); + mov(rcx, rbx); + mov(rdx, rdi); + sub(rsp, 0x20); + call(ptr[rip + f_Custom]); + add(rsp, 0x20); + pop(rcx); + pop(rdx); + + L(j_Out); + jmp(ptr[rip]); + dq(CustomSkinPlayer_Hook + 0x5); + + L(f_Custom); + dq(uintptr_t(Custom)); + + L(f_ExecuteVisitorOnWorn); + dq(InventoryChanges_ExecuteVisitorOnWorn.address()); + + L(f_ShouldOverrideSkinning); + dq(uintptr_t(ShouldOverrideSkinning)); + } + }; + CustomSkinPlayer_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching custom skin player at addr = {}. base = {}", CustomSkinPlayer_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(CustomSkinPlayer_Hook, code); + } + LOG(info, "Done"); + } + }// namespace CustomSkinPlayer + + namespace FixEquipConflictCheck { + using namespace Hooking::FixEquipConflictCheck; + + REL::ID FixEquipConflictCheck_Hook_ID(36979); + std::uintptr_t FixEquipConflictCheck_Hook(FixEquipConflictCheck_Hook_ID.address() + 0x97);// 0x0060CAC7 in 1.5.73 + + REL::ID BGSBipedObjectForm_TestBodyPartByIndex(14026);// 0x001820A0 in 1.5.73 + + void Apply() { + LOG(info, "Patching fix for equip conflict check"); + LOG(info, "FixEquipConflictCheck_Hook = {:x}", FixEquipConflictCheck_Hook - REL::Module::get().base()); + LOG(info, "BGSBipedObjectForm_TestBodyPartByIndex = {:x}", BGSBipedObjectForm_TestBodyPartByIndex.address() - REL::Module::get().base()); + { + struct FixEquipConflictCheck_Code: Xbyak::CodeGenerator { + FixEquipConflictCheck_Code() { + Xbyak::Label j_Out; + Xbyak::Label j_Exit; + Xbyak::Label f_Inner; + Xbyak::Label f_TestBodyPartByIndex; + Xbyak::Label f_ShouldOverride; + + call(ptr[rip + f_TestBodyPartByIndex]); + test(al, al); + jz(j_Exit); + + // rsp+0x10: item + // rdi: Actor + // rbx: Body Slot + push(rcx); + // Set RCX to the RDX argument of the patched function + // RSP + Argument offset rel to original entry + Offset from push above + // + Offset from pushes in original entry + mov(rcx, ptr[rsp + 0x10 + 0x08 + 0xC8]); + sub(rsp, 0x8);// Ensure 16-byte alignment of stack pointer + sub(rsp, 0x20); + call(ptr[rip + f_ShouldOverride]); + add(rsp, 0x20); + add(rsp, 0x8); + pop(rcx); + test(al, al); + mov(rax, 1); + jz(j_Out); + push(rcx); + push(rdx); + mov(rcx, rbx); + mov(rdx, rdi); + sub(rsp, 0x20); + call(ptr[rip + f_Inner]); + add(rsp, 0x20); + pop(rdx); + pop(rcx); + + L(j_Exit); + xor_(al, al); + + L(j_Out); + jmp(ptr[rip]); + dq(FixEquipConflictCheck_Hook + 0x5); + + L(f_TestBodyPartByIndex); + dq(BGSBipedObjectForm_TestBodyPartByIndex.address()); + + L(f_ShouldOverride); + dq(uintptr_t(ShouldOverride)); + + L(f_Inner); + dq(uintptr_t(Inner)); + } + }; + FixEquipConflictCheck_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching fix equip conflict check at addr = {:x}. base = {:x}", FixEquipConflictCheck_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(FixEquipConflictCheck_Hook, code); + } + LOG(info, "Done"); + } + }// namespace FixEquipConflictCheck + + namespace FixSkillLeveling { + using namespace Hooking::FixSkillLeveling; + + REL::ID GetSkillToLevel_Hook_ID(37589); + std::uintptr_t GetSkillToLevel_Hook(GetSkillToLevel_Hook_ID.address() + 0x52); + + REL::ID BipedAnim_VisitGetWornArmorTypeVisitor(37688); + + void Apply() { + LOG(info, "Patching fix for skill level"); + LOG(info, "GetSkillToLevel_Hook = {:x}", GetSkillToLevel_Hook - REL::Module::get().base()); + LOG(info, "BipedAnim__GetActorRaceBodyArmorPtrPtr = {:x}", BipedAnim_VisitGetWornArmorTypeVisitor.address() - REL::Module::get().base()); + { + struct FixSkillLeveling_Code: Xbyak::CodeGenerator { + FixSkillLeveling_Code() { + Xbyak::Label j_Out; + Xbyak::Label f_Inner; + Xbyak::Label f_VisitGetWornArmorTypeVisitor; + + push(rcx); + push(rdx); + sub(rsp, 0x20); + call(ptr[rip + f_Inner]); + add(rsp, 0x20); + pop(rdx); + pop(rcx); + test(al, al); + jnz(j_Out); + call(ptr[rip + f_VisitGetWornArmorTypeVisitor]); + + L(j_Out); + jmp(ptr[rip]); + dq(GetSkillToLevel_Hook + 0x5); + + L(f_VisitGetWornArmorTypeVisitor); + dq(BipedAnim_VisitGetWornArmorTypeVisitor.address()); + + L(f_Inner); + dq(uintptr_t(Inner)); + } + }; + FixSkillLeveling_Code gen; + void* code = g_localTrampoline->allocate(gen); + + LOG(info, "AVI: Patching skill level logic at addr = {:x}. base = {:x}", GetSkillToLevel_Hook, REL::Module::get().base()); + g_branchTrampoline->write_branch<5>(GetSkillToLevel_Hook, code); + } + LOG(info, "Done"); + } + }// namespace FixSkillLeveling + + namespace RTTIPrinter { + } + + void ApplyPlayerSkinningHooks() { + DontVanillaSkinPlayer::Apply(); + ShimWornFlags::Apply(); + CustomSkinPlayer::Apply(); + FixEquipConflictCheck::Apply(); + FixSkillLeveling::Apply(); + } +}// namespace HookingPREAE diff --git a/src/hooking/Patches.cpp b/src/hooking/Patches.cpp new file mode 100644 index 0000000..2ea1565 --- /dev/null +++ b/src/hooking/Patches.cpp @@ -0,0 +1,290 @@ +// +// Created by m on 9/11/2022. +// + +#include "hooking/Patches.hpp" + +#include + +#include "ArmorAddonOverrideService.h" +#include "Utility.h" + +namespace Hooking { + SKSE::Trampoline* g_localTrampoline = nullptr; + SKSE::Trampoline* g_branchTrampoline = nullptr; + + bool ShouldOverrideSkinning(RE::TESObjectREFR* target) { + LogExit exitPrint("ShouldOverrideSkinning"sv); + if (!target) { + LOG(warn, "Target was null"); + return false; + } + if (!ArmorAddonOverrideService::GetInstance().enabled) return false; + auto actor = skyrim_cast(target); + if (!actor) { + LOG(warn, "Target failed to cast to RE::Actor"); + return false; + } + if (!ArmorAddonOverrideService::GetInstance().shouldOverride(actor->GetHandle().native_handle())) return false; + return true; + } + + class EquippedArmorVisitor: public RE::IItemChangeVisitorAugment { + // + // If the player has a shield equipped, and if we're not overriding that + // shield, then we need to grab the equipped shield's worn-flags. + // + public: + virtual VisitorReturn Visit(RE::InventoryEntryData* data) override { + auto form = data->object; + if (form && form->formType == RE::FormType::Armor) { + equipped.emplace(skyrim_cast(form)); + } + return VisitorReturn::kContinue;// Return true to "continue visiting". + }; + + std::unordered_set equipped; + }; + + template + class EquippedVisitorFn: public RE::IItemChangeVisitorAugment { + // + // If the player has a shield equipped, and if we're not overriding that + // shield, then we need to grab the equipped shield's worn-flags. + // + public: + explicit EquippedVisitorFn(F callable) : callable(callable){}; + virtual VisitorReturn Visit(RE::InventoryEntryData* data) override { + auto form = data->object; + if (form) { + callable(data->object); + } + return VisitorReturn::kContinue;// Return true to "continue visiting". + }; + + F callable; + }; + + namespace DontVanillaSkinPlayer { + bool ShouldOverride(RE::TESObjectARMO* armor, RE::TESObjectREFR* target) { + LogExit exitPrint("DontVanillaSkinPlayer.ShouldOverride"sv); + if (!ShouldOverrideSkinning(target)) { return false; } + auto& svc = ArmorAddonOverrideService::GetInstance(); + auto actor = skyrim_cast(target); + if (!actor) { + // Actor failed to cast... + LOG(warn, "ShouldOverride: Failed to cast target to Actor."); + return true; + } + auto& outfit = svc.currentOutfit(actor->GetHandle().native_handle()); + auto inventory = target->GetInventoryChanges(); + EquippedArmorVisitor visitor; + if (inventory) { + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + } else { + LOG(warn, "ShouldOverride: Unable to get target inventory."); + return true; + } + // Block the item (return true) if the item isn't in the display set. + return outfit.computeDisplaySet(visitor.equipped).count(armor) == 0; + } + }// namespace DontVanillaSkinPlayer + + namespace ShimWornFlags { + std::uint32_t OverrideWornFlags(RE::InventoryChanges* inventory, RE::TESObjectREFR* target) { + LogExit exitPrint("ShimWornFlags.OverrideWornFlags"sv); + std::uint32_t mask = 0; + auto actor = skyrim_cast(target); + if (!actor) return mask; + auto& svc = ArmorAddonOverrideService::GetInstance(); + auto& outfit = svc.currentOutfit(actor->GetHandle().native_handle()); + EquippedArmorVisitor visitor; + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + auto displaySet = outfit.computeDisplaySet(visitor.equipped); + for (auto& armor : displaySet) { + mask |= static_cast(armor->GetSlotMask()); + } + return mask; + } + }// namespace ShimWornFlags + + namespace CustomSkinPlayer { + void Custom(RE::Actor* target, RE::ActorWeightModel* actorWeightModel) { + LogExit exitPrint("CustomSkinPlayer.Custom"sv); + auto actor = skyrim_cast(target); + if (!actor) { + // Actor failed to cast... + LOG(info, "Custom: Failed to cast target to Actor."); + return; + } + // Get basic actor information (race and sex) + if (!actorWeightModel) + return; + auto base = skyrim_cast(target->data.objectReference); + if (!base) + return; + auto race = base->race; + bool isFemale = base->IsFemale(); + // + auto& svc = ArmorAddonOverrideService::GetInstance(); + auto& outfit = svc.currentOutfit(actor->GetHandle().native_handle()); + + // Get actor inventory and equipped items + auto inventory = target->GetInventoryChanges(); + EquippedArmorVisitor visitor; + if (inventory) { + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + } else { + LOG(info, "Custom: Unable to get target inventory."); + return; + } + + // Compute the display set. + auto displaySet = outfit.computeDisplaySet(visitor.equipped); + + // Compute the remaining items to be applied to the player + // We assume that the DontVanillaSkinPlayer already passed through + // the equipped items that will be shown, so we only need to worry about the + // items that are not equipped but which are in the outfit or which are masked + // by the outfit. + std::unordered_set applySet; + for (const auto& item : displaySet) { + if (visitor.equipped.find(item) == visitor.equipped.end()) applySet.insert(item); + } + + for (auto it = applySet.cbegin(); it != applySet.cend(); ++it) { + // TODO: [SlotPassthru] Also do the same iteration over the passthrough equipped items? + RE::TESObjectARMO* armor = *it; + if (armor) { + RE::TESObjectARMOAugments::ApplyArmorAddon(armor, race, actorWeightModel, isFemale); + } + } + } + }// namespace CustomSkinPlayer + + namespace FixEquipConflictCheck { + // + // When you try to equip an item, the game loops over the armors in your ActorWeightModel + // rather than your other worn items. Because we're tampering with what goes into the AWM, + // this means that conflict checks are run against your outfit instead of your equipment, + // unless we patch in a fix. (For example, if your outfit doesn't include a helmet, then + // you'd be able to stack helmets endlessly without this patch.) + // + // The loop in question is performed in Actor::Unk_120, which is also generally responsible + // for equipping items at all. + // + class _Visitor: public RE::IItemChangeVisitorAugment { + // + // Bethesda used a visitor to add armor-addons to the ActorWeightModel in the first + // place (see call stack for DontVanillaSkinPlayer patch), so why not use a similar + // visitor to check for conflicts? + // + public: + virtual VisitorReturn Visit(RE::InventoryEntryData* data) override { + // TODO: [SlotPassthru] We might be able to leave this as-is. + auto form = data->object; + if (form && form->formType == RE::FormType::Armor) { + auto armor = skyrim_cast(form); + if (armor && RE::TESObjectARMOAugments::TestBodyPartByIndex(armor, this->conflictIndex)) { + auto em = RE::ActorEquipManager::GetSingleton(); + // + // TODO: The third argument to this call is meant to be a + // BaseExtraList*, and Bethesda supplies one when calling from Unk_120. + // Can we get away with a nullptr here, or do we have to find the + // BaseExtraList that contains an ExtraWorn? + // + // I'm not sure how to investigate this, but I did run one test, and + // that works properly: I gave myself ten Falmer Helmets and applied + // different enchantments to two of them (leaving the others + // unenchanted). In tests, I was unable to stack the helmets with each + // other or with other helmets, suggesting that the BaseExtraList may + // not be strictly necessary. + // + em->UnequipObject(this->target, form, nullptr, 1, nullptr, false, false, true, false, nullptr); + } + } + return VisitorReturn::kContinue;// True to continue visiting + }; + + RE::Actor* target; + std::uint32_t conflictIndex = 0; + }; + void Inner(std::uint32_t bodyPartForNewItem, RE::Actor* target) { + LogExit exitPrint("FixEquipConflictCheck.Inner"sv); + auto inventory = target->GetInventoryChanges(); + if (inventory) { + _Visitor visitor; + visitor.conflictIndex = bodyPartForNewItem; + visitor.target = target; + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &visitor); + } else { + LOG(info, "OverridePlayerSkinning: Conflict check failed: no inventory!"); + } + } + bool ShouldOverride(RE::TESForm* item) { + LogExit exitPrint("FixEquipConflictCheck.ShouldOverride"sv); + // + // We only hijack equipping for armors, so I'd like for this patch to only + // apply to armors as well. It shouldn't really matter -- before I added + // this check, weapons and the like tested in-game with no issues, likely + // because they're handled very differently -- but I just wanna be sure. + // We should use vanilla code whenever we don't need to NOT use it. + // + return (item->formType == RE::FormType::Armor); + } + }// namespace FixEquipConflictCheck + + namespace FixSkillLeveling { + struct Visitor { + RE::TESObjectARMO** shield; + RE::TESObjectARMO** torso; + std::uint32_t light; + std::uint32_t heavy; + }; + static_assert(sizeof(Visitor) == 0x18); + + bool Inner(RE::BipedAnim* biped, Visitor* bipedVisitor) { + LogExit exitPrint("FixSkillLeveling.Inner"sv); + auto target = biped->actorRef.get();// Retain via smart pointer. + if (!target) return false; + auto actor = skyrim_cast(target.get()); + if (!actor) return false; + if (!ShouldOverrideSkinning(actor)) return false; + auto inventory = target->GetInventoryChanges(); + if (!inventory) return false; + EquippedVisitorFn inventoryVisitor([&](RE::TESBoundObject* form) { + if (form->formType == RE::FormType::Armor) { + auto armor = skyrim_cast(form); + if (!armor) return; + RE::BIPED_MODEL::ArmorType armorType = armor->GetArmorType(); + auto mask = static_cast(armor->GetSlotMask()); + // Exclude shields. + if (armor->IsShield()) return; + // Count the number of slots taken by the armor. Body slot counts double. + int count = 0; + if (mask & static_cast(RE::BIPED_MODEL::BipedObjectSlot::kBody)) count++; + count += std::popcount(mask); + if (armorType == RE::BIPED_MODEL::ArmorType::kLightArmor) bipedVisitor->light += count; + if (armorType == RE::BIPED_MODEL::ArmorType::kHeavyArmor) bipedVisitor->heavy += count; + } + return; + }); + RE::InventoryChangesAugments::ExecuteAugmentVisitorOnWorn(inventory, &inventoryVisitor); + return true; + } + }// namespace FixSkillLeveling + + namespace RTTIPrinter { + void Print_RTTI(RE::InventoryChanges::IItemChangeVisitor* target) { + LogExit exitPrint("RTTIPrinter.Print_RTTI"sv); + void* object = (void*) target; + void* vtable = *(void**) object; + void* info_block = ((void**) vtable)[-1]; + uintptr_t id = ((unsigned long*) info_block)[3]; + uintptr_t info = id + REL::Module::get().base(); + char* name = (char*) info + 16; + LOG(info, "vtable = {}, typeinfo = {}, typename = {}", vtable, info_block, + name); + } + }// namespace RTTIPrinter +}// namespace Hooking \ No newline at end of file diff --git a/src/hooking/PlayerSkinning_AE.cpp b/src/hooking/PlayerSkinning_AE.cpp deleted file mode 100644 index 9dabfd6..0000000 --- a/src/hooking/PlayerSkinning_AE.cpp +++ /dev/null @@ -1,523 +0,0 @@ -#ifdef SKYRIM_VERSION_IS_AE - -#include "ArmorAddonOverrideService.h" - -#include - -namespace OutfitSystem { - SKSE::Trampoline* g_localTrampoline = nullptr; - SKSE::Trampoline* g_branchTrampoline = nullptr; - - bool ShouldOverrideSkinning(RE::TESObjectREFR* target) { - if (!ArmorAddonOverrideService::GetInstance().shouldOverride((RE::Actor*)target)) - return false; - return target == RE::PlayerCharacter::GetSingleton(); - } - - class EquippedArmorVisitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // If the player has a shield equipped, and if we're not overriding that - // shield, then we need to grab the equipped shield's worn-flags. - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - auto form = data->object; - if (form && form->formType == RE::FormType::Armor) { - auto armor = reinterpret_cast(form); - equipped.emplace(armor); - } - return ReturnType::kContinue; // Return true to "continue visiting". - }; - - std::unordered_set equipped; - }; - - REL::ID TESObjectARMO_ApplyArmorAddon(17792); // 0x00228AD0 in 1.5.73 - - namespace DontVanillaSkinPlayer { - bool _stdcall ShouldOverride(RE::TESObjectARMO* armor, RE::TESObjectREFR* target) { - if (!ShouldOverrideSkinning(target)) - return false; - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit((RE::Actor*)target); - auto actor = skyrim_cast(target); - if (!actor) { - // Actor failed to cast... - LOG(info, "ShouldOverride: Failed to cast target to Actor."); - return true; - } - auto inventory = target->GetInventoryChanges(); - EquippedArmorVisitor visitor; - if (inventory) { - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "ShouldOverride: Unable to get target inventory."); - return true; - } - // Block the item (return true) if the item isn't in the display set. - return outfit.computeDisplaySet(visitor.equipped).count(armor) == 0; - } - - REL::ID DontVanillaSkinPlayer_Hook_ID(24736); - std::uintptr_t DontVanillaSkinPlayer_Hook(DontVanillaSkinPlayer_Hook_ID.address() + 0x302); // 0x00364652 in 1.5.73 - - void Apply() { - LOG(info, "Patching vanilla player skinning"); - LOG(info, "TESObjectARMO_ApplyArmorAddon = {:x}", TESObjectARMO_ApplyArmorAddon.address() - REL::Module::get().base()); - LOG(info, "DontVanillaSkinPlayer_Hook = {:x}", DontVanillaSkinPlayer_Hook - REL::Module::get().base()); - { - struct DontVanillaSkinPlayer_Code : Xbyak::CodeGenerator { - DontVanillaSkinPlayer_Code() { - Xbyak::Label j_Out; - Xbyak::Label f_ApplyArmorAddon; - Xbyak::Label f_ShouldOverride; - - // armor in rcx, target in r13 - push(rcx); - push(rdx); - push(r9); - push(r8); - mov(rdx, r13); - sub(rsp, 0x40); - call(ptr[rip + f_ShouldOverride]); - add(rsp, 0x40); - pop(r8); - pop(r9); - pop(rdx); - pop(rcx); - test(al, al); - jnz(j_Out); - call(ptr[rip + f_ApplyArmorAddon]); - L(j_Out); - jmp(ptr[rip]); - dq(DontVanillaSkinPlayer_Hook + 0x5); - - L(f_ApplyArmorAddon); - dq(TESObjectARMO_ApplyArmorAddon.address()); - - L(f_ShouldOverride); - dq(uintptr_t(ShouldOverride)); - } - }; - DontVanillaSkinPlayer_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "Patching vanilla player skinning at addr = {:x}. base = {:x}", DontVanillaSkinPlayer_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(DontVanillaSkinPlayer_Hook, code); - } - LOG(info, "Done"); - } - } // namespace DontVanillaSkinPlayer - - namespace ShimWornFlags { - std::uint32_t OverrideWornFlags(RE::InventoryChanges* inventory, RE::TESObjectREFR* target) { - std::uint32_t mask = 0; - auto actor = skyrim_cast(target); - if (!actor) return mask; - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit(actor); - EquippedArmorVisitor visitor; - inventory->ExecuteVisitorOnWorn(&visitor); - auto displaySet = outfit.computeDisplaySet(visitor.equipped); - for (auto& armor : displaySet) { - mask |= static_cast(armor->GetSlotMask()); - } - return mask; - } - - REL::ID ShimWornFlags_Hook_ID(24724); - std::uintptr_t ShimWornFlags_Hook(ShimWornFlags_Hook_ID.address() + 0x80); // 0x00362F0C in 1.5.73 - - REL::ID InventoryChanges_GetWornMask(16044); // 0x001D9040 in 1.5.73 - - void Apply() { - LOG(info, "Patching shim worn flags"); - LOG(info, "ShimWornFlags_Hook = {:x}", ShimWornFlags_Hook - REL::Module::get().base()); - LOG(info, "InventoryChanges_GetWornMask = {:x}", InventoryChanges_GetWornMask.address() - REL::Module::get().base()); - { - struct ShimWornFlags_Code : Xbyak::CodeGenerator { - ShimWornFlags_Code() { - Xbyak::Label j_SuppressVanilla; - Xbyak::Label j_Out; - Xbyak::Label f_ShouldOverrideSkinning; - Xbyak::Label f_GetWornMask; - Xbyak::Label f_OverrideWornFlags; - - // target in rbx - push(rcx); - mov(rcx, rbx); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverrideSkinning]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - test(al, al); - jnz(j_SuppressVanilla); - call(ptr[rip + f_GetWornMask]); - jmp(j_Out); - - L(j_SuppressVanilla); - push(rdx); - mov(rdx, rbx); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_OverrideWornFlags]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rdx); - - L(j_Out); - jmp(ptr[rip]); - dq(ShimWornFlags_Hook + 0x5); - - L(f_ShouldOverrideSkinning); - dq(uintptr_t(ShouldOverrideSkinning)); - - L(f_GetWornMask); - dq(InventoryChanges_GetWornMask.address()); - - L(f_OverrideWornFlags); - dq(uintptr_t(OverrideWornFlags)); - } - }; - ShimWornFlags_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "Patching shim worn flags at addr = {:x}. base = {:x}", ShimWornFlags_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(ShimWornFlags_Hook, code); - } - LOG(info, "Done"); - } - } // namespace ShimWornFlags - - namespace CustomSkinPlayer { - void Custom(RE::Actor* target, RE::ActorWeightModel* actorWeightModel) { - if (!skyrim_cast(target)) { - // Actor failed to cast... - LOG(info, "Custom: Failed to cast target to Actor."); - return; - } - // Get basic actor information (race and sex) - if (!actorWeightModel) - return; - auto base = skyrim_cast(target->data.objectReference); - if (!base) - return; - auto race = base->race; - bool isFemale = base->IsFemale(); - // - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit((RE::Actor*)target); - - // Get actor inventory and equipped items - auto inventory = target->GetInventoryChanges(); - EquippedArmorVisitor visitor; - if (inventory) { - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "Custom: Unable to get target inventory."); - return; - } - - // Compute the display set. - auto displaySet = outfit.computeDisplaySet(visitor.equipped); - - // Compute the remaining items to be applied to the player - // We assume that the DontVanillaSkinPlayer already passed through - // the equipped items that will be shown, so we only need to worry about the - // items that are not equipped but which are in the outfit or which are masked - // by the outfit. - std::unordered_set applySet; - for (const auto& item : displaySet) { - if (visitor.equipped.find(item) == visitor.equipped.end()) applySet.insert(item); - } - - for (auto it = applySet.cbegin(); it != applySet.cend(); ++it) { - // TODO: [SlotPassthru] Also do the same iteration over the passthrough equipped items? - RE::TESObjectARMO* armor = *it; - if (armor) { - armor->ApplyArmorAddon(race, actorWeightModel, isFemale); - } - } - } - - // This hook is completely different from pre-AE. - // The function we wanted to patch (AE 24735 + 0x81) was inlined into AE 24725. - // We might consider hooking both? - REL::ID CustomSkinPlayer_Hook_ID(24725); - std::uintptr_t CustomSkinPlayer_Hook(CustomSkinPlayer_Hook_ID.address() + 0x1EF); // 0x00364301 in 1.5.73 - - REL::ID InventoryChanges_ExecuteVisitorOnWorn(16096); // 0x001E51D0 in 1.5.73 - - void Apply() { - LOG(info, "Patching custom skin player"); - LOG(info, "CustomSkinPlayer_Hook = {:x}", CustomSkinPlayer_Hook - REL::Module::get().base()); - LOG(info, "InventoryChanges_ExecuteVisitorOnWorn = {:x}", InventoryChanges_ExecuteVisitorOnWorn.address() - REL::Module::get().base()); - { - struct CustomSkinPlayer_Code : Xbyak::CodeGenerator { - CustomSkinPlayer_Code() { - Xbyak::Label j_Out; - Xbyak::Label f_Custom; - Xbyak::Label f_ExecuteVisitorOnWorn; - Xbyak::Label f_ShouldOverrideSkinning; - - // call original function - call(ptr[rip + f_ExecuteVisitorOnWorn]); - - push(rcx); - mov(rcx, rbx); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverrideSkinning]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - - test(al, al); - jz(j_Out); - - push(rdx); - push(rcx); - mov(rcx, rbx); - mov(rdx, r15); - sub(rsp, 0x20); - call(ptr[rip + f_Custom]); - add(rsp, 0x20); - pop(rcx); - pop(rdx); - - L(j_Out); - jmp(ptr[rip]); - dq(CustomSkinPlayer_Hook + 0x5); - - L(f_Custom); - dq(uintptr_t(Custom)); - - L(f_ExecuteVisitorOnWorn); - dq(InventoryChanges_ExecuteVisitorOnWorn.address()); - - L(f_ShouldOverrideSkinning); - dq(uintptr_t(ShouldOverrideSkinning)); - } - }; - CustomSkinPlayer_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "AVI: Patching custom skin player at addr = {}. base = {}", CustomSkinPlayer_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(CustomSkinPlayer_Hook, code); - } - LOG(info, "Done"); - } - } // namespace CustomSkinPlayer - - namespace FixEquipConflictCheck { - // - // When you try to equip an item, the game loops over the armors in your ActorWeightModel - // rather than your other worn items. Because we're tampering with what goes into the AWM, - // this means that conflict checks are run against your outfit instead of your equipment, - // unless we patch in a fix. (For example, if your outfit doesn't include a helmet, then - // you'd be able to stack helmets endlessly without this patch.) - // - // The loop in question is performed in Actor::Unk_120, which is also generally responsible - // for equipping items at all. - // - class _Visitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // Bethesda used a visitor to add armor-addons to the ActorWeightModel in the first - // place (see call stack for DontVanillaSkinPlayer patch), so why not use a similar - // visitor to check for conflicts? - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - // TODO: [SlotPassthru] We might be able to leave this as-is. - auto form = data->object; - if (form && form->formType == RE::FormType::Armor) { - auto armor = reinterpret_cast(form); - if (armor->TestBodyPartByIndex(this->conflictIndex)) { - auto em = RE::ActorEquipManager::GetSingleton(); - // - // TODO: The third argument to this call is meant to be a - // BaseExtraList*, and Bethesda supplies one when calling from Unk_120. - // Can we get away with a nullptr here, or do we have to find the - // BaseExtraList that contains an ExtraWorn? - // - // I'm not sure how to investigate this, but I did run one test, and - // that works properly: I gave myself ten Falmer Helmets and applied - // different enchantments to two of them (leaving the others - // unenchanted). In tests, I was unable to stack the helmets with each - // other or with other helmets, suggesting that the BaseExtraList may - // not be strictly necessary. - // - em->UnequipObject(this->target, form, nullptr, 1, nullptr, false, false, true, false, nullptr); - } - } - return ReturnType::kContinue; // True to continue visiting - }; - - RE::Actor* target; - std::uint32_t conflictIndex = 0; - }; - void Inner(std::uint32_t bodyPartForNewItem, RE::Actor* target) { - auto inventory = target->GetInventoryChanges(); - if (inventory) { - _Visitor visitor; - visitor.conflictIndex = bodyPartForNewItem; - visitor.target = target; - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "OverridePlayerSkinning: Conflict check failed: no inventory!"); - } - } - bool ShouldOverride(RE::TESForm* item) { - // - // We only hijack equipping for armors, so I'd like for this patch to only - // apply to armors as well. It shouldn't really matter -- before I added - // this check, weapons and the like tested in-game with no issues, likely - // because they're handled very differently -- but I just wanna be sure. - // We should use vanilla code whenever we don't need to NOT use it. - // - return (item->formType == RE::FormType::Armor); - } - - REL::ID FixEquipConflictCheck_Hook_ID(38004); - std::uintptr_t FixEquipConflictCheck_Hook(FixEquipConflictCheck_Hook_ID.address() + 0x97); // 0x0060CAC7 in 1.5.73 - - REL::ID BGSBipedObjectForm_TestBodyPartByIndex(14119); // 0x001820A0 in 1.5.73 - - void Apply() { - LOG(info, "Patching fix for equip conflict check"); - LOG(info, "FixEquipConflictCheck_Hook = {:x}", FixEquipConflictCheck_Hook - REL::Module::get().base()); - LOG(info, "BGSBipedObjectForm_TestBodyPartByIndex = {:x}", BGSBipedObjectForm_TestBodyPartByIndex.address() - REL::Module::get().base()); - { - struct FixEquipConflictCheck_Code : Xbyak::CodeGenerator { - FixEquipConflictCheck_Code() { - Xbyak::Label j_Out; - Xbyak::Label j_Exit; - Xbyak::Label f_Inner; - Xbyak::Label f_TestBodyPartByIndex; - Xbyak::Label f_ShouldOverride; - - call(ptr[rip + f_TestBodyPartByIndex]); - test(al, al); - jz(j_Exit); - - // rsp+0x10: item - // rdi: Actor - // rbx: Body Slot - push(rcx); - // Set RCX to the RDX argument of the patched function - // RSP + Argument offset rel to original entry + Offset from push above - // + Offset from pushes in original entry - mov(rcx, ptr[rsp + 0x10 + 0x08 + 0xC8]); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverride]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - test(al, al); - mov(rax, 1); - jz(j_Out); - push(rcx); - push(rdx); - mov(rcx, rbx); - mov(rdx, rdi); - sub(rsp, 0x20); - call(ptr[rip + f_Inner]); - add(rsp, 0x20); - pop(rdx); - pop(rcx); - - L(j_Exit); - xor_(al, al); - - L(j_Out); - jmp(ptr[rip]); - dq(FixEquipConflictCheck_Hook + 0x5); - - L(f_TestBodyPartByIndex); - dq(BGSBipedObjectForm_TestBodyPartByIndex.address()); - - L(f_ShouldOverride); - dq(uintptr_t(ShouldOverride)); - - L(f_Inner); - dq(uintptr_t(Inner)); - } - }; - FixEquipConflictCheck_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "AVI: Patching fix equip conflict check at addr = {:x}. base = {:x}", FixEquipConflictCheck_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(FixEquipConflictCheck_Hook, code); - } - LOG(info, "Done"); - } - } // namespace FixEquipConflictCheck - - namespace RTTIPrinter { - void Print_RTTI(RE::InventoryChanges::IItemChangeVisitor* target) { - void* object = (void*)target; - void* vtable = *(void**)object; - void* info_block = ((void**)vtable)[-1]; - uintptr_t id = ((unsigned long*)info_block)[3]; - uintptr_t info = id + REL::Module::get().base(); - char* name = (char*)info + 16; - LOG(info, "vtable = {}, typeinfo = {}, typename = {}", vtable, info_block, - name); - } - - REL::ID InventoryChanges_ExecuteVisitorOnWorn(16096); // 0x001E51D0 in 1.5.73 - std::uintptr_t InventoryChanges_ExecuteVisitorOnWorn_Hook( - InventoryChanges_ExecuteVisitorOnWorn.address() + - 0x2A); // 0x00364301 in 1.5.73 - - void Apply() { - LOG(info, "Patching RTTI print"); - LOG(info, "InventoryChanges_ExecuteVisitorOnWorn_Hook = {:x}", - InventoryChanges_ExecuteVisitorOnWorn_Hook - REL::Module::get().base()); - { - struct RTTI_Code : Xbyak::CodeGenerator { - RTTI_Code() { - Xbyak::Label f_PrintRTTI; - - push(rcx); - push(rdx); - push(rax); - mov(rcx, rdx); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_PrintRTTI]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rax); - pop(rdx); - pop(rcx); - - jmp(ptr[rip]); - dq(InventoryChanges_ExecuteVisitorOnWorn_Hook + 0x6); - - L(f_PrintRTTI); - dq(uintptr_t(Print_RTTI)); - } - }; - RTTI_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "AVI: Patching RTTI print at addr = {:x}. base = {:x}", - InventoryChanges_ExecuteVisitorOnWorn_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<6>( - InventoryChanges_ExecuteVisitorOnWorn_Hook, code); - } - } - } // namespace RTTIPrinter - - void ApplyPlayerSkinningHooks() { - DontVanillaSkinPlayer::Apply(); - ShimWornFlags::Apply(); - CustomSkinPlayer::Apply(); - FixEquipConflictCheck::Apply(); - } -} // namespace OutfitSystem -#endif \ No newline at end of file diff --git a/src/hooking/PlayerSkinning_PRE_AE.cpp b/src/hooking/PlayerSkinning_PRE_AE.cpp deleted file mode 100644 index 4058050..0000000 --- a/src/hooking/PlayerSkinning_PRE_AE.cpp +++ /dev/null @@ -1,462 +0,0 @@ -#ifdef SKYRIM_VERSION_IS_PRE_AE - -#include "ArmorAddonOverrideService.h" - -#include - -namespace OutfitSystem { - SKSE::Trampoline* g_localTrampoline = nullptr; - SKSE::Trampoline* g_branchTrampoline = nullptr; - - bool ShouldOverrideSkinning(RE::TESObjectREFR* target) { - if (!ArmorAddonOverrideService::GetInstance().shouldOverride((RE::Actor*)target)) - return false; - return target == RE::PlayerCharacter::GetSingleton(); - } - - class EquippedArmorVisitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // If the player has a shield equipped, and if we're not overriding that - // shield, then we need to grab the equipped shield's worn-flags. - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - auto form = data->object; - if (form && form->formType == RE::FormType::Armor) { - equipped.emplace(skyrim_cast(form)); - } - return ReturnType::kContinue; // Return true to "continue visiting". - }; - - std::unordered_set equipped; - }; - - REL::ID TESObjectARMO_ApplyArmorAddon(17392); // 0x00228AD0 in 1.5.73 - - namespace DontVanillaSkinPlayer { - bool _stdcall ShouldOverride(RE::TESObjectARMO* armor, RE::TESObjectREFR* target) { - if (!ShouldOverrideSkinning(target)) - return false; - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit((RE::Actor*)target); - auto actor = skyrim_cast(target); - if (!actor) { - // Actor failed to cast... - LOG(info, "ShouldOverride: Failed to cast target to Actor."); - return true; - } - auto inventory = target->GetInventoryChanges(); - EquippedArmorVisitor visitor; - if (inventory) { - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "ShouldOverride: Unable to get target inventory."); - return true; - } - // Block the item (return true) if the item isn't in the display set. - return outfit.computeDisplaySet(visitor.equipped).count(armor) == 0; - } - - REL::ID DontVanillaSkinPlayer_Hook_ID(24232); - std::uintptr_t DontVanillaSkinPlayer_Hook(DontVanillaSkinPlayer_Hook_ID.address() + 0x302); // 0x00364652 in 1.5.73 - - void Apply() { - LOG(info, "Patching vanilla player skinning"); - LOG(info, "TESObjectARMO_ApplyArmorAddon = {:x}", TESObjectARMO_ApplyArmorAddon.address() - REL::Module::get().base()); - LOG(info, "DontVanillaSkinPlayer_Hook = {:x}", DontVanillaSkinPlayer_Hook - REL::Module::get().base()); - { - struct DontVanillaSkinPlayer_Code : Xbyak::CodeGenerator { - DontVanillaSkinPlayer_Code() { - Xbyak::Label j_Out; - Xbyak::Label f_ApplyArmorAddon; - Xbyak::Label f_ShouldOverride; - - // armor in rcx, target in r13 - push(rcx); - push(rdx); - push(r9); - push(r8); - mov(rdx, r13); - sub(rsp, 0x40); - call(ptr[rip + f_ShouldOverride]); - add(rsp, 0x40); - pop(r8); - pop(r9); - pop(rdx); - pop(rcx); - test(al, al); - jnz(j_Out); - call(ptr[rip + f_ApplyArmorAddon]); - L(j_Out); - jmp(ptr[rip]); - dq(DontVanillaSkinPlayer_Hook + 0x5); - - L(f_ApplyArmorAddon); - dq(TESObjectARMO_ApplyArmorAddon.address()); - - L(f_ShouldOverride); - dq(uintptr_t(ShouldOverride)); - } - }; - DontVanillaSkinPlayer_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "Patching vanilla player skinning at addr = {:x}. base = {:x}", DontVanillaSkinPlayer_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(DontVanillaSkinPlayer_Hook, code); - } - LOG(info, "Done"); - } - } // namespace DontVanillaSkinPlayer - - namespace ShimWornFlags { - std::uint32_t OverrideWornFlags(RE::InventoryChanges* inventory, RE::TESObjectREFR* target) { - std::uint32_t mask = 0; - auto actor = skyrim_cast(target); - if (!actor) return mask; - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit(actor); - EquippedArmorVisitor visitor; - inventory->ExecuteVisitorOnWorn(&visitor); - auto displaySet = outfit.computeDisplaySet(visitor.equipped); - for (auto& armor : displaySet) { - mask |= static_cast(armor->GetSlotMask()); - } - return mask; - } - - REL::ID ShimWornFlags_Hook_ID(24220); - std::uintptr_t ShimWornFlags_Hook(ShimWornFlags_Hook_ID.address() + 0x7C); // 0x00362F0C in 1.5.73 - - REL::ID InventoryChanges_GetWornMask(15806); // 0x001D9040 in 1.5.73 - - void Apply() { - LOG(info, "Patching shim worn flags"); - LOG(info, "ShimWornFlags_Hook = {:x}", ShimWornFlags_Hook - REL::Module::get().base()); - LOG(info, "InventoryChanges_GetWornMask = {:x}", InventoryChanges_GetWornMask.address() - REL::Module::get().base()); - { - struct ShimWornFlags_Code : Xbyak::CodeGenerator { - ShimWornFlags_Code() { - Xbyak::Label j_SuppressVanilla; - Xbyak::Label j_Out; - Xbyak::Label f_ShouldOverrideSkinning; - Xbyak::Label f_GetWornMask; - Xbyak::Label f_OverrideWornFlags; - - // target in rsi - push(rcx); - mov(rcx, rsi); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverrideSkinning]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - test(al, al); - jnz(j_SuppressVanilla); - call(ptr[rip + f_GetWornMask]); - jmp(j_Out); - - L(j_SuppressVanilla); - push(rdx); - mov(rdx, rsi); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_OverrideWornFlags]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rdx); - - L(j_Out); - jmp(ptr[rip]); - dq(ShimWornFlags_Hook + 0x5); - - L(f_ShouldOverrideSkinning); - dq(uintptr_t(ShouldOverrideSkinning)); - - L(f_GetWornMask); - dq(InventoryChanges_GetWornMask.address()); - - L(f_OverrideWornFlags); - dq(uintptr_t(OverrideWornFlags)); - } - }; - ShimWornFlags_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "Patching shim worn flags at addr = {:x}. base = {:x}", ShimWornFlags_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(ShimWornFlags_Hook, code); - } - LOG(info, "Done"); - } - } // namespace ShimWornFlags - - namespace CustomSkinPlayer { - void Custom(RE::Actor* target, RE::ActorWeightModel* actorWeightModel) { - if (!skyrim_cast(target)) { - // Actor failed to cast... - LOG(info, "Custom: Failed to cast target to Actor."); - return; - } - // Get basic actor information (race and sex) - if (!actorWeightModel) - return; - auto base = skyrim_cast(target->data.objectReference); - if (!base) - return; - auto race = base->race; - bool isFemale = base->IsFemale(); - // - auto& svc = ArmorAddonOverrideService::GetInstance(); - auto& outfit = svc.currentOutfit((RE::Actor*)target); - - // Get actor inventory and equipped items - auto inventory = target->GetInventoryChanges(); - EquippedArmorVisitor visitor; - if (inventory) { - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "Custom: Unable to get target inventory."); - return; - } - - // Compute the display set. - auto displaySet = outfit.computeDisplaySet(visitor.equipped); - - // Compute the remaining items to be applied to the player - // We assume that the DontVanillaSkinPlayer already passed through - // the equipped items that will be shown, so we only need to worry about the - // items that are not equipped but which are in the outfit or which are masked - // by the outfit. - std::unordered_set applySet; - for (const auto& item : displaySet) { - if (visitor.equipped.find(item) == visitor.equipped.end()) applySet.insert(item); - } - - for (auto it = applySet.cbegin(); it != applySet.cend(); ++it) { - // TODO: [SlotPassthru] Also do the same iteration over the passthrough equipped items? - RE::TESObjectARMO* armor = *it; - if (armor) { - armor->ApplyArmorAddon(race, actorWeightModel, isFemale); - } - } - } - - REL::ID CustomSkinPlayer_Hook_ID(24231); - std::uintptr_t CustomSkinPlayer_Hook(CustomSkinPlayer_Hook_ID.address() + 0x81); // 0x00364301 in 1.5.73 - - REL::ID InventoryChanges_ExecuteVisitorOnWorn(15856); // 0x001E51D0 in 1.5.73 - - void Apply() { - LOG(info, "Patching custom skin player"); - LOG(info, "CustomSkinPlayer_Hook = {:x}", CustomSkinPlayer_Hook - REL::Module::get().base()); - LOG(info, "InventoryChanges_ExecuteVisitorOnWorn = {:x}", InventoryChanges_ExecuteVisitorOnWorn.address() - REL::Module::get().base()); - { - struct CustomSkinPlayer_Code : Xbyak::CodeGenerator { - CustomSkinPlayer_Code() { - Xbyak::Label j_Out; - Xbyak::Label f_Custom; - Xbyak::Label f_ExecuteVisitorOnWorn; - Xbyak::Label f_ShouldOverrideSkinning; - - // call original function - call(ptr[rip + f_ExecuteVisitorOnWorn]); - - push(rcx); - mov(rcx, rbx); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverrideSkinning]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - - test(al, al); - jz(j_Out); - - push(rdx); - push(rcx); - mov(rcx, rbx); - mov(rdx, rdi); - sub(rsp, 0x20); - call(ptr[rip + f_Custom]); - add(rsp, 0x20); - pop(rcx); - pop(rdx); - - L(j_Out); - jmp(ptr[rip]); - dq(CustomSkinPlayer_Hook + 0x5); - - L(f_Custom); - dq(uintptr_t(Custom)); - - L(f_ExecuteVisitorOnWorn); - dq(InventoryChanges_ExecuteVisitorOnWorn.address()); - - L(f_ShouldOverrideSkinning); - dq(uintptr_t(ShouldOverrideSkinning)); - } - }; - CustomSkinPlayer_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "AVI: Patching custom skin player at addr = {}. base = {}", CustomSkinPlayer_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(CustomSkinPlayer_Hook, code); - } - LOG(info, "Done"); - } - } // namespace CustomSkinPlayer - - namespace FixEquipConflictCheck { - // - // When you try to equip an item, the game loops over the armors in your ActorWeightModel - // rather than your other worn items. Because we're tampering with what goes into the AWM, - // this means that conflict checks are run against your outfit instead of your equipment, - // unless we patch in a fix. (For example, if your outfit doesn't include a helmet, then - // you'd be able to stack helmets endlessly without this patch.) - // - // The loop in question is performed in Actor::Unk_120, which is also generally responsible - // for equipping items at all. - // - class _Visitor : public RE::InventoryChanges::IItemChangeVisitor { - // - // Bethesda used a visitor to add armor-addons to the ActorWeightModel in the first - // place (see call stack for DontVanillaSkinPlayer patch), so why not use a similar - // visitor to check for conflicts? - // - public: - virtual ReturnType Visit(RE::InventoryEntryData* data) override { - // TODO: [SlotPassthru] We might be able to leave this as-is. - auto form = data->object; - if (form && form->formType == RE::FormType::Armor) { - auto armor = reinterpret_cast(form); - if (armor->TestBodyPartByIndex(this->conflictIndex)) { - auto em = RE::ActorEquipManager::GetSingleton(); - // - // TODO: The third argument to this call is meant to be a - // BaseExtraList*, and Bethesda supplies one when calling from Unk_120. - // Can we get away with a nullptr here, or do we have to find the - // BaseExtraList that contains an ExtraWorn? - // - // I'm not sure how to investigate this, but I did run one test, and - // that works properly: I gave myself ten Falmer Helmets and applied - // different enchantments to two of them (leaving the others - // unenchanted). In tests, I was unable to stack the helmets with each - // other or with other helmets, suggesting that the BaseExtraList may - // not be strictly necessary. - // - em->UnequipObject(this->target, form, nullptr, 1, nullptr, false, false, true, false, nullptr); - } - } - return ReturnType::kContinue; // True to continue visiting - }; - - RE::Actor* target; - std::uint32_t conflictIndex = 0; - }; - void Inner(std::uint32_t bodyPartForNewItem, RE::Actor* target) { - auto inventory = target->GetInventoryChanges(); - if (inventory) { - _Visitor visitor; - visitor.conflictIndex = bodyPartForNewItem; - visitor.target = target; - inventory->ExecuteVisitorOnWorn(&visitor); - } else { - LOG(info, "OverridePlayerSkinning: Conflict check failed: no inventory!"); - } - } - bool ShouldOverride(RE::TESForm* item) { - // - // We only hijack equipping for armors, so I'd like for this patch to only - // apply to armors as well. It shouldn't really matter -- before I added - // this check, weapons and the like tested in-game with no issues, likely - // because they're handled very differently -- but I just wanna be sure. - // We should use vanilla code whenever we don't need to NOT use it. - // - return (item->formType == RE::FormType::Armor); - } - - REL::ID FixEquipConflictCheck_Hook_ID(36979); - std::uintptr_t FixEquipConflictCheck_Hook(FixEquipConflictCheck_Hook_ID.address() + 0x97); // 0x0060CAC7 in 1.5.73 - - REL::ID BGSBipedObjectForm_TestBodyPartByIndex(14026); // 0x001820A0 in 1.5.73 - - void Apply() { - LOG(info, "Patching fix for equip conflict check"); - LOG(info, "FixEquipConflictCheck_Hook = {:x}", FixEquipConflictCheck_Hook - REL::Module::get().base()); - LOG(info, "BGSBipedObjectForm_TestBodyPartByIndex = {:x}", BGSBipedObjectForm_TestBodyPartByIndex.address() - REL::Module::get().base()); - { - struct FixEquipConflictCheck_Code : Xbyak::CodeGenerator { - FixEquipConflictCheck_Code() { - Xbyak::Label j_Out; - Xbyak::Label j_Exit; - Xbyak::Label f_Inner; - Xbyak::Label f_TestBodyPartByIndex; - Xbyak::Label f_ShouldOverride; - - call(ptr[rip + f_TestBodyPartByIndex]); - test(al, al); - jz(j_Exit); - - // rsp+0x10: item - // rdi: Actor - // rbx: Body Slot - push(rcx); - // Set RCX to the RDX argument of the patched function - // RSP + Argument offset rel to original entry + Offset from push above - // + Offset from pushes in original entry - mov(rcx, ptr[rsp + 0x10 + 0x08 + 0xC8]); - sub(rsp, 0x8); // Ensure 16-byte alignment of stack pointer - sub(rsp, 0x20); - call(ptr[rip + f_ShouldOverride]); - add(rsp, 0x20); - add(rsp, 0x8); - pop(rcx); - test(al, al); - mov(rax, 1); - jz(j_Out); - push(rcx); - push(rdx); - mov(rcx, rbx); - mov(rdx, rdi); - sub(rsp, 0x20); - call(ptr[rip + f_Inner]); - add(rsp, 0x20); - pop(rdx); - pop(rcx); - - L(j_Exit); - xor_(al, al); - - L(j_Out); - jmp(ptr[rip]); - dq(FixEquipConflictCheck_Hook + 0x5); - - L(f_TestBodyPartByIndex); - dq(BGSBipedObjectForm_TestBodyPartByIndex.address()); - - L(f_ShouldOverride); - dq(uintptr_t(ShouldOverride)); - - L(f_Inner); - dq(uintptr_t(Inner)); - } - }; - FixEquipConflictCheck_Code gen; - void* code = g_localTrampoline->allocate(gen); - - LOG(info, "AVI: Patching fix equip conflict check at addr = {:x}. base = {:x}", FixEquipConflictCheck_Hook, REL::Module::get().base()); - g_branchTrampoline->write_branch<5>(FixEquipConflictCheck_Hook, code); - } - LOG(info, "Done"); - } - } // namespace FixEquipConflictCheck - - void ApplyPlayerSkinningHooks() { - DontVanillaSkinPlayer::Apply(); - ShimWornFlags::Apply(); - CustomSkinPlayer::Apply(); - FixEquipConflictCheck::Apply(); - } -} // namespace OutfitSystem -#endif \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index dcb6f80..9d68cbc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,47 +1,55 @@ #include "OutfitSystem.h" -#include - -#include -#include +#include "ArmorAddonOverrideService.h" +#include "hooking/Hooks.hpp" +#include "Utility.h" void WaitForDebugger(void) { - while (!IsDebuggerPresent()) { - Sleep(10); - } + while (!IsDebuggerPresent()) { + Sleep(10); + } - Sleep(1000 * 2); + Sleep(1000 * 2); } int ReportHook(int reportType, char* message, int* returnValue) { - // Got an error - util::report_and_fail(message); + // Got an error + util::report_and_fail(message); } namespace { - void InitializeLog() { - auto path = SKSE::log::log_directory(); - if (!path) { - util::report_and_fail("Failed to find standard logging directory"sv); - } + void InitializeLog() { + auto path = SKSE::log::log_directory(); + if (!path) { + util::report_and_fail("Failed to find standard logging directory"sv); + } - *path /= fmt::format("{}.log"sv, Plugin::NAME); - auto sink = std::make_shared(path->string(), true); + *path /= fmt::format("{}.log"sv, Plugin::NAME); + auto sink = std::make_shared(path->string(), true); -#ifndef NDEBUG - const auto level = spdlog::level::trace; -#else - const auto level = spdlog::level::info; -#endif + // Activate logging of everything until we load the log setting. + auto log = std::make_shared("global log"s, std::move(sink)); + log->set_level(spdlog::level::trace); + log->flush_on(spdlog::level::trace); - auto log = std::make_shared("global log"s, std::move(sink)); - log->set_level(level); - log->flush_on(level); + spdlog::set_default_logger(std::move(log)); + spdlog::set_pattern("%g(%#): [%^%l%$] %v"s); - spdlog::set_default_logger(std::move(log)); - spdlog::set_pattern("%g(%#): [%^%l%$] %v"s); - } -} + // Load the actual log setting we should use. + auto level = spdlog::level::info; + bool deepLogEnabled = Settings::Instance()->GetBoolean("Debug", "ExtraLogging", false); + if (deepLogEnabled) { + LOG(info, "Extra logging enabled."); + level = spdlog::level::trace; + } else { + LOG(info, "Extra logging disabled."); + } + + // Set the actual log setting from hereon. + spdlog::default_logger()->set_level(level); + spdlog::default_logger()->flush_on(level); + } +}// namespace std::uint32_t g_pluginSerializationSignature = 'cOft'; @@ -50,168 +58,172 @@ void Callback_Serialization_Save(SKSE::SerializationInterface* intfc); void Callback_Serialization_Load(SKSE::SerializationInterface* intfc); extern "C" { -#if SKYRIM_VERSION_IS_AE - // Plugin Query for AE - DllExport constinit auto SKSEPlugin_Version = []() { - SKSE::PluginVersionData v; +// Plugin Query for AE +DllExport constinit auto SKSEPlugin_Version = []() { + SKSE::PluginVersionData v; - v.PluginVersion(Plugin::VERSION); - v.PluginName(Plugin::NAME); + v.PluginVersion(Plugin::VERSION); + v.PluginName(Plugin::NAME); - v.UsesAddressLibrary(true); - v.CompatibleVersions({ SKSE::RUNTIME_LATEST }); + v.UsesAddressLibrary(true); + v.CompatibleVersions({SKSE::RUNTIME_SSE_LATEST_AE}); + v.UsesNoStructs(true); - return v; - }(); -#elif SKYRIM_VERSION_IS_PRE_AE - // Plugin Query for SE - DllExport bool SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) { - LOG(info, "Query: {} v{}", Plugin::NAME, Plugin::VERSION.string()); + return v; +}(); - a_info->infoVersion = SKSE::PluginInfo::kVersion; - a_info->name = "SkyrimOutfitSystemSE"; - a_info->version = 1; +// Plugin Query for SE +DllExport bool SKSEPlugin_Query(const SKSE::QueryInterface* a_skse, SKSE::PluginInfo* a_info) { + a_info->infoVersion = SKSE::PluginInfo::kVersion; + a_info->name = "SkyrimOutfitSystemSE"; + a_info->version = 1; - if (a_skse->IsEditor()) { - LOG(critical, "[FATAL ERROR] Loaded in editor, marking as incompatible!\n"); - return false; - } + if (a_skse->IsEditor()) { + LOG(critical, "[FATAL ERROR] Loaded in editor, marking as incompatible!\n"); + return false; + } - return true; - } -#endif + return true; +} - // Entry point - DllExport bool SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) { - InitializeLog(); - LOG(info, "Load: {} v{}", Plugin::NAME, Plugin::VERSION.string()); +// Entry point +DllExport bool SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) { + REL::Module::reset(); + InitializeLog(); + LOG(info, "Load: {} v{}", Plugin::NAME, Plugin::VERSION.string()); + + auto gameVersion = REL::Relocate("SE", "AE"); + LOG(info, "Game type: {}", gameVersion); + + auto version = REL::Module::get().version(); + LOG(info, "Game version: {}", version.string()); #ifdef _DEBUG - // Intercept Visual C++ exceptions, but only if we're developing. - _CrtSetReportHook(ReportHook); + // Intercept Visual C++ exceptions, but only if we're developing. + _CrtSetReportHook(ReportHook); #endif - SKSE::Init(a_skse); + SKSE::Init(a_skse); - // Check some interface versions - if (SKSE::GetMessagingInterface()->Version() < SKSE::MessagingInterface::kVersion) { - LOG(critical, "Messaging interface too old."); - return false; - } + // Check some interface versions + if (SKSE::GetMessagingInterface()->Version() < SKSE::MessagingInterface::kVersion) { + LOG(critical, "Messaging interface too old."); + return false; + } - if (SKSE::GetSerializationInterface()->Version() < SKSE::SerializationInterface::kVersion) { - LOG(critical, "Serialization interface too old."); - return false; - } + if (SKSE::GetSerializationInterface()->Version() < SKSE::SerializationInterface::kVersion) { + LOG(critical, "Serialization interface too old."); + return false; + } - // Create Trampolines - SKSE::AllocTrampoline(128, true); - OutfitSystem::g_branchTrampoline = &SKSE::GetTrampoline(); + // Create Trampolines + SKSE::AllocTrampoline(128, true); + Hooking::g_branchTrampoline = &SKSE::GetTrampoline(); - OutfitSystem::g_localTrampoline = new SKSE::Trampoline("local"); - OutfitSystem::g_localTrampoline->create(1024 * 64); + Hooking::g_localTrampoline = new SKSE::Trampoline("local"); + Hooking::g_localTrampoline->create(1024 * 64); - // Actual plugin load - LOG(info, "Patching player skinning"); - OutfitSystem::ApplyPlayerSkinningHooks(); + // Actual plugin load + LOG(info, "Patching player skinning"); + REL::Relocate(HookingPREAE::ApplyPlayerSkinningHooks, HookingAE::ApplyPlayerSkinningHooks)(); - // Messaging Callback - LOG(info, "Registering messaging callback"); - SKSE::GetMessagingInterface()->RegisterListener(Callback_Messaging_SKSE); + // Messaging Callback + LOG(info, "Registering messaging callback"); + SKSE::GetMessagingInterface()->RegisterListener(Callback_Messaging_SKSE); - // Serialization Callbacks - LOG(info, "Registering serialization callback"); - SKSE::GetSerializationInterface()->SetUniqueID(g_pluginSerializationSignature); - SKSE::GetSerializationInterface()->SetSaveCallback(Callback_Serialization_Save); - SKSE::GetSerializationInterface()->SetLoadCallback(Callback_Serialization_Load); + // Serialization Callbacks + LOG(info, "Registering serialization callback"); + SKSE::GetSerializationInterface()->SetUniqueID(g_pluginSerializationSignature); + SKSE::GetSerializationInterface()->SetSaveCallback(Callback_Serialization_Save); + SKSE::GetSerializationInterface()->SetLoadCallback(Callback_Serialization_Load); - // Papyrus Registrations - LOG(info, "Registering papyrus"); - SKSE::GetPapyrusInterface()->Register(OutfitSystem::RegisterPapyrus); + // Papyrus Registrations + LOG(info, "Registering papyrus"); + SKSE::GetPapyrusInterface()->Register(OutfitSystem::RegisterPapyrus); - return true; - } + return true; +} } void Callback_Messaging_SKSE(SKSE::MessagingInterface::Message* message) { - if (message->type == SKSE::MessagingInterface::kPostLoad) { - } else if (message->type == SKSE::MessagingInterface::kPostPostLoad) { - } else if (message->type == SKSE::MessagingInterface::kDataLoaded) { - } else if (message->type == SKSE::MessagingInterface::kNewGame) { - ArmorAddonOverrideService::GetInstance().reset(); - } else if (message->type == SKSE::MessagingInterface::kPreLoadGame) { - ArmorAddonOverrideService::GetInstance() - .reset(); // AAOS::load resets as well, but this is needed in case the save we're about to load doesn't have any AAOS data. - } + if (message->type == SKSE::MessagingInterface::kPostLoad) { + } else if (message->type == SKSE::MessagingInterface::kPostPostLoad) { + } else if (message->type == SKSE::MessagingInterface::kDataLoaded) { + } else if (message->type == SKSE::MessagingInterface::kNewGame) { + ArmorAddonOverrideService::GetInstance().reset(); + } else if (message->type == SKSE::MessagingInterface::kPreLoadGame) { + ArmorAddonOverrideService::GetInstance() + .reset();// AAOS::load resets as well, but this is needed in case the save we're about to load doesn't have any AAOS data. + } } void _assertWrite(bool result, const char* err); void _assertRead(bool result, const char* err); void Callback_Serialization_Save(SKSE::SerializationInterface* intfc) { - LOG(info, "Writing savedata..."); - // - if (intfc->OpenRecord(ArmorAddonOverrideService::signature, ArmorAddonOverrideService::kSaveVersionV4)) { - try { - auto& service = ArmorAddonOverrideService::GetInstance(); - const auto& data = service.save(); - const auto& data_ser = data.SerializeAsString(); - _assertWrite(intfc->WriteRecordData(data_ser.data(), static_cast(data_ser.size())), - "Failed to write proto into SKSE record."); - } catch (const ArmorAddonOverrideService::save_error& exception) { - LOG(info, "Save FAILED for ArmorAddonOverrideService."); - LOG(info, " - Exception string: %s", exception.what()); - } - } else - LOG(info, "Save FAILED for ArmorAddonOverrideService. Record didn't open."); - // - LOG(info, "Saving done!"); + LOG(info, "Writing savedata..."); + // + if (intfc->OpenRecord(ArmorAddonOverrideService::signature, ArmorAddonOverrideService::kSaveVersionV4)) { + try { + auto& service = ArmorAddonOverrideService::GetInstance(); + const auto& data = service.save(); + const auto& data_ser = data.SerializeAsString(); + _assertWrite(intfc->WriteRecordData(data_ser.data(), static_cast(data_ser.size())), + "Failed to write proto into SKSE record."); + } catch (const ArmorAddonOverrideService::save_error& exception) { + LOG(info, "Save FAILED for ArmorAddonOverrideService."); + LOG(info, " - Exception string: %s", exception.what()); + } + } else + LOG(info, "Save FAILED for ArmorAddonOverrideService. Record didn't open."); + // + LOG(info, "Saving done!"); } void Callback_Serialization_Load(SKSE::SerializationInterface* intfc) { - LOG(info, "Loading savedata..."); - // - std::uint32_t - type; // This IS correct. A std::uint32_t and a four-character ASCII string have the same length (and can be read interchangeably, it seems). - std::uint32_t version; - std::uint32_t length; - bool error = false; - // - while (!error && intfc->GetNextRecordInfo(type, version, length)) { - switch (type) { - case ArmorAddonOverrideService::signature: - try { - auto& service = ArmorAddonOverrideService::GetInstance(); - if (version >= ArmorAddonOverrideService::kSaveVersionV4) { - // Read data from protobuf. - std::vector buf; - buf.insert(buf.begin(), length, 0); - _assertRead(intfc->ReadRecordData(buf.data(), length) == length, "Failed to load protobuf."); + LOG(info, "Loading savedata..."); + // + std::uint32_t + type;// This IS correct. A std::uint32_t and a four-character ASCII string have the same length (and can be read interchangeably, it seems). + std::uint32_t version; + std::uint32_t length; + bool error = false; + // + while (!error && intfc->GetNextRecordInfo(type, version, length)) { + switch (type) { + case ArmorAddonOverrideService::signature: + try { + auto& service = ArmorAddonOverrideService::GetInstance(); + if (version >= ArmorAddonOverrideService::kSaveVersionV4) { + // Read data from protobuf. + std::vector buf; + buf.insert(buf.begin(), length, 0); + _assertRead(intfc->ReadRecordData(buf.data(), length) == length, "Failed to load protobuf."); - // Parse data in protobuf. - proto::OutfitSystem data; - _assertRead(data.ParseFromArray(buf.data(), static_cast(buf.size())), - "Failed to parse protobuf."); + // Parse data in protobuf. + proto::OutfitSystem data; + _assertRead(data.ParseFromArray(buf.data(), static_cast(buf.size())), + "Failed to parse protobuf."); - // Load data from protobuf struct. - service.load(intfc, data); - } else { - service.load_legacy(intfc, version); - } - } catch (const ArmorAddonOverrideService::load_error& exception) { - LOG(info, "Load FAILED for ArmorAddonOverrideService."); - LOG(info, " - Exception string: %s", exception.what()); - } - break; - default: - LOG(info, "Loading: Unhandled type %c%c%c%c", - (char)(type >> 0x18), - (char)(type >> 0x10), - (char)(type >> 0x8), - (char)type); - error = true; - break; - } - } - // - LOG(info, "Loading done!"); + // Load data from protobuf struct. + service.load(intfc, data); + } else { + service.load_legacy(intfc, version); + } + } catch (const ArmorAddonOverrideService::load_error& exception) { + LOG(info, "Load FAILED for ArmorAddonOverrideService."); + LOG(info, " - Exception string: %s", exception.what()); + } + break; + default: + LOG(info, "Loading: Unhandled type %c%c%c%c", + (char) (type >> 0x18), + (char) (type >> 0x10), + (char) (type >> 0x8), + (char) type); + error = true; + break; + } + } + // + LOG(info, "Loading done!"); } \ No newline at end of file diff --git a/src/papyrus/skyoutsysautoswitchtrigger.psc b/src/papyrus/skyoutsysautoswitchtrigger.psc index d038f8c..d673088 100644 --- a/src/papyrus/skyoutsysautoswitchtrigger.psc +++ b/src/papyrus/skyoutsysautoswitchtrigger.psc @@ -5,7 +5,7 @@ Event OnLocationChange(Location akOldLoc, Location akNewLoc) ; Debug.Trace("SOS: Running OnLocationChange") GoToState("Waiting") Utility.Wait(10.0) - SkyrimOutfitSystemNativeFuncs.SetOutfitUsingLocation(Game.GetPlayer().GetCurrentLocation(), Weather.GetCurrentWeather()) + SkyrimOutfitSystemNativeFuncs.SetOutfitUsingLocation(Game.GetPlayer(), Game.GetPlayer().GetCurrentLocation(), Weather.GetCurrentWeather()) SkyrimOutfitSystemNativeFuncs.RefreshArmorFor(Game.GetPlayer()) GoToState("Listening") endEvent diff --git a/src/papyrus/skyoutsysmcm.psc b/src/papyrus/skyoutsysmcm.psc index 693be76..0f9e09d 100644 --- a/src/papyrus/skyoutsysmcm.psc +++ b/src/papyrus/skyoutsysmcm.psc @@ -5,6 +5,9 @@ Int _iOutfitNameMaxBytes = 256 ; should never change at run-time; can chang String[] _sOutfitNames String _sSelectedOutfit = "" +Actor _aCurrentActor +Actor[] _kActorSelection_SelectCandidates + String _sEditingOutfit = "" String _sOutfitShowingContextMenu = "" Int _iOutfitEditorBodySlotPage = 0 @@ -45,7 +48,7 @@ Event OnConfigOpen() RefreshCache() EndEvent Event OnConfigClose() - SkyrimOutfitSystemNativeFuncs.RefreshArmorFor(Game.GetPlayer()) + SkyrimOutfitSystemNativeFuncs.RefreshArmorForAllConfiguredActors() ResetOutfitBrowser() ResetOutfitEditor() EndEvent @@ -72,7 +75,7 @@ EndFunction Function RefreshCache() _sOutfitNames = SkyrimOutfitSystemNativeFuncs.ListOutfits() - _sSelectedOutfit = SkyrimOutfitSystemNativeFuncs.GetSelectedOutfit() + _sSelectedOutfit = SkyrimOutfitSystemNativeFuncs.GetSelectedOutfit(_aCurrentActor) ; _sOutfitNames = SkyrimOutfitSystemNativeFuncs.NaturalSort_ASCII(_sOutfitNames) EndFunction @@ -80,6 +83,8 @@ EndFunction Function ResetOutfitBrowser() _iOutfitBrowserPage = 0 _iOutfitEditorBodySlotPage = 0 + _kActorSelection_SelectCandidates = new Actor[1] + _aCurrentActor = Game.GetPlayer() _sEditingOutfit = "" _sOutfitShowingContextMenu = "" _sOutfitNames = new String[1] @@ -189,12 +194,12 @@ EndFunction EndIf Int iAutoswitchIndex = StringUtil.Substring(sState, 19) as Int If aiIndex == -1 ; user wants no outfit - SkyrimOutfitSystemNativeFuncs.UnsetLocationOutfit(iAutoswitchIndex) + SkyrimOutfitSystemNativeFuncs.UnsetLocationOutfit(_aCurrentActor, iAutoswitchIndex) SetMenuOptionValueST("$SkyOutSys_AutoswitchEdit_None") Else ; set the requested outfit String sOutfitName = _sOutfitNames[aiIndex] - SkyrimOutfitSystemNativeFuncs.SetLocationOutfit(iAutoswitchIndex, sOutfitName) - SetMenuOptionValueST(SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(iAutoswitchIndex)) + SkyrimOutfitSystemNativeFuncs.SetLocationOutfit(_aCurrentActor, iAutoswitchIndex, sOutfitName) + SetMenuOptionValueST(SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(_aCurrentActor, iAutoswitchIndex)) EndIf Return EndIf @@ -220,7 +225,7 @@ EndFunction Int iAutoswitchIndex = StringUtil.Substring(sState, 19) as Int Bool bDelete = ShowMessage("$SkyOutSys_Confirm_UnsetAutoswitch_Text", True, "$SkyOutSys_Confirm_UnsetAutoswitch_Yes", "$SkyOutSys_Confirm_UnsetAutoswitch_No") If bDelete - SkyrimOutfitSystemNativeFuncs.UnsetLocationOutfit(iAutoswitchIndex) + SkyrimOutfitSystemNativeFuncs.UnsetLocationOutfit(_aCurrentActor, iAutoswitchIndex) SetMenuOptionValueST("") EndIf Return @@ -233,7 +238,12 @@ EndFunction ;/Block/; ; Left column SetCursorFillMode(TOP_TO_BOTTOM) SetCursorPosition(0) + If SKSE.GetPluginVersion("SkyrimOutfitSystemSE") == -1 + AddHeaderOption("$SkyOutSys_Text_WarningHeader") + return + EndIf AddToggleOptionST("OPT_Enabled", "$Enabled", SkyrimOutfitSystemNativeFuncs.IsEnabled()) + AddMenuOptionST("OPT_SelectActorSelection", "$SkyOutSys_Text_SelectActorSelection", _aCurrentActor.GetBaseObject().GetName()) AddEmptyOption() ; ; Quickslots: @@ -243,6 +253,13 @@ EndFunction AddToggleOptionST("OPT_QuickslotsEnabled", "$SkyOutSys_Text_EnableQuickslots", kQM.GetEnabled()) AddEmptyOption() ; + ; Active actor selection + ; + AddHeaderOption("$SkyOutSys_Text_ActiveActorHeader") + AddMenuOptionST("OPT_AddActorSelection", "$SkyOutSys_Text_AddActorSelection", "") + AddMenuOptionST("OPT_RemoveActorSelection", "$SkyOutSys_Text_RemoveActorSelection", "") + AddEmptyOption() + ; ; Setting import/export ; AddHeaderOption("$SkyOutSys_Text_SettingExportImport") @@ -251,20 +268,22 @@ EndFunction ;/EndBlock/; ;/Block/; ; Right column SetCursorPosition(1) - AddHeaderOption("$SkyOutSys_MCMHeader_Autoswitch") - Int[] iIndices = SkyrimOutfitSystemNativeFuncs.GetAutoSwitchLocationArray() - Int iCount = iIndices.Length - AddToggleOptionST("OPT_AutoswitchEnabled", "$SkyOutSys_Text_EnableAutoswitch", SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled()) - If SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled() - Int iIterator = 0 - While iIterator < iCount - String sLocationOutfit = SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(iIndices[iIterator]) - If sLocationOutfit == "" - sLocationOutfit = "$SkyOutSys_AutoswitchEdit_None" - EndIf - AddMenuOptionST("OPT_AutoswitchEntry" + iIndices[iIterator], "$SkyOutSys_Text_Autoswitch" + iIndices[iIterator], sLocationOutfit) - iIterator = iIterator + 1 - EndWhile + If _aCurrentActor == Game.GetPlayer() + AddHeaderOption("$SkyOutSys_MCMHeader_Autoswitch") + Int[] iIndices = SkyrimOutfitSystemNativeFuncs.GetAutoSwitchLocationArray() + Int iCount = iIndices.Length + AddToggleOptionST("OPT_AutoswitchEnabled", "$SkyOutSys_Text_EnableAutoswitch", SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled()) + If SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled() + Int iIterator = 0 + While iIterator < iCount + String sLocationOutfit = SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(_aCurrentActor, iIndices[iIterator]) + If sLocationOutfit == "" + sLocationOutfit = "$SkyOutSys_AutoswitchEdit_None" + EndIf + AddMenuOptionST("OPT_AutoswitchEntry" + iIndices[iIterator], "$SkyOutSys_Text_Autoswitch" + iIndices[iIterator], sLocationOutfit) + iIterator = iIterator + 1 + EndWhile + EndIf EndIf ;/EndBlock/; @@ -277,6 +296,90 @@ EndFunction SetToggleOptionValueST(bToggle) EndEvent EndState + State OPT_AddActorSelection + Event OnMenuOpenST() + _kActorSelection_SelectCandidates = SkyrimOutfitSystemNativeFuncs.ActorNearPC() + String[] kActorNames = Utility.CreateStringArray(_kActorSelection_SelectCandidates.Length) + Int iIterator = 0 + While iIterator < _kActorSelection_SelectCandidates.Length + kActorNames[iIterator] = _kActorSelection_SelectCandidates[iIterator].GetActorBase().GetName() + iIterator = iIterator + 1 + EndWhile + String[] sMenu = PrependStringToArray(kActorNames, "$SkyOutSys_OEdit_AddCancel") + SetMenuDialogOptions(sMenu) + SetMenuDialogStartIndex(0) + SetMenuDialogDefaultIndex(0) + EndEvent + Event OnMenuAcceptST(Int aiIndex) + If aiIndex == 0 || aiIndex > _kActorSelection_SelectCandidates.Length + return + Endif + SkyrimOutfitSystemNativeFuncs.AddActor(_kActorSelection_SelectCandidates[aiIndex - 1]) + EndEvent + Event OnDefaultST() + EndEvent + Event OnHighlightST() + SetInfoText("$SkyOutSys_Desc_AddActor") + EndEvent + EndState + State OPT_RemoveActorSelection + Event OnMenuOpenST() + _kActorSelection_SelectCandidates = SkyrimOutfitSystemNativeFuncs.ListActors() + String[] kActorNames = Utility.CreateStringArray(_kActorSelection_SelectCandidates.Length) + Int iIterator = 0 + While iIterator < _kActorSelection_SelectCandidates.Length + kActorNames[iIterator] = _kActorSelection_SelectCandidates[iIterator].GetActorBase().GetName() + iIterator = iIterator + 1 + EndWhile + String[] sMenu = PrependStringToArray(kActorNames, "$SkyOutSys_OEdit_AddCancel") + SetMenuDialogOptions(sMenu) + SetMenuDialogStartIndex(0) + SetMenuDialogDefaultIndex(0) + EndEvent + Event OnMenuAcceptST(Int aiIndex) + If aiIndex == 0 || aiIndex > _kActorSelection_SelectCandidates.Length + return + Endif + If _kActorSelection_SelectCandidates[aiIndex - 1] == Game.GetPlayer() + return + Endif + SkyrimOutfitSystemNativeFuncs.RemoveActor(_kActorSelection_SelectCandidates[aiIndex - 1]) + SkyrimOutfitSystemNativeFuncs.RefreshArmorFor(_kActorSelection_SelectCandidates[aiIndex - 1]) + EndEvent + Event OnDefaultST() + EndEvent + Event OnHighlightST() + SetInfoText("$SkyOutSys_Desc_RemoveActor") + EndEvent + EndState + State OPT_SelectActorSelection + Event OnMenuOpenST() + _kActorSelection_SelectCandidates = SkyrimOutfitSystemNativeFuncs.ListActors() + String[] kActorNames = Utility.CreateStringArray(_kActorSelection_SelectCandidates.Length) + Int iIterator = 0 + While iIterator < _kActorSelection_SelectCandidates.Length + kActorNames[iIterator] = _kActorSelection_SelectCandidates[iIterator].GetActorBase().GetName() + iIterator = iIterator + 1 + EndWhile + String[] sMenu = PrependStringToArray(kActorNames, "$SkyOutSys_OEdit_AddCancel") + SetMenuDialogOptions(sMenu) + SetMenuDialogStartIndex(0) + SetMenuDialogDefaultIndex(0) + EndEvent + Event OnMenuAcceptST(Int aiIndex) + If aiIndex == 0 || aiIndex > _kActorSelection_SelectCandidates.Length + return + Endif + _aCurrentActor = _kActorSelection_SelectCandidates[aiIndex - 1] + RefreshCache() + ForcePageReset() + EndEvent + Event OnDefaultST() + EndEvent + Event OnHighlightST() + SetInfoText("$SkyOutSys_Desc_ActorSelect") + EndEvent + EndState State OPT_QuickslotsEnabled Event OnSelectST() SkyOutSysQuickslotManager kQM = GetQuickslotManager() @@ -479,9 +582,9 @@ EndFunction State OutfitContext_Toggle Event OnSelectST() If _sSelectedOutfit == _sOutfitShowingContextMenu - SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit("") + SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit(_aCurrentActor, "") Else - SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit(_sOutfitShowingContextMenu) + SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit(_aCurrentActor, _sOutfitShowingContextMenu) EndIf RefreshCache() ForcePageReset() diff --git a/src/papyrus/skyoutsysquicksloteffect.psc b/src/papyrus/skyoutsysquicksloteffect.psc index 259c287..cca5ca9 100644 --- a/src/papyrus/skyoutsysquicksloteffect.psc +++ b/src/papyrus/skyoutsysquicksloteffect.psc @@ -21,14 +21,14 @@ Event OnEffectStart(Actor akCaster, Actor akTarget) result = "" Endif If result != "[DISMISS]" - SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit(result) + SkyrimOutfitSystemNativeFuncs.SetSelectedOutfit(Game.GetPlayer(), result) ; Update the autoswitch slot if ; 1) autoswitching is enabled, ; 2) the current location has an outfit assigned already, and ; 3) if we have an outfit selected in this menu Int playerLocationType = SkyrimOutfitSystemNativeFuncs.IdentifyLocationType(Game.GetPlayer().GetCurrentLocation(), Weather.GetCurrentWeather()) - If SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled() && SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(playerLocationType) != "" && result != "" - SkyrimOutfitSystemNativeFuncs.SetLocationOutfit(playerLocationType, result) + If SkyrimOutfitSystemNativeFuncs.GetLocationBasedAutoSwitchEnabled() && SkyrimOutfitSystemNativeFuncs.GetLocationOutfit(Game.GetPlayer(), playerLocationType) != "" && result != "" + SkyrimOutfitSystemNativeFuncs.SetLocationOutfit(Game.GetPlayer(), playerLocationType, result) Debug.Notification("This outfit will be remembered for this location type.") EndIf SkyrimOutfitSystemNativeFuncs.RefreshArmorFor(Game.GetPlayer()) diff --git a/src/papyrus/skyrimoutfitsystemnativefuncs.psc b/src/papyrus/skyrimoutfitsystemnativefuncs.psc index 8b23f9f..dbe69bd 100644 --- a/src/papyrus/skyrimoutfitsystemnativefuncs.psc +++ b/src/papyrus/skyrimoutfitsystemnativefuncs.psc @@ -27,11 +27,11 @@ Int Function GetOutfitNameMaxLength() Global Native Armor[] Function GetCarriedArmor (Actor akSubject) Global Native Armor[] Function GetWornItems (Actor akSubject) Global Native Function RefreshArmorFor (Actor akSubject) Global Native ; force akSubject to update their ArmorAddons - + Function RefreshArmorForAllConfiguredActors () Global Native ; force all known actors to update their ArmorAddons ; ; Searching for actors. Used in menus. ; -Actor[] Function GetActorNearPC () Global Native +Actor[] Function ActorNearPC () Global Native ; ; Search through all armor forms defined in the game (excluding templated ones). @@ -75,7 +75,7 @@ Bool Function GetOutfitPassthroughStatus(String asOutfitName) Global Native Function SetOutfitPassthroughStatus(String asOutfitName, Bool abPassthrough) Global Native Bool Function GetOutfitEquipRequiredStatus(String asOutfitName) Global Native Function SetOutfitEquipRequiredStatus(String asOutfitName, Bool asEquipRequired) Global Native -String Function GetSelectedOutfit () Global Native +String Function GetSelectedOutfit (Actor actor) Global Native Bool Function IsEnabled () Global Native String[] Function ListOutfits (Bool favoritesOnly = False) Global Native Function RemoveArmorFromOutfit (String asOutfitName, Armor akArmor) Global Native @@ -84,17 +84,18 @@ Bool Function RenameOutfit (String asOutfitName, String asRenameTo) Glo Bool Function OutfitExists (String asOutfitName) Global Native Function OverwriteOutfit (String asOutfitName, Armor[] akArmors) Global Native Function SetEnabled (Bool abEnabled) Global Native - Function SetSelectedOutfit (String asOutfitName) Global Native + Function SetSelectedOutfit (Actor actor, String asOutfitName) Global Native Function AddActor (Actor akSubject) Global Native Function RemoveActor (Actor akSubject) Global Native +Actor[] Function ListActors() Global Native Function SetLocationBasedAutoSwitchEnabled (Bool abEnabled) Global Native Bool Function GetLocationBasedAutoSwitchEnabled () Global Native Int[] Function GetAutoSwitchLocationArray () Global Native Int Function IdentifyLocationType (Location alLocation, Weather awWeather) Global Native - Function SetOutfitUsingLocation (Location alLocation, Weather awWeather) Global Native - Function SetLocationOutfit (Int aiLocationType, String asOutfitName) Global Native - Function UnsetLocationOutfit (Int aiLocationType) Global Native -String Function GetLocationOutfit (Int aiLocationType) Global Native + Function SetOutfitUsingLocation (Actor actor, Location alLocation, Weather awWeather) Global Native + Function SetLocationOutfit (Actor actor, Int aiLocationType, String asOutfitName) Global Native + Function UnsetLocationOutfit (Actor actor, Int aiLocationType) Global Native +String Function GetLocationOutfit (Actor actor, Int aiLocationType) Global Native Bool Function ExportSettings () Global Native Bool Function ImportSettings () Global Native diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 0000000..58dc7a5 --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,16 @@ +{ + "registries": [ + { + "kind": "git", + "repository": "https://gitlab.com/colorglass/vcpkg-colorglass", + "baseline": "1b279b20c7e0db1c9d549ff3b64e024c01317b55", + "packages": [ + "commonlibsse-ng", + "commonlibsse-ng-ae", + "commonlibsse-ng-se", + "commonlibsse-ng-vr", + "commonlibsse-ng-flatrim" + ] + } + ] +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 3ed12a5..f6d31fc 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -6,10 +6,12 @@ "dependencies": [ "span-lite", "spdlog", + "inih", "protobuf", "boost-stl-interfaces", "rsm-binary-io", - "xbyak" + "xbyak", + "commonlibsse-ng-flatrim" ], "builtin-baseline": "687f4aab11df9b1a854d0d7207c558da545b4cc9" }